Index: 3rdParty_sources/undertow/io/undertow/Handlers.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/Handlers.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/Handlers.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,512 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.predicate.Predicate; +import io.undertow.predicate.PredicateParser; +import io.undertow.predicate.PredicatesHandler; +import io.undertow.server.HttpHandler; +import io.undertow.server.JvmRouteHandler; +import io.undertow.server.RoutingHandler; +import io.undertow.server.handlers.AccessControlListHandler; +import io.undertow.server.handlers.DateHandler; +import io.undertow.server.handlers.DisableCacheHandler; +import io.undertow.server.handlers.ExceptionHandler; +import io.undertow.server.handlers.GracefulShutdownHandler; +import io.undertow.server.handlers.HttpContinueAcceptingHandler; +import io.undertow.server.handlers.HttpContinueReadHandler; +import io.undertow.server.handlers.HttpTraceHandler; +import io.undertow.server.handlers.IPAddressAccessControlHandler; +import io.undertow.server.handlers.NameVirtualHostHandler; +import io.undertow.server.handlers.PathHandler; +import io.undertow.server.handlers.PathTemplateHandler; +import io.undertow.server.handlers.PredicateContextHandler; +import io.undertow.server.handlers.PredicateHandler; +import io.undertow.server.handlers.ProxyPeerAddressHandler; +import io.undertow.server.handlers.RedirectHandler; +import io.undertow.server.handlers.RequestDumpingHandler; +import io.undertow.server.handlers.RequestLimit; +import io.undertow.server.handlers.RequestLimitingHandler; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.server.handlers.SetAttributeHandler; +import io.undertow.server.handlers.SetHeaderHandler; +import io.undertow.server.handlers.URLDecodingHandler; +import io.undertow.server.handlers.builder.PredicatedHandler; +import io.undertow.server.handlers.proxy.ProxyClient; +import io.undertow.server.handlers.proxy.ProxyHandler; +import io.undertow.server.handlers.resource.ResourceHandler; +import io.undertow.server.handlers.resource.ResourceManager; +import io.undertow.websockets.WebSocketConnectionCallback; +import io.undertow.websockets.WebSocketProtocolHandshakeHandler; + +import java.util.List; + +/** + * Utility class with convenience methods for dealing with handlers + * + * @author Stuart Douglas + */ +public class Handlers { + + /** + * Creates a new path handler, with the default handler specified + * + * @param defaultHandler The default handler + * @return A new path handler + */ + public static PathHandler path(final HttpHandler defaultHandler) { + return new PathHandler(defaultHandler); + } + + /** + * Creates a new path handler + * + * @return A new path handler + */ + public static PathHandler path() { + return new PathHandler(); + } + + /** + * + * @return a new path template handler + */ + public static PathTemplateHandler pathTemplate() { + return new PathTemplateHandler(); + } + + /** + * + * @param rewriteQueryParams If the query params should be rewritten + * @return The routing handler + */ + public static RoutingHandler routing(boolean rewriteQueryParams) { + return new RoutingHandler(rewriteQueryParams); + } + + /** + * + * @return a new routing handler + */ + public static RoutingHandler routing() { + return new RoutingHandler(); + } + + /** + * + * @param rewriteQueryParams If the query params should be rewritten + * @return The path template handler + */ + public static PathTemplateHandler pathTemplate(boolean rewriteQueryParams) { + return new PathTemplateHandler(rewriteQueryParams); + } + + + /** + * Creates a new virtual host handler + * + * @return A new virtual host handler + */ + public static NameVirtualHostHandler virtualHost() { + return new NameVirtualHostHandler(); + } + + /** + * Creates a new virtual host handler using the provided default handler + * + * @return A new virtual host handler + */ + public static NameVirtualHostHandler virtualHost(final HttpHandler defaultHandler) { + return new NameVirtualHostHandler().setDefaultHandler(defaultHandler); + } + + /** + * Creates a new virtual host handler that uses the provided handler as the root handler for the given hostnames. + * + * @param hostHandler The host handler + * @param hostnames The host names + * @return A new virtual host handler + */ + public static NameVirtualHostHandler virtualHost(final HttpHandler hostHandler, String... hostnames) { + NameVirtualHostHandler handler = new NameVirtualHostHandler(); + for (String host : hostnames) { + handler.addHost(host, hostHandler); + } + return handler; + } + + /** + * Creates a new virtual host handler that uses the provided handler as the root handler for the given hostnames. + * + * @param defaultHandler The default handler + * @param hostHandler The host handler + * @param hostnames The host names + * @return A new virtual host handler + */ + public static NameVirtualHostHandler virtualHost(final HttpHandler defaultHandler, final HttpHandler hostHandler, String... hostnames) { + return virtualHost(hostHandler, hostnames).setDefaultHandler(defaultHandler); + } + + /** + * @param sessionHandler The web socket session handler + * @return The web socket handler + */ + public static WebSocketProtocolHandshakeHandler websocket(final WebSocketConnectionCallback sessionHandler) { + return new WebSocketProtocolHandshakeHandler(sessionHandler); + } + + /** + * @param sessionHandler The web socket session handler + * @param next The handler to invoke if the web socket connection fails + * @return The web socket handler + */ + public static WebSocketProtocolHandshakeHandler websocket(final WebSocketConnectionCallback sessionHandler, final HttpHandler next) { + return new WebSocketProtocolHandshakeHandler(sessionHandler, next); + } + + /** + * Return a new resource handler + * + * @param resourceManager The resource manager to use + * @return A new resource handler + */ + public static ResourceHandler resource(final ResourceManager resourceManager) { + return new ResourceHandler(resourceManager).setDirectoryListingEnabled(false); + } + + /** + * Returns a new redirect handler + * + * @param location The redirect location + * @return A new redirect handler + */ + public static RedirectHandler redirect(final String location) { + return new RedirectHandler(location); + } + + /** + * Returns a new HTTP trace handler. This handler will handle HTTP TRACE + * requests as per the RFC. + *

+ * WARNING: enabling trace requests may leak information, in general it is recommended that + * these be disabled for security reasons. + * + * @param next The next handler in the chain + * @return A HTTP trace handler + */ + public static HttpTraceHandler trace(final HttpHandler next) { + return new HttpTraceHandler(next); + } + + /** + * Returns a new HTTP handler that sets the Date: header. + * + * This is no longer necessary, as it is handled by the connectors directly. + * + * @param next The next handler in the chain + * @return A new date handler + */ + @Deprecated + public static DateHandler date(final HttpHandler next) { + return new DateHandler(next); + } + + /** + * Returns a new predicate handler, that will delegate to one of the two provided handlers based on the value of the + * provided predicate. + * + * @param predicate The predicate + * @param trueHandler The handler that will be executed if the predicate is true + * @param falseHandler The handler that will be exected if the predicate is false + * @return A new predicate handler + * @see Predicate + * @see io.undertow.predicate.Predicates + */ + public static PredicateHandler predicate(final Predicate predicate, final HttpHandler trueHandler, final HttpHandler falseHandler) { + return new PredicateHandler(predicate, trueHandler, falseHandler); + } + + /** + * @param next The next handler + * @return a handler that sets up a new predicate context + */ + public static HttpHandler predicateContext(HttpHandler next) { + return new PredicateContextHandler(next); + } + + public static PredicatesHandler predicates(final List handlers, HttpHandler next) { + final PredicatesHandler predicatesHandler = new PredicatesHandler(next); + for(PredicatedHandler handler : handlers) { + predicatesHandler.addPredicatedHandler(handler); + } + return predicatesHandler; + } + + /** + * Returns a handler that sets a response header + * + * @param next The next handler in the chain + * @param headerName The name of the header + * @param headerValue The header value + * @return A new set header handler + */ + public static SetHeaderHandler header(final HttpHandler next, final String headerName, final String headerValue) { + return new SetHeaderHandler(next, headerName, headerValue); + } + + /** + * Returns a new handler that can allow or deny access to a resource based on IP address + * + * @param next The next handler in the chain + * @param defaultAllow Determine if a non-matching address will be allowed by default + * @return A new IP access control handler + */ + public static final IPAddressAccessControlHandler ipAccessControl(final HttpHandler next, boolean defaultAllow) { + return new IPAddressAccessControlHandler(next).setDefaultAllow(defaultAllow); + } + + /** + * Returns a new handler that can allow or deny access to a resource based an at attribute of the exchange + * + * @param next The next handler in the chain + * @param defaultAllow Determine if a non-matching user agent will be allowed by default + * @return A new user agent access control handler + */ + public static final AccessControlListHandler acl(final HttpHandler next, boolean defaultAllow, ExchangeAttribute attribute) { + return new AccessControlListHandler(next, attribute).setDefaultAllow(defaultAllow); + } + + /** + * A handler that automatically handles HTTP 100-continue responses, by sending a continue + * response when the first attempt is made to read from the request channel. + * + * @param next The next handler in the chain + * @return A new continue handler + */ + public static final HttpContinueReadHandler httpContinueRead(final HttpHandler next) { + return new HttpContinueReadHandler(next); + } + + /** + * Returns a handler that sends back a HTTP 100 continue response if the given predicate resolves to true. + * + * This handler differs from the one returned by {@link #httpContinueRead(io.undertow.server.HttpHandler)} in + * that it will eagerly send the response, and not wait for the first read attempt. + * + * @param next The next handler + * @param accept The predicate used to determine if the request should be accepted + * @return The accepting handler + */ + public static final HttpContinueAcceptingHandler httpContinueAccepting(final HttpHandler next, final Predicate accept) { + return new HttpContinueAcceptingHandler(next, accept); + } + + /** + * Returns a handler that sends back a HTTP 100 continue response to all requests. + * + * This handler differs from the one returned by {@link #httpContinueRead(io.undertow.server.HttpHandler)} in + * that it will eagerly send the response, and not wait for the first read attempt. + * + * @param next The next handler + * @return The accepting handler + */ + public static final HttpContinueAcceptingHandler httpContinueAccepting(final HttpHandler next) { + return new HttpContinueAcceptingHandler(next); + } + + /** + * A handler that will decode the URL, query parameters and to the specified charset. + *

+ * If you are using this handler you must set the {@link io.undertow.UndertowOptions#DECODE_URL} parameter to false. + *

+ * This is not as efficient as using the parsers built in UTF-8 decoder. Unless you need to decode to something other + * than UTF-8 you should rely on the parsers decoding instead. + * + * @param next The next handler in the chain + * @param charset The charset to decode to + * @return a new url decoding handler + */ + public static final URLDecodingHandler urlDecoding(final HttpHandler next, final String charset) { + return new URLDecodingHandler(next, charset); + } + + /** + * Returns an attribute setting handler that can be used to set an arbitrary attribute on the exchange. + * This includes functions such as adding and removing headers etc. + * + * @param next The next handler + * @param attribute The attribute to set, specified as a string presentation of an {@link io.undertow.attribute.ExchangeAttribute} + * @param value The value to set, specified an a string representation of an {@link io.undertow.attribute.ExchangeAttribute} + * @param classLoader The class loader to use to parser the exchange attributes + * @return The handler + */ + public static SetAttributeHandler setAttribute(final HttpHandler next, final String attribute, final String value, final ClassLoader classLoader) { + return new SetAttributeHandler(next, attribute, value, classLoader); + } + + /** + * Creates the set of handlers that are required to perform a simple rewrite. + * @param condition The rewrite condition + * @param target The rewrite target if the condition matches + * @param next The next handler + * @return + */ + public static HttpHandler rewrite(final String condition, final String target, final ClassLoader classLoader, final HttpHandler next) { + return predicateContext(predicate(PredicateParser.parse(condition, classLoader), setAttribute(next, "%R", target, classLoader), next)); + } + + /** + * Returns a new handler that decodes the URL and query parameters into the specified charset, assuming it + * has not already been done by the connector. For this handler to take effect the parameter + * {@link UndertowOptions#DECODE_URL} must have been set to false. + * + * @param charset The charset to decode + * @param next The next handler + * @return A handler that decodes the URL + */ + public static HttpHandler urlDecodingHandler(final String charset, final HttpHandler next) { + return new URLDecodingHandler(next, charset); + } + + + /** + * Returns a new handler that can be used to wait for all requests to finish before shutting down the server gracefully. + * + * @param next The next http handler + * @return The graceful shutdown handler + */ + public static GracefulShutdownHandler gracefulShutdown(HttpHandler next) { + return new GracefulShutdownHandler(next); + } + + /** + * Returns a new handler that sets the peer address based on the X-Forwarded-For and + * X-Forwarded-Proto header + * @param next The next http handler + * @return The handler + */ + public static ProxyPeerAddressHandler proxyPeerAddress(HttpHandler next) { + return new ProxyPeerAddressHandler(next); + } + + /** + * Handler that appends the JVM route to the session cookie + * @param sessionCookieName The session cookie name + * @param jvmRoute The JVM route to append + * @param next The next handler + * @return The handler + */ + public static JvmRouteHandler jvmRoute(final String sessionCookieName, final String jvmRoute, HttpHandler next) { + return new JvmRouteHandler(next, sessionCookieName, jvmRoute); + } + + /** + * Returns a handler that limits the maximum number of requests that can run at a time. + * + * @param maxRequest The maximum number of requests + * @param queueSize The maximum number of queued requests + * @param next The next handler + * @return The handler + */ + public static RequestLimitingHandler requestLimitingHandler(final int maxRequest, final int queueSize, HttpHandler next) { + return new RequestLimitingHandler(maxRequest, queueSize, next); + } + + /** + * Returns a handler that limits the maximum number of requests that can run at a time. + * + * @param requestLimit The request limit object that can be shared between handlers, to apply the same limits across multiple handlers + * @param next The next handler + * @return The handler + */ + public static RequestLimitingHandler requestLimitingHandler(final RequestLimit requestLimit, HttpHandler next) { + return new RequestLimitingHandler(requestLimit, next); + } + + /** + * Returns a handler that can act as a load balancing reverse proxy. + * + * @param proxyClient The proxy client to use to connect to the remote server + * @param maxRequestTime The maximum amount of time a request can be in progress before it is forcibly closed + * @param next The next handler to invoke if the proxy client does not know how to proxy the request + * @return The proxy handler + */ + public static ProxyHandler proxyHandler(ProxyClient proxyClient, int maxRequestTime, HttpHandler next) { + return new ProxyHandler(proxyClient, maxRequestTime, next); + } + /** + * Returns a handler that can act as a load balancing reverse proxy. + * + * @param proxyClient The proxy client to use to connect to the remote server + * @param next The next handler to invoke if the proxy client does not know how to proxy the request + * @return The proxy handler + */ + public static ProxyHandler proxyHandler(ProxyClient proxyClient, HttpHandler next) { + return new ProxyHandler(proxyClient, next); + } + + /** + * Returns a handler that can act as a load balancing reverse proxy. + * + * @param proxyClient The proxy client to use to connect to the remote server + * @return The proxy handler + */ + public static ProxyHandler proxyHandler(ProxyClient proxyClient) { + return new ProxyHandler(proxyClient, ResponseCodeHandler.HANDLE_404); + } + + /** + * Handler that sets the headers that disable caching of the response + * @param next The next handler + * @return The handler + */ + public static HttpHandler disableCache(final HttpHandler next) { + return new DisableCacheHandler(next); + } + + /** + * Returns a handler that dumps requests to the log for debugging purposes. + * + * @param next The next handler + * @return The request dumping handler + */ + public static HttpHandler requestDump(final HttpHandler next) { + return new RequestDumpingHandler(next); + } + + /** + * Returns a handler that maps exceptions to additional handlers + * @param next The next handler + * @return The exception handler + */ + public static ExceptionHandler exceptionHandler(final HttpHandler next) { + return new ExceptionHandler(next); + } + + private Handlers() { + + } + + public static void handlerNotNull(final HttpHandler handler) { + if (handler == null) { + throw UndertowMessages.MESSAGES.handlerCannotBeNull(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/Undertow.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/Undertow.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/Undertow.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,395 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow; + +import io.undertow.security.api.AuthenticationMode; +import io.undertow.security.api.GSSAPIServerSubjectFactory; +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpHandler; +import io.undertow.server.OpenListener; +import io.undertow.server.protocol.ajp.AjpOpenListener; +import io.undertow.server.protocol.http.HttpOpenListener; +import io.undertow.server.protocol.spdy.SpdyOpenListener; +import org.xnio.BufferAllocator; +import org.xnio.ByteBufferSlicePool; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Options; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.Xnio; +import org.xnio.XnioWorker; +import org.xnio.channels.AcceptingChannel; +import org.xnio.ssl.JsseXnioSsl; +import org.xnio.ssl.SslConnection; +import org.xnio.ssl.XnioSsl; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * Convenience class used to build an Undertow server. + *

+ * TODO: This API is still a work in progress + * + * @author Stuart Douglas + */ +public class Undertow { + + private final int bufferSize; + private final int buffersPerRegion; + private final int ioThreads; + private final int workerThreads; + private final boolean directBuffers; + private final List listeners = new ArrayList<>(); + private final HttpHandler rootHandler; + private final OptionMap workerOptions; + private final OptionMap socketOptions; + private final OptionMap serverOptions; + + private XnioWorker worker; + private List> channels; + private Xnio xnio; + + private Undertow(Builder builder) { + this.bufferSize = builder.bufferSize; + this.buffersPerRegion = builder.buffersPerRegion; + this.ioThreads = builder.ioThreads; + this.workerThreads = builder.workerThreads; + this.directBuffers = builder.directBuffers; + this.listeners.addAll(builder.listeners); + this.rootHandler = builder.handler; + this.workerOptions = builder.workerOptions.getMap(); + this.socketOptions = builder.socketOptions.getMap(); + this.serverOptions = builder.serverOptions.getMap(); + } + + /** + * @return A builder that can be used to create an Undertow server instance + */ + public static Builder builder() { + return new Builder(); + } + + public synchronized void start() { + xnio = Xnio.getInstance(Undertow.class.getClassLoader()); + channels = new ArrayList<>(); + try { + worker = xnio.createWorker(OptionMap.builder() + .set(Options.WORKER_IO_THREADS, ioThreads) + .set(Options.CONNECTION_HIGH_WATER, 1000000) + .set(Options.CONNECTION_LOW_WATER, 1000000) + .set(Options.WORKER_TASK_CORE_THREADS, workerThreads) + .set(Options.WORKER_TASK_MAX_THREADS, workerThreads) + .set(Options.TCP_NODELAY, true) + .set(Options.CORK, true) + .addAll(workerOptions) + .getMap()); + + OptionMap socketOptions = OptionMap.builder() + .set(Options.WORKER_IO_THREADS, ioThreads) + .set(Options.TCP_NODELAY, true) + .set(Options.REUSE_ADDRESSES, true) + .set(Options.BALANCING_TOKENS, 1) + .set(Options.BALANCING_CONNECTIONS, 2) + .addAll(this.socketOptions) + .getMap(); + + + Pool buffers = new ByteBufferSlicePool(directBuffers ? BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR : BufferAllocator.BYTE_BUFFER_ALLOCATOR, bufferSize, bufferSize * buffersPerRegion); + + for (ListenerConfig listener : listeners) { + if (listener.type == ListenerType.AJP) { + AjpOpenListener openListener = new AjpOpenListener(buffers, serverOptions, bufferSize); + openListener.setRootHandler(rootHandler); + ChannelListener> acceptListener = ChannelListeners.openListenerAdapter(openListener); + AcceptingChannel server = worker.createStreamConnectionServer(new InetSocketAddress(Inet4Address.getByName(listener.host), listener.port), acceptListener, socketOptions); + server.resumeAccepts(); + channels.add(server); + } else { + OptionMap undertowOptions = OptionMap.builder().set(UndertowOptions.BUFFER_PIPELINED_DATA, true).addAll(serverOptions).getMap(); + if (listener.type == ListenerType.HTTP) { + HttpOpenListener openListener = new HttpOpenListener(buffers, undertowOptions, bufferSize); + openListener.setRootHandler(rootHandler); + ChannelListener> acceptListener = ChannelListeners.openListenerAdapter(openListener); + AcceptingChannel server = worker.createStreamConnectionServer(new InetSocketAddress(Inet4Address.getByName(listener.host), listener.port), acceptListener, socketOptions); + server.resumeAccepts(); + channels.add(server); + } else if (listener.type == ListenerType.HTTPS) { + OpenListener openListener = new HttpOpenListener(buffers, undertowOptions, bufferSize); + if(serverOptions.get(UndertowOptions.ENABLE_SPDY, false)) { + openListener = new SpdyOpenListener(buffers, new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 1024, 1024), undertowOptions, bufferSize, (HttpOpenListener) openListener); + } + openListener.setRootHandler(rootHandler); + ChannelListener> acceptListener = ChannelListeners.openListenerAdapter(openListener); + XnioSsl xnioSsl; + if (listener.sslContext != null) { + xnioSsl = new JsseXnioSsl(xnio, OptionMap.create(Options.USE_DIRECT_BUFFERS, true), listener.sslContext); + } else { + xnioSsl = xnio.getSslProvider(listener.keyManagers, listener.trustManagers, OptionMap.create(Options.USE_DIRECT_BUFFERS, true)); + } + AcceptingChannel sslServer = xnioSsl.createSslConnectionServer(worker, new InetSocketAddress(Inet4Address.getByName(listener.host), listener.port), (ChannelListener) acceptListener, socketOptions); + sslServer.resumeAccepts(); + channels.add(sslServer); + } + } + + } + + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + public synchronized void stop() { + for (AcceptingChannel channel : channels) { + IoUtils.safeClose(channel); + } + channels = null; + worker.shutdownNow(); + worker = null; + xnio = null; + } + + + public static enum ListenerType { + HTTP, + HTTPS, + AJP + } + + private static class ListenerConfig { + final ListenerType type; + final int port; + final String host; + final KeyManager[] keyManagers; + final TrustManager[] trustManagers; + final SSLContext sslContext; + + private ListenerConfig(final ListenerType type, final int port, final String host, KeyManager[] keyManagers, TrustManager[] trustManagers) { + this.type = type; + this.port = port; + this.host = host; + this.keyManagers = keyManagers; + this.trustManagers = trustManagers; + this.sslContext = null; + } + + private ListenerConfig(final ListenerType type, final int port, final String host, SSLContext sslContext) { + this.type = type; + this.port = port; + this.host = host; + this.keyManagers = null; + this.trustManagers = null; + this.sslContext = sslContext; + } + } + + public static class LoginConfig { + private final IdentityManager identityManager; + private boolean basic; + private boolean digest; + private boolean kerberos; + private boolean form; + private String realmName; + private String errorPage, loginPage; + private GSSAPIServerSubjectFactory subjectFactory; + private AuthenticationMode authenticationMode = AuthenticationMode.PRO_ACTIVE; + + public LoginConfig(final IdentityManager identityManager) { + this.identityManager = identityManager; + } + + public LoginConfig basicAuth(final String realmName) { + if (digest) { + throw UndertowMessages.MESSAGES.authTypeCannotBeCombined("basic", "digest"); + } else if (form) { + throw UndertowMessages.MESSAGES.authTypeCannotBeCombined("basic", "form"); + } + basic = true; + this.realmName = realmName; + return this; + } + + public LoginConfig digestAuth(final String realmName) { + if (basic) { + throw UndertowMessages.MESSAGES.authTypeCannotBeCombined("digest", "basic"); + } else if (form) { + throw UndertowMessages.MESSAGES.authTypeCannotBeCombined("digest", "form"); + } + digest = true; + this.realmName = realmName; + return this; + } + + public LoginConfig kerberosAuth(GSSAPIServerSubjectFactory subjectFactory) { + kerberos = true; + this.subjectFactory = subjectFactory; + return this; + } + + public LoginConfig formAuth(final String loginPage, final String errorPage) { + if (digest) { + throw UndertowMessages.MESSAGES.authTypeCannotBeCombined("form", "digest"); + } else if (basic) { + throw UndertowMessages.MESSAGES.authTypeCannotBeCombined("form", "basic"); + } + this.loginPage = loginPage; + this.errorPage = errorPage; + form = true; + return this; + } + + public LoginConfig setAuthenticationMode(final AuthenticationMode authenticationMode) { + this.authenticationMode = authenticationMode; + return this; + } + } + + public static final class Builder { + + private int bufferSize; + private int buffersPerRegion; + private int ioThreads; + private int workerThreads; + private boolean directBuffers; + private final List listeners = new ArrayList<>(); + private HttpHandler handler; + + private final OptionMap.Builder workerOptions = OptionMap.builder(); + private final OptionMap.Builder socketOptions = OptionMap.builder(); + private final OptionMap.Builder serverOptions = OptionMap.builder(); + + private Builder() { + ioThreads = Math.max(Runtime.getRuntime().availableProcessors(), 2); + workerThreads = ioThreads * 8; + long maxMemory = Runtime.getRuntime().maxMemory(); + //smaller than 64mb of ram we use 512b buffers + if (maxMemory < 64 * 1024 * 1024) { + //use 512b buffers + directBuffers = false; + bufferSize = 512; + buffersPerRegion = 10; + } else if (maxMemory < 128 * 1024 * 1024) { + //use 1k buffers + directBuffers = true; + bufferSize = 1024; + buffersPerRegion = 10; + } else { + //use 16k buffers for best performance + //as 16k is generally the max amount of data that can be sent in a single write() call + directBuffers = true; + bufferSize = 1024 * 16; + buffersPerRegion = 20; + } + + } + + public Undertow build() { + return new Undertow(this); + } + + @Deprecated + public Builder addListener(int port, String host) { + listeners.add(new ListenerConfig(ListenerType.HTTP, port, host, null, null)); + return this; + } + + public Builder addHttpListener(int port, String host) { + listeners.add(new ListenerConfig(ListenerType.HTTP, port, host, null, null)); + return this; + } + + public Builder addHttpsListener(int port, String host, KeyManager[] keyManagers, TrustManager[] trustManagers) { + listeners.add(new ListenerConfig(ListenerType.HTTPS, port, host, keyManagers, trustManagers)); + return this; + } + + public Builder addHttpsListener(int port, String host, SSLContext sslContext) { + listeners.add(new ListenerConfig(ListenerType.HTTPS, port, host, sslContext)); + return this; + } + + public Builder addAjpListener(int port, String host) { + listeners.add(new ListenerConfig(ListenerType.AJP, port, host, null, null)); + return this; + } + + @Deprecated + public Builder addListener(int port, String host, ListenerType listenerType) { + listeners.add(new ListenerConfig(listenerType, port, host, null, null)); + return this; + } + + public Builder setBufferSize(final int bufferSize) { + this.bufferSize = bufferSize; + return this; + } + + public Builder setBuffersPerRegion(final int buffersPerRegion) { + this.buffersPerRegion = buffersPerRegion; + return this; + } + + public Builder setIoThreads(final int ioThreads) { + this.ioThreads = ioThreads; + return this; + } + + public Builder setWorkerThreads(final int workerThreads) { + this.workerThreads = workerThreads; + return this; + } + + public Builder setDirectBuffers(final boolean directBuffers) { + this.directBuffers = directBuffers; + return this; + } + + public Builder setHandler(final HttpHandler handler) { + this.handler = handler; + return this; + } + + public Builder setServerOption(final Option option, final T value) { + serverOptions.set(option, value); + return this; + } + + public Builder setSocketOption(final Option option, final T value) { + socketOptions.set(option, value); + return this; + } + + public Builder setWorkerOption(final Option option, final T value) { + workerOptions.set(option, value); + return this; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/UndertowLogger.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/UndertowLogger.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/UndertowLogger.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,179 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow; + +import io.undertow.client.ClientConnection; +import io.undertow.server.ServerConnection; +import org.jboss.logging.BasicLogger; +import org.jboss.logging.Logger; +import org.jboss.logging.annotations.Cause; +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageLogger; + +import java.io.File; +import java.io.IOException; +import java.net.SocketAddress; +import java.net.URI; +import java.sql.SQLException; + +/** + * log messages start at 5000 + * + * @author Stuart Douglas + */ +@MessageLogger(projectCode = "UT") +public interface UndertowLogger extends BasicLogger { + + UndertowLogger ROOT_LOGGER = Logger.getMessageLogger(UndertowLogger.class, UndertowLogger.class.getPackage().getName()); + UndertowLogger CLIENT_LOGGER = Logger.getMessageLogger(UndertowLogger.class, ClientConnection.class.getPackage().getName()); + + UndertowLogger REQUEST_LOGGER = Logger.getMessageLogger(UndertowLogger.class, UndertowLogger.class.getPackage().getName() + ".request"); + UndertowLogger PROXY_REQUEST_LOGGER = Logger.getMessageLogger(UndertowLogger.class, UndertowLogger.class.getPackage().getName() + ".proxy"); + UndertowLogger REQUEST_DUMPER_LOGGER = Logger.getMessageLogger(UndertowLogger.class, UndertowLogger.class.getPackage().getName() + ".request.dump"); + /** + * Logger used for IO exceptions. Generally these should be suppressed, because they are of little interest, and it is easy for an + * attacker to fill up the logs by intentionally causing IO exceptions. + */ + UndertowLogger REQUEST_IO_LOGGER = Logger.getMessageLogger(UndertowLogger.class, UndertowLogger.class.getPackage().getName() + ".request.io"); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5001, value = "An exception occurred processing the request") + void exceptionProcessingRequest(@Cause Throwable cause); + + @LogMessage(level = Logger.Level.INFO) + @Message(id = 5002, value = "Exception reading file %s: %s") + void exceptionReadingFile(final File file, final IOException e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5003, value = "IOException reading from channel") + void ioExceptionReadingFromChannel(@Cause IOException e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5005, value = "Cannot remove uploaded file %s") + void cannotRemoveUploadedFile(File file); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5006, value = "Connection from %s terminated as request header was larger than %s") + void requestHeaderWasTooLarge(SocketAddress address, int size); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 5007, value = "Request was not fully consumed") + void requestWasNotFullyConsumed(); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 5008, value = "An invalid token '%s' with value '%s' has been received.") + void invalidTokenReceived(final String tokenName, final String tokenValue); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 5009, value = "A mandatory token %s is missing from the request.") + void missingAuthorizationToken(final String tokenName); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 5010, value = "Verification of authentication tokens for user '%s' has failed using mechanism '%s'.") + void authenticationFailed(final String userName, final String mechanism); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5011, value = "Ignoring AJP request with prefix %s") + void ignoringAjpRequestWithPrefixCode(byte prefix); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 5013, value = "An IOException occurred") + void ioException(@Cause IOException e); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 5014, value = "Failed to parse HTTP request") + void failedToParseRequest(@Cause Exception e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5015, value = "Error rotating access log") + void errorRotatingAccessLog(@Cause IOException e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5016, value = "Error writing access log") + void errorWritingAccessLog(@Cause IOException e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5017, value = "Unknown variable %s") + void unknownVariable(String token); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5018, value = "Exception invoking close listener %s") + void exceptionInvokingCloseListener(ServerConnection.CloseListener l, @Cause Throwable e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5019, value = "Cannot upgrade connection") + void cannotUpgradeConnection(@Cause Exception e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5020, value = "Error writing JDBC log") + void errorWritingJDBCLog(@Cause SQLException e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5021, value = "Proxy request to %s timed out") + void proxyRequestTimedOut(String requestURI); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5022, value = "Exception generating error page %s") + void exceptionGeneratingErrorPage(@Cause Exception e, String location); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5023, value = "Exception handling request to %s") + void exceptionHandlingRequest(@Cause Throwable t, String requestURI); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5024, value = "Could not register resource change listener for caching resource manager, automatic invalidation of cached resource will not work") + void couldNotRegisterChangeListener(@Cause Exception e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5025, value = "Could not initiate SPDY connection and no HTTP fallback defined") + void couldNotInitiateSpdyConnection(); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5026, value = "Jetty ALPN support not found on boot class path, SPDY client will not be available.") + void jettyALPNNotFound(); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5027, value = "Timing out request to %s") + void timingOutRequest(String requestURI); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5028, value = "Proxy request to %s failed") + void proxyRequestFailed(String requestURI, @Cause Exception e); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5030, value = "Proxy request to %s could not resolve a backend server") + void proxyRequestFailedToResolveBackend(String requestURI); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5031, value = "Proxy request to %s could not connect to backend server %s") + void proxyFailedToConnectToBackend(String requestURI, URI uri); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5032, value = "Listener not making progress on framed channel, closing channel to prevent infinite loop") + void listenerNotProgressing(); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5033, value = "Failed to initiate HTTP2 connection") + void couldNotInitiateHttp2Connection(); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 5034, value = "Remote endpoint failed to send initial settings frame in HTTP2 connection") + void remoteEndpointFailedToSendInitialSettings(); +} Index: 3rdParty_sources/undertow/io/undertow/UndertowMessages.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/UndertowMessages.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/UndertowMessages.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,341 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.channels.ClosedChannelException; + +import io.undertow.predicate.PredicateBuilder; +import io.undertow.server.handlers.builder.HandlerBuilder; +import org.jboss.logging.Messages; +import org.jboss.logging.annotations.Cause; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageBundle; + +import javax.net.ssl.SSLPeerUnverifiedException; + +/** + * @author Stuart Douglas + */ +@MessageBundle(projectCode = "UT") +public interface UndertowMessages { + + UndertowMessages MESSAGES = Messages.getBundle(UndertowMessages.class); + + @Message(id = 1, value = "Maximum concurrent requests must be larger than zero.") + IllegalArgumentException maximumConcurrentRequestsMustBeLargerThanZero(); + + @Message(id = 2, value = "The response has already been started") + IllegalStateException responseAlreadyStarted(); + + // id = 3 + + @Message(id = 4, value = "getResponseChannel() has already been called") + IllegalStateException responseChannelAlreadyProvided(); + + @Message(id = 5, value = "getRequestChannel() has already been called") + IllegalStateException requestChannelAlreadyProvided(); + + // id = 6 + + // id = 7 + + @Message(id = 8, value = "Handler cannot be null") + IllegalArgumentException handlerCannotBeNull(); + + @Message(id = 9, value = "Path must be specified") + IllegalArgumentException pathMustBeSpecified(); + + @Message(id = 10, value = "Session not found %s") + IllegalStateException sessionNotFound(final String session); + + @Message(id = 11, value = "Session manager must not be null") + IllegalStateException sessionManagerMustNotBeNull(); + + @Message(id = 12, value = "Session manager was not attached to the request. Make sure that the SessionAttachmentHander is installed in the handler chain") + IllegalStateException sessionManagerNotFound(); + + @Message(id = 13, value = "Argument %s cannot be null") + IllegalArgumentException argumentCannotBeNull(final String argument); + + @Message(id = 14, value = "close() called with data still to be flushed. Please call shutdownWrites() and then call flush() until it returns true before calling close()") + IOException closeCalledWithDataStillToBeFlushed(); + + @Message(id = 16, value = "Could not add cookie as cookie handler was not present in the handler chain") + IllegalStateException cookieHandlerNotPresent(); + + @Message(id = 17, value = "Form value is a file, use getFile() instead") + IllegalStateException formValueIsAFile(); + + @Message(id = 18, value = "Form value is a String, use getValue() instead") + IllegalStateException formValueIsAString(); + + @Message(id = 19, value = "Connection from %s terminated as request entity was larger than %s") + IOException requestEntityWasTooLarge(SocketAddress address, long size); + + @Message(id = 20, value = "Connection terminated as request was larger than %s") + IOException requestEntityWasTooLarge(long size); + + @Message(id = 21, value = "Session already invalidated") + IllegalStateException sessionAlreadyInvalidated(); + + @Message(id = 22, value = "The specified hash algorithm '%s' can not be found.") + IllegalArgumentException hashAlgorithmNotFound(String algorithmName); + + @Message(id = 23, value = "An invalid Base64 token has been received.") + IllegalArgumentException invalidBase64Token(@Cause final IOException cause); + + @Message(id = 24, value = "An invalidly formatted nonce has been received.") + IllegalArgumentException invalidNonceReceived(); + + @Message(id = 25, value = "Unexpected token '%s' within header.") + IllegalArgumentException unexpectedTokenInHeader(final String name); + + @Message(id = 26, value = "Invalid header received.") + IllegalArgumentException invalidHeader(); + + @Message(id = 27, value = "Could not find session cookie config in the request") + IllegalStateException couldNotFindSessionCookieConfig(); + + @Message(id = 28, value = "Session %s already exists") + IllegalStateException sessionAlreadyExists(final String id); + + @Message(id = 29, value = "Channel was closed mid chunk, if you have attempted to write chunked data you cannot shutdown the channel until after it has all been written.") + IOException chunkedChannelClosedMidChunk(); + + @Message(id = 30, value = "User %s successfully authenticated.") + String userAuthenticated(final String userName); + + @Message(id = 31, value = "User %s has logged out.") + String userLoggedOut(final String userName); + + @Message(id = 33, value = "Authentication type %s cannot be combined with %s") + IllegalStateException authTypeCannotBeCombined(String type, String existing); + + @Message(id = 34, value = "Stream is closed") + IOException streamIsClosed(); + + @Message(id = 35, value = "Cannot get stream as startBlocking has not been invoked") + IllegalStateException startBlockingHasNotBeenCalled(); + + @Message(id = 36, value = "Connection terminated parsing multipart data") + IOException connectionTerminatedReadingMultiPartData(); + + @Message(id = 37, value = "Failed to parse path in HTTP request") + RuntimeException failedToParsePath(); + + @Message(id = 38, value = "Authentication failed, requested user name '%s'") + String authenticationFailed(final String userName); + + @Message(id = 39, value = "To many query parameters, cannot have more than %s query parameters") + RuntimeException tooManyQueryParameters(int noParams); + + @Message(id = 40, value = "To many headers, cannot have more than %s header") + RuntimeException tooManyHeaders(int noParams); + + @Message(id = 41, value = "Channel is closed") + ClosedChannelException channelIsClosed(); + + @Message(id = 42, value = "Could not decode trailers in HTTP request") + IOException couldNotDecodeTrailers(); + + @Message(id = 43, value = "Data is already being sent. You must wait for the completion callback to be be invoked before calling send() again") + IllegalStateException dataAlreadyQueued(); + + @Message(id = 44, value = "More than one predicate with name %s. Builder class %s and %s") + IllegalStateException moreThanOnePredicateWithName(String name, Class aClass, Class existing); + + @Message(id = 45, value = "Error parsing predicate string %s:%n%s") + IllegalArgumentException errorParsingPredicateString(String reason, String s); + + @Message(id = 46, value = "The number of cookies sent exceeded the maximum of %s") + IllegalStateException tooManyCookies(int maxCookies); + + @Message(id = 47, value = "The number of parameters exceeded the maximum of %s") + IllegalStateException tooManyParameters(int maxValues); + + @Message(id = 48, value = "No request is currently active") + IllegalStateException noRequestActive(); + + @Message(id = 50, value = "AuthenticationMechanism Outcome is null") + IllegalStateException authMechanismOutcomeNull(); + + @Message(id = 51, value = "Not a valid IP pattern %s") + IllegalArgumentException notAValidIpPattern(String peer); + + @Message(id = 52, value = "Session data requested when non session based authentication in use") + IllegalStateException noSessionData(); + + @Message(id = 53, value = "Listener %s already registered") + IllegalArgumentException listenerAlreadyRegistered(String name); + + @Message(id = 54, value = "The maximum size %s for an individual file in a multipart request was exceeded") + IOException maxFileSizeExceeded(long maxIndividualFileSize); + + @Message(id = 55, value = "Could not set attribute %s to %s as it is read only") + String couldNotSetAttribute(String attributeName, String newValue); + + @Message(id = 56, value = "Could not parse URI template %s, exception at char %s") + RuntimeException couldNotParseUriTemplate(String path, int i); + + @Message(id = 57, value = "Mismatched braces in attribute string %s") + RuntimeException mismatchedBraces(String valueString); + + @Message(id = 58, value = "More than one handler with name %s. Builder class %s and %s") + IllegalStateException moreThanOneHandlerWithName(String name, Class aClass, Class existing); + + @Message(id = 59, value = "Invalid syntax %s") + IllegalArgumentException invalidSyntax(String line); + + @Message(id = 60, value = "Error parsing handler string %s:%n%s") + IllegalArgumentException errorParsingHandlerString(String reason, String s); + + @Message(id = 61, value = "Out of band responses only allowed for 100-continue requests") + IllegalArgumentException outOfBandResponseOnlyAllowedFor100Continue(); + + @Message(id = 62, value = "AJP does not support HTTP upgrade") + IllegalStateException ajpDoesNotSupportHTTPUpgrade(); + + @Message(id = 63, value = "File system watcher already started") + IllegalStateException fileSystemWatcherAlreadyStarted(); + + @Message(id = 64, value = "File system watcher not started") + IllegalStateException fileSystemWatcherNotStarted(); + + @Message(id = 65, value = "SSL must be specified to connect to a https URL") + IOException sslWasNull(); + + @Message(id = 66, value = "Incorrect magic number for AJP packet header") + IOException wrongMagicNumber(); + + @Message(id = 67, value = "No client cert was provided") + SSLPeerUnverifiedException peerUnverified(); + + @Message(id = 68, value = "Servlet path match failed") + IllegalArgumentException servletPathMatchFailed(); + + @Message(id = 69, value = "Could not parse set cookie header %s") + IllegalArgumentException couldNotParseCookie(String headerValue); + + @Message(id = 70, value = "method can only be called by IO thread") + IllegalStateException canOnlyBeCalledByIoThread(); + + @Message(id = 71, value = "Cannot add path template %s, matcher already contains an equivalent pattern %s") + IllegalStateException matcherAlreadyContainsTemplate(String templateString, String templateString1); + + @Message(id = 72, value = "Failed to decode url %s to charset %s") + IllegalArgumentException failedToDecodeURL(String s, String enc); + + @Message(id = 73, value = "Resource change listeners are not supported") + IllegalArgumentException resourceChangeListenerNotSupported(); + + @Message(id = 74, value = "Could not renegotiate SSL connection to require client certificate, as client had sent more data") + IllegalStateException couldNotRenegotiate(); + + @Message(id = 75, value = "Object was freed") + IllegalStateException objectWasFreed(); + + @Message(id = 76, value = "Handler not shutdown") + IllegalStateException handlerNotShutdown(); + + @Message(id = 77, value = "The underlying transport does not support HTTP upgrade") + IllegalStateException upgradeNotSupported(); + + @Message(id = 78, value = "Renegotiation not supported") + IOException renegotiationNotSupported(); + + @Message(id = 79, value = "Not a valid user agent pattern %s") + IllegalArgumentException notAValidUserAgentPattern(String userAgent); + + @Message(id = 80, value = "Not a valid regular expression pattern %s") + IllegalArgumentException notAValidRegularExpressionPattern(String pattern); + + @Message(id = 81, value = "Bad request") + RuntimeException badRequest(); + + @Message(id = 82, value = "Host %s already registered") + RuntimeException hostAlreadyRegistered(Object host); + + @Message(id = 83, value = "Host %s has not been registered") + RuntimeException hostHasNotBeenRegistered(Object host); + + @Message(id = 84, value = "Attempted to write additional data after the last chunk") + IOException extraDataWrittenAfterChunkEnd(); + + @Message(id = 85, value = "Could not generate unique session id") + RuntimeException couldNotGenerateUniqueSessionId(); + + @Message(id = 86, value = "SPDY needs to be provided with a heap buffer pool, for use in compressing and decompressing headers.") + IllegalArgumentException mustProvideHeapBuffer(); + + @Message(id = 87, value = "Unexpected SPDY frame type %s") + IOException unexpectedFrameType(int type); + + @Message(id = 88, value = "SPDY control frames cannot have body content") + IOException controlFrameCannotHaveBodyContent(); + + @Message(id = 89, value = "SPDY not supported") + IOException spdyNotSupported(); + + @Message(id = 90, value = "Jetty NPN not available") + IOException jettyNPNNotAvailable(); + + @Message(id = 91, value = "Buffer has already been freed") + IllegalStateException bufferAlreadyFreed(); + + @Message(id = 92, value = "A SPDY header was too large to fit in a response buffer, if you want to support larger headers please increase the buffer size") + IllegalStateException headersTooLargeToFitInHeapBuffer(); + + @Message(id = 93, value = "A SPDY stream was reset by the remote endpoint") + IOException spdyStreamWasReset(); + + @Message(id = 94, value = "Blocking await method called from IO thread. Blocking IO must be dispatched to a worker thread or deadlocks will result.") + IOException awaitCalledFromIoThread(); + + @Message(id = 95, value = "Recursive call to flushSenders()") + RuntimeException recursiveCallToFlushingSenders(); + + @Message(id = 96, value = "More data was written to the channel than specified in the content-length") + IllegalStateException fixedLengthOverflow(); + + @Message(id = 97, value = "AJP request already in progress") + IllegalStateException ajpRequestAlreadyInProgress(); + + @Message(id = 98, value = "HTTP ping data must be 8 bytes in length") + IllegalArgumentException httpPingDataMustBeLength8(); + + @Message(id = 99, value = "Received a ping of size other than 8") + String invalidPingSize(); + + @Message(id = 100, value = "stream id must be zero for frame type %s") + String streamIdMustBeZeroForFrameType(int frameType); + + @Message(id = 101, value = "stream id must not be zero for frame type %s") + String streamIdMustNotBeZeroForFrameType(int frameType); + + @Message(id = 102, value = "RST_STREAM received for idle stream") + String rstStreamReceivedForIdleStream(); + + @Message(id = 103, value = "Http2 stream was reset") + IOException http2StreamWasReset(); + + @Message(id = 104, value = "Incorrect HTTP2 preface") + IOException incorrectHttp2Preface(); +} Index: 3rdParty_sources/undertow/io/undertow/UndertowOptions.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/UndertowOptions.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/UndertowOptions.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,185 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow; + +import org.xnio.Option; + +/** + * @author Stuart Douglas + */ +public class UndertowOptions { + + /** + * The maximum size in bytes of a http request header. + */ + public static final Option MAX_HEADER_SIZE = Option.simple(UndertowOptions.class, "MAX_HEADER_SIZE", Integer.class); + /** + * The default size we allow for the HTTP header. + */ + public static final int DEFAULT_MAX_HEADER_SIZE = 50 * 1024; + + /** + * The default maximum size of the HTTP entity body. + */ + public static final Option MAX_ENTITY_SIZE = Option.simple(UndertowOptions.class, "MAX_ENTITY_SIZE", Long.class); + + /** + * We do not have a default upload limit + */ + public static final long DEFAULT_MAX_ENTITY_SIZE = -1; + + /** + * If we should buffer pipelined requests. Defaults to false. + */ + public static final Option BUFFER_PIPELINED_DATA = Option.simple(UndertowOptions.class, "BUFFER_PIPELINED_DATA", Boolean.class); + + /** + * The idle timeout in milliseconds after which the channel will be closed. + * + * If the underlying channel already has a read or write timeout set the smaller of the two values will be used + * for read/write timeouts. + * + */ + public static final Option IDLE_TIMEOUT = Option.simple(UndertowOptions.class, "IDLE_TIMEOUT", Integer.class); + + /** + * The maximum number of parameters that will be parsed. This is used to protect against hash vulnerabilities. + *

+ * This applies to both query parameters, and to POST data, but is not cumulative (i.e. you can potentially have + * max parameters * 2 total parameters). + *

+ * Defaults to 1000 + */ + public static final Option MAX_PARAMETERS = Option.simple(UndertowOptions.class, "MAX_PARAMETERS", Integer.class); + + /** + * The maximum number of headers that will be parsed. This is used to protect against hash vulnerabilities. + *

+ * Defaults to 200 + */ + public static final Option MAX_HEADERS = Option.simple(UndertowOptions.class, "MAX_HEADERS", Integer.class); + + + /** + * The maximum number of cookies that will be parsed. This is used to protect against hash vulnerabilities. + *

+ * Defaults to 200 + */ + public static final Option MAX_COOKIES = Option.simple(UndertowOptions.class, "MAX_COOKIES", Integer.class); + + /** + * If a request comes in with encoded / characters (i.e. %2F), will these be decoded. + *

+ * This can cause security problems if a front end proxy does not perform the same decoding, and as a result + * this is disabled by default. + *

+ * Defaults to false + * + * @see http://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2007-0450 + */ + public static final Option ALLOW_ENCODED_SLASH = Option.simple(UndertowOptions.class, "ALLOW_ENCODED_SLASH", Boolean.class); + + /** + * If this is true then the parser will decode the URL and query parameters using the selected character encoding (UTF-8 by default). If this is false they will + * not be decoded. This will allow a later handler to decode them into whatever charset is desired. + *

+ * Defaults to true. + */ + public static final Option DECODE_URL = Option.simple(UndertowOptions.class, "DECODE_URL", Boolean.class); + + + /** + * If this is true then the parser will decode the URL and query parameters using the selected character encoding (UTF-8 by default). If this is false they will + * not be decoded. This will allow a later handler to decode them into whatever charset is desired. + *

+ * Defaults to true. + */ + public static final Option URL_CHARSET = Option.simple(UndertowOptions.class, "URL_CHARSET", String.class); + + /** + * If this is true then a Connection: keep-alive header will be added to responses, even when it is not strictly required by + * the specification. + *

+ * Defaults to true + */ + public static final Option ALWAYS_SET_KEEP_ALIVE = Option.simple(UndertowOptions.class, "ALWAYS_SET_KEEP_ALIVE", Boolean.class); + + /** + * If this is true then a Date header will be added to all responses. The HTTP spec says this header should be added to all + * responses, unless the server does not have an accurate clock. + *

+ * Defaults to true + */ + public static final Option ALWAYS_SET_DATE = Option.simple(UndertowOptions.class, "ALWAYS_SET_DATE", Boolean.class); + + /** + * Maximum size of a buffered request, in bytes + *

+ * Requests are not usually buffered, the most common case is when performing SSL renegotiation for a POST request, and the post data must be fully + * buffered in order to perform the renegotiation. + *

+ * Defaults to 16384. + */ + public static final Option MAX_BUFFERED_REQUEST_SIZE = Option.simple(UndertowOptions.class, "MAX_BUFFERED_REQUEST_SIZE", Integer.class); + + /** + * If this is true then Undertow will record the request start time, to allow for request time to be logged + * + * This has a small but measurable performance impact + * + * default is false + */ + public static final Option RECORD_REQUEST_START_TIME = Option.simple(UndertowOptions.class, "RECORD_REQUEST_START_TIME", Boolean.class); + + /** + * If this is true then Undertow will allow non-escaped equals characters in unquoted cookie values. + *

+ * Unquoted cookie values may not contain equals characters. If present the value ends before the equals sign. The remainder of the cookie value will be dropped. + *

+ * default is false + */ + public static final Option ALLOW_EQUALS_IN_COOKIE_VALUE = Option.simple(UndertowOptions.class, "ALLOW_EQUALS_IN_COOKIE_VALUE", Boolean.class); + + /** + * If we should attempt to use SPDY for HTTPS connections. + */ + public static final Option ENABLE_SPDY = Option.simple(UndertowOptions.class, "ENABLE_SPDY", Boolean.class); + + /** + * If we should attempt to use HTTP2 for HTTPS connections. + */ + public static final Option ENABLE_HTTP2 = Option.simple(UndertowOptions.class, "ENABLE_HTTP2", Boolean.class); + + /** + * The size of the header table that is used in the encoder + */ + public static final Option HTTP2_SETTINGS_HEADER_TABLE_SIZE = Option.simple(UndertowOptions.class, "HTTP2_SETTINGS_HEADER_TABLE_SIZE", Integer.class); + public static final int HTTP2_SETTINGS_HEADER_TABLE_SIZE_DEFAULT = 4096; + + public static final Option HTTP2_SETTINGS_ENABLE_PUSH = Option.simple(UndertowOptions.class, "HTTP2_SETTINGS_ENABLE_PUSH", Boolean.class); + public static final Option HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS = Option.simple(UndertowOptions.class, "HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS", Integer.class); + + public static final Option HTTP2_SETTINGS_INITIAL_WINDOW_SIZE = Option.simple(UndertowOptions.class, "HTTP2_SETTINGS_INITIAL_WINDOW_SIZE", Integer.class); + public static final Option HTTP2_SETTINGS_MAX_FRAME_SIZE = Option.simple(UndertowOptions.class, "HTTP2_SETTINGS_MAX_FRAME_SIZE", Integer.class); + public static final Option HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE = Option.simple(UndertowOptions.class, "HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE", Integer.class); + + private UndertowOptions() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/Version.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/Version.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/Version.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow; + +import java.util.Properties; + +/** + * @author Tomaz Cerar (c) 2013 Red Hat Inc. + */ +public class Version { + private static final String versionString; + private static final String SERVER_NAME = "Undertow"; + private static final String fullVersionString; + + static { + String version = "Unknown"; + try { + Properties props = new Properties(); + props.load(Version.class.getResourceAsStream("version.properties")); + version = props.getProperty("undertow.version"); + } catch (Exception e) { + e.printStackTrace(); + } + versionString = version; + fullVersionString = SERVER_NAME + " - "+ versionString; + } + + public static String getVersionString() { + return versionString; + } + + public static String getFullVersionString() { + return fullVersionString; + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/BytesSentAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/BytesSentAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/BytesSentAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,70 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The bytes sent + * + * @author Filipe Ferraz + */ +public class BytesSentAttribute implements ExchangeAttribute { + + public static final String BYTES_SENT_SHORT_UPPER = "%B"; + public static final String BYTES_SENT_SHORT_LOWER = "%b"; + public static final String BYTES_SENT = "%{BYTES_SENT}"; + + private final String attribute; + + public BytesSentAttribute(final String attribute) { + this.attribute = attribute; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + if (attribute.equals(BYTES_SENT_SHORT_LOWER)) { + long bytesSent = exchange.getResponseContentLength(); + return bytesSent == 0 ? "-" : Long.toString(bytesSent); + } else { + return Long.toString(exchange.getResponseContentLength()); + } + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Bytes sent", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Bytes Sent"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(BYTES_SENT) || token.equals(BYTES_SENT_SHORT_UPPER) || token.equals(BYTES_SENT_SHORT_LOWER)) { + return new BytesSentAttribute(token); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/CompositeExchangeAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/CompositeExchangeAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/CompositeExchangeAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,54 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * Exchange attribute that represents a combination of attributes that should be merged into a single string. + * + * @author Stuart Douglas + */ +public class CompositeExchangeAttribute implements ExchangeAttribute { + + private final ExchangeAttribute[] attributes; + + public CompositeExchangeAttribute(ExchangeAttribute[] attributes) { + ExchangeAttribute[] copy = new ExchangeAttribute[attributes.length]; + System.arraycopy(attributes, 0, copy, 0, attributes.length); + this.attributes = copy; + } + + @Override + public String readAttribute(HttpServerExchange exchange) { + final StringBuilder sb = new StringBuilder(); + for (int i = 0; i < attributes.length; ++i) { + final String val = attributes[i].readAttribute(exchange); + if(val != null) { + sb.append(val); + } + } + return sb.toString(); + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("combined", newValue); + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ConstantExchangeAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ConstantExchangeAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ConstantExchangeAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * Exchange attribute that represents a fixed value + * + * @author Stuart Douglas + */ +public class ConstantExchangeAttribute implements ExchangeAttribute { + + private final String value; + + public ConstantExchangeAttribute(final String value) { + this.value = value; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return value; + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("constant", newValue); + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/CookieAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/CookieAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/CookieAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.CookieImpl; + +/** + * A cookie + * + * @author Stuart Douglas + */ +public class CookieAttribute implements ExchangeAttribute { + + private final String cookieName; + + public CookieAttribute(final String cookieName) { + this.cookieName = cookieName; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + Cookie cookie = exchange.getRequestCookies().get(cookieName); + if (cookie == null) { + return null; + } + return cookie.getValue(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.setResponseCookie(new CookieImpl(cookieName, newValue)); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Cookie"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{c,") && token.endsWith("}")) { + final String cookieName = token.substring(4, token.length() - 1); + return new CookieAttribute(cookieName); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/DateTimeAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/DateTimeAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/DateTimeAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.util.Date; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.DateUtils; + +/** + * The request status code + * + * @author Stuart Douglas + */ +public class DateTimeAttribute implements ExchangeAttribute { + + public static final String DATE_TIME_SHORT = "%t"; + public static final String DATE_TIME = "%{DATE_TIME}"; + + public static final ExchangeAttribute INSTANCE = new DateTimeAttribute(); + + private DateTimeAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return DateUtils.toCommonLogFormat(new Date()); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Date time", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Date Time"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(DATE_TIME) || token.equals(DATE_TIME_SHORT)) { + return DateTimeAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * Representation of a string attribute from a HTTP server exchange. + * + * + * @author Stuart Douglas + */ +public interface ExchangeAttribute { + + /** + * Resolve the attribute from the HTTP server exchange. This may return null if the attribute is not present. + * @param exchange The exchange + * @return The attribute + */ + String readAttribute(final HttpServerExchange exchange); + + /** + * Sets a new value for the attribute. Not all attributes are writable. + * @param exchange The exchange + * @param newValue The new value for the attribute + */ + void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException; +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +/** + * An interface that knows how to build an exchange attribute from a textual representation. + *

+ * This makes it easy to configure attributes based on a string representation + * + * @author Stuart Douglas + */ +public interface ExchangeAttributeBuilder { + + /** + * The string representation of the attribute name. This is used solely for debugging / informational purposes + * + * @return The attribute name + */ + String name(); + + /** + * Build the attribute from a text based representation. If the attribute does not understand this representation then + * it will just return null. + * + * @param token The string token + * @return The exchange attribute, or null + */ + ExchangeAttribute build(final String token); + +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,170 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.ServiceLoader; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; + +/** + * Attribute parser for exchange attributes. This builds an attribute from a string definition. + *

+ * This uses a service loader mechanism to allow additional token types to be loaded. Token definitions are loaded + * from the provided class loader. + * + * @author Stuart Douglas + * @see ExchangeAttributes#parser(ClassLoader) + */ +public class ExchangeAttributeParser { + + private final List builders; + private final List wrappers; + + ExchangeAttributeParser(final ClassLoader classLoader, List wrappers) { + this.wrappers = wrappers; + ServiceLoader loader = ServiceLoader.load(ExchangeAttributeBuilder.class, classLoader); + final List builders = new ArrayList<>(); + for (ExchangeAttributeBuilder instance : loader) { + builders.add(instance); + } + this.builders = Collections.unmodifiableList(builders); + + } + + /** + * Parses the provided value string, and turns it into a list of exchange attributes. + *

+ * Tokens are created according to the following rules: + *

+ * %a - % followed by single character. %% is an escape for a literal % + * %{.*}a? - % plus curly braces with any amount of content inside, followed by an optional character + * ${.*} - $ followed by a curly braces to reference an item from the predicate context + * + * @param valueString + * @return + */ + public ExchangeAttribute parse(final String valueString) { + final List attributes = new ArrayList<>(); + int pos = 0; + int state = 0; //0 = literal, 1 = %, 2 = %{, 3 = $, 4 = ${ + for (int i = 0; i < valueString.length(); ++i) { + char c = valueString.charAt(i); + switch (state) { + case 0: { + if (c == '%' || c == '$') { + if (pos != i) { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i)))); + pos = i; + } + if (c == '%') { + state = 1; + } else { + state = 3; + } + } + break; + } + case 1: { + if (c == '{') { + state = 2; + } else if (c == '%') { + //literal percent + attributes.add(wrap(new ConstantExchangeAttribute("%"))); + pos = i + 1; + state = 0; + } else { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + case 2: { + if (c == '}') { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + case 3: { + if (c == '{') { + state = 4; + } else { + state = 0; + } + break; + } + case 4: { + if (c == '}') { + attributes.add(wrap(parseSingleToken(valueString.substring(pos, i + 1)))); + pos = i + 1; + state = 0; + } + break; + } + + } + } + switch (state) { + case 0: + case 1: + case 3:{ + if(pos != valueString.length()) { + attributes.add(wrap(parseSingleToken(valueString.substring(pos)))); + } + break; + } + case 2: + case 4: { + throw UndertowMessages.MESSAGES.mismatchedBraces(valueString); + } + } + if(attributes.size() == 1) { + return attributes.get(0); + } + return new CompositeExchangeAttribute(attributes.toArray(new ExchangeAttribute[attributes.size()])); + } + + public ExchangeAttribute parseSingleToken(final String token) { + for (final ExchangeAttributeBuilder builder : builders) { + ExchangeAttribute res = builder.build(token); + if (res != null) { + return res; + } + } + if (token.startsWith("%")) { + UndertowLogger.ROOT_LOGGER.unknownVariable(token); + } + return new ConstantExchangeAttribute(token); + } + + private ExchangeAttribute wrap(ExchangeAttribute attribute) { + ExchangeAttribute res = attribute; + for(ExchangeAttributeWrapper w : wrappers) { + res = w.wrap(res); + } + return res; + } + +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeWrapper.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeWrapper.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributeWrapper.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,12 @@ +package io.undertow.attribute; + +/** + * Interface that can be used to wrap an exchange attribute. + * + * @author Stuart Douglas + */ +public interface ExchangeAttributeWrapper { + + ExchangeAttribute wrap(ExchangeAttribute attribute); + +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributes.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributes.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ExchangeAttributes.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,134 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; + +import java.util.Arrays; +import java.util.Collections; + +/** + * Utility class for retrieving exchange attributes + * + * @author Stuart Douglas + */ +public class ExchangeAttributes { + + public static ExchangeAttributeParser parser(final ClassLoader classLoader) { + return new ExchangeAttributeParser(classLoader, Collections.emptyList()); + } + + public static ExchangeAttributeParser parser(final ClassLoader classLoader, ExchangeAttributeWrapper ... wrappers) { + return new ExchangeAttributeParser(classLoader, Arrays.asList(wrappers)); + } + + public static ExchangeAttribute cookie(final String cookieName) { + return new CookieAttribute(cookieName); + } + + public static ExchangeAttribute bytesSent(final String attribute) { + return new BytesSentAttribute(attribute); + } + + public static ExchangeAttribute dateTime() { + return DateTimeAttribute.INSTANCE; + } + + public static ExchangeAttribute localIp() { + return LocalIPAttribute.INSTANCE; + } + + public static ExchangeAttribute localPort() { + return LocalPortAttribute.INSTANCE; + } + + public static ExchangeAttribute localServerName() { + return LocalServerNameAttribute.INSTANCE; + } + + public static ExchangeAttribute queryString() { + return QueryStringAttribute.INSTANCE; + } + + public static ExchangeAttribute relativePath() { + return RelativePathAttribute.INSTANCE; + } + + public static ExchangeAttribute remoteIp() { + return RemoteIPAttribute.INSTANCE; + } + + public static ExchangeAttribute remoteUser() { + return RemoteUserAttribute.INSTANCE; + } + + public static ExchangeAttribute requestHeader(final HttpString header) { + return new RequestHeaderAttribute(header); + } + + public static ExchangeAttribute requestList() { + return RequestLineAttribute.INSTANCE; + } + + public static ExchangeAttribute requestMethod() { + return RequestMethodAttribute.INSTANCE; + } + + public static ExchangeAttribute requestProtocol() { + return RequestProtocolAttribute.INSTANCE; + } + + public static ExchangeAttribute requestURL() { + return RequestURLAttribute.INSTANCE; + } + + public static ExchangeAttribute responseCode() { + return ResponseCodeAttribute.INSTANCE; + } + + public static ExchangeAttribute responseHeader(final HttpString header) { + return new ResponseHeaderAttribute(header); + } + + public static ExchangeAttribute threadName() { + return ThreadNameAttribute.INSTANCE; + } + + public static ExchangeAttribute constant(String value) { + return new ConstantExchangeAttribute(value); + } + + public static String resolve(final HttpServerExchange exchange, final ExchangeAttribute[] attributes) { + final StringBuilder result = new StringBuilder(); + for (int i = 0; i < attributes.length; ++i) { + final String str = attributes[i].readAttribute(exchange); + if (str != null) { + result.append(str); + } + } + return result.toString(); + } + + + private ExchangeAttributes() { + + } + +} Index: 3rdParty_sources/undertow/io/undertow/attribute/IdentUsernameAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/IdentUsernameAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/IdentUsernameAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The ident username, not used, included for apache access log compatibility + * + * @author Stuart Douglas + */ +public class IdentUsernameAttribute implements ExchangeAttribute { + + public static final String IDENT_USERNAME = "%l"; + + public static final ExchangeAttribute INSTANCE = new IdentUsernameAttribute(); + + private IdentUsernameAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return null; + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Ident username", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Ident Username"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(IDENT_USERNAME)) { + return IdentUsernameAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/LocalIPAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/LocalIPAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/LocalIPAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.net.InetSocketAddress; + +import io.undertow.server.HttpServerExchange; + +/** + * The local IP address + * + * @author Stuart Douglas + */ +public class LocalIPAttribute implements ExchangeAttribute { + + public static final String LOCAL_IP = "%{LOCAL_IP}"; + public static final String LOCAL_IP_SHORT = "%A"; + + public static final ExchangeAttribute INSTANCE = new LocalIPAttribute(); + + private LocalIPAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + InetSocketAddress localAddress = (InetSocketAddress) exchange.getConnection().getLocalAddress(); + return localAddress.getAddress().getHostAddress(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Local IP", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Local IP"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(LOCAL_IP) || token.equals(LOCAL_IP_SHORT)) { + return LocalIPAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/LocalPortAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/LocalPortAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/LocalPortAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.net.InetSocketAddress; + +import io.undertow.server.HttpServerExchange; + +/** + * The local port + * + * @author Stuart Douglas + */ +public class LocalPortAttribute implements ExchangeAttribute { + + public static final String LOCAL_PORT_SHORT = "%p"; + public static final String LOCAL_PORT = "%{LOCAL_PORT}"; + + public static final ExchangeAttribute INSTANCE = new LocalPortAttribute(); + + private LocalPortAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + InetSocketAddress localAddress = (InetSocketAddress) exchange.getConnection().getLocalAddress(); + return Integer.toString(localAddress.getPort()); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Local port", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Local Port"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(LOCAL_PORT) || token.equals(LOCAL_PORT_SHORT)) { + return LocalPortAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/LocalServerNameAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/LocalServerNameAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/LocalServerNameAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; + +/** + * The local server name + * + * @author Stuart Douglas + */ +public class LocalServerNameAttribute implements ExchangeAttribute { + + public static final String LOCAL_SERVER_NAME_SHORT = "%v"; + public static final String LOCAL_SERVER_NAME = "%{LOCAL_SERVER_NAME}"; + + public static final ExchangeAttribute INSTANCE = new LocalServerNameAttribute(); + + private LocalServerNameAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getRequestHeaders().getFirst(Headers.HOST); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Local server name", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Local server name"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(LOCAL_SERVER_NAME) || token.equals(LOCAL_SERVER_NAME_SHORT)) { + return LocalServerNameAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/PathParameterAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/PathParameterAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/PathParameterAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,86 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.util.ArrayDeque; +import java.util.Deque; + +import io.undertow.server.HttpServerExchange; + +/** + * Path parameter + * + * @author Stuart Douglas + */ +public class PathParameterAttribute implements ExchangeAttribute { + + + private final String parameter; + + public PathParameterAttribute(String parameter) { + this.parameter = parameter; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + Deque res = exchange.getPathParameters().get(parameter); + if(res == null) { + return null; + }else if(res.isEmpty()) { + return ""; + } else if(res.size() ==1) { + return res.getFirst(); + } else { + StringBuilder sb = new StringBuilder("["); + int i = 0; + for(String s : res) { + sb.append(s); + if(++i != res.size()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + final ArrayDeque value = new ArrayDeque<>(); + value.add(newValue); + exchange.getPathParameters().put(parameter, value); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Path Parameter"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{p,") && token.endsWith("}")) { + final String qp = token.substring(4, token.length() - 1); + return new PathParameterAttribute(qp); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/PredicateContextAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/PredicateContextAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/PredicateContextAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,72 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.util.Map; + +import io.undertow.predicate.Predicate; +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +public class PredicateContextAttribute implements ExchangeAttribute { + + private final String name; + + public PredicateContextAttribute(final String name) { + this.name = name; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + Map context = exchange.getAttachment(Predicate.PREDICATE_CONTEXT); + if (context != null) { + Object object = context.get(name); + return object == null ? null : object.toString(); + } + return null; + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + Map context = exchange.getAttachment(Predicate.PREDICATE_CONTEXT); + if (context != null) { + context.put(name, newValue); + } + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Predicate context variable"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("${") && token.endsWith("}") && token.length() > 3) { + return new PredicateContextAttribute(token.substring(2, token.length() - 1)); + } else if (token.startsWith("$")) { + return new PredicateContextAttribute(token.substring(1, token.length())); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/QueryParameterAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/QueryParameterAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/QueryParameterAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,86 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +import java.util.ArrayDeque; +import java.util.Deque; + +/** + * Query parameter + * + * @author Stuart Douglas + */ +public class QueryParameterAttribute implements ExchangeAttribute { + + + private final String parameter; + + public QueryParameterAttribute(String parameter) { + this.parameter = parameter; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + Deque res = exchange.getQueryParameters().get(parameter); + if(res == null) { + return null; + }else if(res.isEmpty()) { + return ""; + } else if(res.size() ==1) { + return res.getFirst(); + } else { + StringBuilder sb = new StringBuilder("["); + int i = 0; + for(String s : res) { + sb.append(s); + if(++i != res.size()) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + final ArrayDeque value = new ArrayDeque<>(); + value.add(newValue); + exchange.getQueryParameters().put(parameter, value); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Query Parameter"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{q,") && token.endsWith("}")) { + final String qp = token.substring(4, token.length() - 1); + return new QueryParameterAttribute(qp); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/QueryStringAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/QueryStringAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/QueryStringAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The query string + * + * @author Stuart Douglas + */ +public class QueryStringAttribute implements ExchangeAttribute { + + public static final String QUERY_STRING_SHORT = "%q"; + public static final String QUERY_STRING = "%{QUERY_STRING}"; + + public static final ExchangeAttribute INSTANCE = new QueryStringAttribute(); + + private QueryStringAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + String qs = exchange.getQueryString(); + if(qs.isEmpty()) { + return qs; + } + return '?' + qs; + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.setQueryString(newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Query String"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(QUERY_STRING) || token.equals(QUERY_STRING_SHORT)) { + return QueryStringAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ReadOnlyAttributeException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ReadOnlyAttributeException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ReadOnlyAttributeException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,37 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.UndertowMessages; + +/** + * An exception that is thrown when an attribute is read only + * + * @author Stuart Douglas + */ +public class ReadOnlyAttributeException extends Exception { + + public ReadOnlyAttributeException() { + } + + public ReadOnlyAttributeException(final String attributeName, final String newValue) { + super(UndertowMessages.MESSAGES.couldNotSetAttribute(attributeName, newValue)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RelativePathAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RelativePathAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RelativePathAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,76 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.QueryParameterUtils; + +/** + * The relative path + * + * @author Stuart Douglas + */ +public class RelativePathAttribute implements ExchangeAttribute { + + public static final String RELATIVE_PATH_SHORT = "%R"; + public static final String RELATIVE_PATH = "%{RELATIVE_PATH}"; + + public static final ExchangeAttribute INSTANCE = new RelativePathAttribute(); + + private RelativePathAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getRelativePath(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + int pos = newValue.indexOf('?'); + if (pos == -1) { + exchange.setRelativePath(newValue); + exchange.setRequestURI(exchange.getResolvedPath() + newValue); + exchange.setRequestPath(exchange.getResolvedPath() + newValue); + } else { + final String path = newValue.substring(0, pos); + exchange.setRelativePath(path); + exchange.setRequestURI(exchange.getResolvedPath() + newValue); + exchange.setRequestPath(exchange.getResolvedPath() + newValue); + + final String newQueryString = newValue.substring(pos); + exchange.setQueryString(newQueryString); + exchange.getQueryParameters().putAll(QueryParameterUtils.parseQueryString(newQueryString.substring(1), QueryParameterUtils.getQueryParamEncoding(exchange))); + } + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Relative Path"; + } + + @Override + public ExchangeAttribute build(final String token) { + return token.equals(RELATIVE_PATH) || token.equals(RELATIVE_PATH_SHORT) ? INSTANCE : null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RemoteIPAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RemoteIPAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RemoteIPAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import java.net.InetSocketAddress; + +import io.undertow.server.HttpServerExchange; + +/** + * The remote IP address + * + * @author Stuart Douglas + */ +public class RemoteIPAttribute implements ExchangeAttribute { + + public static final String REMOTE_IP_SHORT = "%a"; + public static final String REMOTE_HOST_NAME_SHORT = "%h"; + public static final String REMOTE_IP = "%{REMOTE_IP}"; + + public static final ExchangeAttribute INSTANCE = new RemoteIPAttribute(); + + private RemoteIPAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + final InetSocketAddress peerAddress = (InetSocketAddress) exchange.getConnection().getPeerAddress(); + return peerAddress.getAddress().getHostAddress(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Remote IP", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Remote IP"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REMOTE_IP) || token.equals(REMOTE_IP_SHORT) || token.equals(REMOTE_HOST_NAME_SHORT)) { + return RemoteIPAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RemoteUserAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RemoteUserAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RemoteUserAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,69 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpServerExchange; + +/** + * The remote user + * + * @author Stuart Douglas + */ +public class RemoteUserAttribute implements ExchangeAttribute { + + public static final String REMOTE_USER_SHORT = "%u"; + public static final String REMOTE_USER = "%{REMOTE_USER}"; + + public static final ExchangeAttribute INSTANCE = new RemoteUserAttribute(); + + private RemoteUserAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + SecurityContext sc = exchange.getSecurityContext(); + if (sc == null || !sc.isAuthenticated()) { + return null; + } + return sc.getAuthenticatedAccount().getPrincipal().getName(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Remote user", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Remote user"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REMOTE_USER) || token.equals(REMOTE_USER_SHORT)) { + return RemoteUserAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RequestHeaderAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RequestHeaderAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RequestHeaderAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; + +/** + * A request header + * + * @author Stuart Douglas + */ +public class RequestHeaderAttribute implements ExchangeAttribute { + + + private final HttpString requestHeader; + + public RequestHeaderAttribute(final HttpString requestHeader) { + this.requestHeader = requestHeader; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getRequestHeaders().getFirst(requestHeader); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.getRequestHeaders().put(requestHeader, newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request header"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{i,") && token.endsWith("}")) { + final HttpString headerName = HttpString.tryFromString(token.substring(4, token.length() - 1)); + return new RequestHeaderAttribute(headerName); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RequestLineAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RequestLineAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RequestLineAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,74 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The request line + * + * @author Stuart Douglas + */ +public class RequestLineAttribute implements ExchangeAttribute { + + public static final String REQUEST_LINE_SHORT = "%r"; + public static final String REQUEST_LINE = "%{REQUEST_LINE}"; + + public static final ExchangeAttribute INSTANCE = new RequestLineAttribute(); + + private RequestLineAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + StringBuilder sb = new StringBuilder() + .append(exchange.getRequestMethod().toString()) + .append(' ') + .append(exchange.getRequestURI()); + if (!exchange.getQueryString().isEmpty()) { + sb.append('?'); + sb.append(exchange.getQueryString()); + } + sb.append(' ') + .append(exchange.getProtocol().toString()).toString(); + return sb.toString(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Request line", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request line"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_LINE) || token.equals(REQUEST_LINE_SHORT)) { + return RequestLineAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RequestMethodAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RequestMethodAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RequestMethodAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The request method + * + * @author Stuart Douglas + */ +public class RequestMethodAttribute implements ExchangeAttribute { + + public static final String REQUEST_METHOD_SHORT = "%m"; + public static final String REQUEST_METHOD = "%{METHOD}"; + + public static final ExchangeAttribute INSTANCE = new RequestMethodAttribute(); + + private RequestMethodAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getRequestMethod().toString(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Request method", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request method"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_METHOD) || token.equals(REQUEST_METHOD_SHORT)) { + return RequestMethodAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RequestProtocolAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RequestProtocolAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RequestProtocolAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The request protocol + * + * @author Stuart Douglas + */ +public class RequestProtocolAttribute implements ExchangeAttribute { + + public static final String REQUEST_PROTOCOL_SHORT = "%H"; + public static final String REQUEST_PROTOCOL = "%{PROTOCOL}"; + + public static final ExchangeAttribute INSTANCE = new RequestProtocolAttribute(); + + private RequestProtocolAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getProtocol().toString(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Request protocol", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request protocol"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_PROTOCOL) || token.equals(REQUEST_PROTOCOL_SHORT)) { + return RequestProtocolAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/RequestURLAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/RequestURLAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/RequestURLAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.QueryParameterUtils; + +/** + * The request URL + * + * @author Stuart Douglas + */ +public class RequestURLAttribute implements ExchangeAttribute { + + public static final String REQUEST_URL_SHORT = "%U"; + public static final String REQUEST_URL = "%{REQUEST_URL}"; + + public static final ExchangeAttribute INSTANCE = new RequestURLAttribute(); + + private RequestURLAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getRequestURI(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + int pos = newValue.indexOf('?'); + if (pos == -1) { + exchange.setRelativePath(newValue); + exchange.setRequestURI(newValue); + exchange.setRequestPath(newValue); + exchange.setResolvedPath(""); + } else { + final String path = newValue.substring(0, pos); + exchange.setRelativePath(path); + exchange.setRequestURI(path); + exchange.setRequestPath(path); + exchange.setResolvedPath(""); + final String newQueryString = newValue.substring(pos); + exchange.setQueryString(newQueryString); + exchange.getQueryParameters().putAll(QueryParameterUtils.parseQueryString(newQueryString.substring(1), QueryParameterUtils.getQueryParamEncoding(exchange))); + } + + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Request URL"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(REQUEST_URL) || token.equals(REQUEST_URL_SHORT)) { + return RequestURLAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ResponseCodeAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ResponseCodeAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ResponseCodeAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The request status code + * + * @author Stuart Douglas + */ +public class ResponseCodeAttribute implements ExchangeAttribute { + + public static final String RESPONSE_CODE_SHORT = "%s"; + public static final String RESPONSE_CODE = "%{RESPONSE_CODE}"; + + public static final ExchangeAttribute INSTANCE = new ResponseCodeAttribute(); + + private ResponseCodeAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return Integer.toString(exchange.getResponseCode()); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.setResponseCode(Integer.parseInt(newValue)); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response code"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(RESPONSE_CODE) || token.equals(RESPONSE_CODE_SHORT)) { + return ResponseCodeAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ResponseHeaderAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ResponseHeaderAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ResponseHeaderAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; + +/** + * A response header + * + * @author Stuart Douglas + */ +public class ResponseHeaderAttribute implements ExchangeAttribute { + + + private final HttpString responseHeader; + + public ResponseHeaderAttribute(final HttpString responseHeader) { + this.responseHeader = responseHeader; + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return exchange.getResponseHeaders().getFirst(responseHeader); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + exchange.getResponseHeaders().put(responseHeader, newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response header"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.startsWith("%{o,") && token.endsWith("}")) { + final HttpString headerName = HttpString.tryFromString(token.substring(4, token.length() - 1)); + return new ResponseHeaderAttribute(headerName); + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ResponseTimeAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ResponseTimeAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ResponseTimeAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +import java.util.concurrent.TimeUnit; + +/** + * The response time + * + * This will only work if {@link io.undertow.UndertowOptions#RECORD_REQUEST_START_TIME} has been set + */ +public class ResponseTimeAttribute implements ExchangeAttribute { + + public static final String RESPONSE_TIME_MILLIS_SHORT = "%D"; + public static final String RESPONSE_TIME_SECONDS_SHORT = "%T"; + public static final String RESPONSE_TIME_MILLIS = "%{RESPONSE_TIME}"; + + private final TimeUnit timeUnit; + + public ResponseTimeAttribute(TimeUnit timeUnit) { + this.timeUnit = timeUnit; + } + + @Override + public String readAttribute(HttpServerExchange exchange) { + long requestStartTime = exchange.getRequestStartTime(); + if(requestStartTime == -1) { + return null; + } + return String.valueOf(timeUnit.convert(System.nanoTime() - requestStartTime, TimeUnit.NANOSECONDS)); + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Response Time", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Response Time"; + } + + @Override + public ExchangeAttribute build(String token) { + if (token.equals(RESPONSE_TIME_MILLIS) || token.equals(RESPONSE_TIME_MILLIS_SHORT)) { + return new ResponseTimeAttribute(TimeUnit.MILLISECONDS); + } + if (token.equals(RESPONSE_TIME_SECONDS_SHORT)) { + return new ResponseTimeAttribute(TimeUnit.SECONDS); + } + return null; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/attribute/SslCipherAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/SslCipherAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/SslCipherAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,60 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; + +/** + * @author Stuart Douglas + */ +public class SslCipherAttribute implements ExchangeAttribute { + + public static final SslCipherAttribute INSTANCE = new SslCipherAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo(); + if(ssl == null) { + return null; + } + return ssl.getCipherSuite(); + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Cipher", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Cipher"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_CIPHER}")) { + return INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/SslClientCertAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/SslClientCertAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/SslClientCertAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,79 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.RenegotiationRequiredException; +import io.undertow.server.SSLSessionInfo; +import io.undertow.util.Certificates; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.CertificateEncodingException; +import javax.security.cert.X509Certificate; + +/** + * @author Stuart Douglas + */ +public class SslClientCertAttribute implements ExchangeAttribute { + + public static final SslClientCertAttribute INSTANCE = new SslClientCertAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo(); + if(ssl == null) { + return null; + } + X509Certificate[] certificates; + try { + certificates = ssl.getPeerCertificateChain(); + if(certificates.length > 0) { + return Certificates.toPem(certificates[0]); + } + return null; + } catch (SSLPeerUnverifiedException e) { + return null; + } catch (CertificateEncodingException e) { + return null; + } catch (RenegotiationRequiredException e) { + return null; + } + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Client Cert", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Client Cert"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_CLIENT_CERT}")) { + return INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/SslSessionIdAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/SslSessionIdAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/SslSessionIdAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; +import io.undertow.util.FlexBase64; + +/** + * @author Stuart Douglas + */ +public class SslSessionIdAttribute implements ExchangeAttribute { + + public static final SslSessionIdAttribute INSTANCE = new SslSessionIdAttribute(); + + @Override + public String readAttribute(HttpServerExchange exchange) { + SSLSessionInfo ssl = exchange.getConnection().getSslSessionInfo(); + if(ssl == null || ssl.getSessionId() == null) { + return null; + } + return FlexBase64.encodeString(ssl.getSessionId(), false); + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("SSL Session ID", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "SSL Session ID"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals("%{SSL_SESSION_ID}")) { + return INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/SubstituteEmptyWrapper.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/SubstituteEmptyWrapper.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/SubstituteEmptyWrapper.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,34 @@ +package io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +public class SubstituteEmptyWrapper implements ExchangeAttributeWrapper { + + private final String substitute; + + public SubstituteEmptyWrapper(String substitute) { + this.substitute = substitute; + } + + @Override + public ExchangeAttribute wrap(final ExchangeAttribute attribute) { + return new ExchangeAttribute() { + @Override + public String readAttribute(HttpServerExchange exchange) { + String val = attribute.readAttribute(exchange); + if(val == null || val.isEmpty()) { + return substitute; + } + return val; + } + + @Override + public void writeAttribute(HttpServerExchange exchange, String newValue) throws ReadOnlyAttributeException { + attribute.writeAttribute(exchange, newValue); + } + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/attribute/ThreadNameAttribute.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/attribute/ThreadNameAttribute.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/attribute/ThreadNameAttribute.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.attribute; + +import io.undertow.server.HttpServerExchange; + +/** + * The thread name + * + * @author Stuart Douglas + */ +public class ThreadNameAttribute implements ExchangeAttribute { + + public static final String THREAD_NAME_SHORT = "%I"; + public static final String THREAD_NAME = "%{THREAD_NAME}"; + + public static final ExchangeAttribute INSTANCE = new ThreadNameAttribute(); + + private ThreadNameAttribute() { + + } + + @Override + public String readAttribute(final HttpServerExchange exchange) { + return Thread.currentThread().getName(); + } + + @Override + public void writeAttribute(final HttpServerExchange exchange, final String newValue) throws ReadOnlyAttributeException { + throw new ReadOnlyAttributeException("Thread name", newValue); + } + + public static final class Builder implements ExchangeAttributeBuilder { + + @Override + public String name() { + return "Thread name"; + } + + @Override + public ExchangeAttribute build(final String token) { + if (token.equals(THREAD_NAME) || token.equals(THREAD_NAME_SHORT)) { + return ThreadNameAttribute.INSTANCE; + } + return null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/DelegatingStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/DelegatingStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/DelegatingStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,157 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +/** + * @author Stuart Douglas + */ +public abstract class DelegatingStreamSinkChannel implements StreamSinkChannel { + + protected final StreamSinkChannel delegate; + protected final ChannelListener.SimpleSetter writeSetter = new ChannelListener.SimpleSetter<>(); + protected final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + public DelegatingStreamSinkChannel(final StreamSinkChannel delegate) { + this.delegate = delegate; + delegate.getWriteSetter().set(ChannelListeners.delegatingChannelListener((T) this, writeSetter)); + delegate.getCloseSetter().set(ChannelListeners.delegatingChannelListener((T) this, closeSetter)); + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return delegate.transferFrom(src, position, count); + } + + public XnioWorker getWorker() { + return delegate.getWorker(); + } + + public boolean isWriteResumed() { + return delegate.isWriteResumed(); + } + + public boolean flush() throws IOException { + return delegate.flush(); + } + + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + delegate.awaitWritable(time, timeUnit); + } + + public int write(final ByteBuffer src) throws IOException { + return delegate.write(src); + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + return delegate.write(srcs, offset, length); + } + + public void awaitWritable() throws IOException { + delegate.awaitWritable(); + } + + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + return delegate.setOption(option, value); + } + + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + public ChannelListener.Setter getWriteSetter() { + return writeSetter; + } + + public boolean supportsOption(final Option option) { + return delegate.supportsOption(option); + } + + public final long write(final ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + public void resumeWrites() { + delegate.resumeWrites(); + } + + public boolean isOpen() { + return delegate.isOpen(); + } + + public void shutdownWrites() throws IOException { + delegate.shutdownWrites(); + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return delegate.transferFrom(source, count, throughBuffer); + } + + public XnioExecutor getWriteThread() { + return delegate.getWriteThread(); + } + + public void wakeupWrites() { + delegate.wakeupWrites(); + } + + public void close() throws IOException { + delegate.close(); + } + + public T getOption(final Option option) throws IOException { + return delegate.getOption(option); + } + + public void suspendWrites() { + delegate.suspendWrites(); + } + + @Override + public XnioIoThread getIoThread() { + return delegate.getIoThread(); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return delegate.writeFinal(src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return delegate.writeFinal(srcs, offset, length); + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + return delegate.writeFinal(srcs); + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/DelegatingStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/DelegatingStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/DelegatingStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,138 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +/** + * @author Stuart Douglas + */ +public abstract class DelegatingStreamSourceChannel implements StreamSourceChannel { + + protected final ChannelListener.SimpleSetter readSetter = new ChannelListener.SimpleSetter<>(); + protected final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + protected final StreamSourceChannel delegate; + + public DelegatingStreamSourceChannel(final StreamSourceChannel delegate) { + this.delegate = delegate; + delegate.getReadSetter().set(ChannelListeners.delegatingChannelListener((T) this, readSetter)); + delegate.getCloseSetter().set(ChannelListeners.delegatingChannelListener((T) this, closeSetter)); + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + return delegate.transferTo(position, count, target); + } + + public void awaitReadable() throws IOException { + delegate.awaitReadable(); + } + + public void suspendReads() { + delegate.suspendReads(); + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + return delegate.transferTo(count, throughBuffer, target); + } + + public XnioWorker getWorker() { + return delegate.getWorker(); + } + + public boolean isReadResumed() { + return delegate.isReadResumed(); + } + + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + return delegate.setOption(option, value); + } + + public boolean supportsOption(final Option option) { + return delegate.supportsOption(option); + } + + public void shutdownReads() throws IOException { + delegate.shutdownReads(); + } + + public ChannelListener.Setter getReadSetter() { + return readSetter; + } + + public boolean isOpen() { + return delegate.isOpen(); + } + + public long read(final ByteBuffer[] dsts) throws IOException { + return delegate.read(dsts); + } + + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + return delegate.read(dsts, offset, length); + } + + public void wakeupReads() { + delegate.wakeupReads(); + } + + public XnioExecutor getReadThread() { + return delegate.getReadThread(); + } + + public void awaitReadable(final long time, final TimeUnit timeUnit) throws IOException { + delegate.awaitReadable(time, timeUnit); + } + + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + public void close() throws IOException { + delegate.close(); + } + + public T getOption(final Option option) throws IOException { + return delegate.getOption(option); + } + + public void resumeReads() { + delegate.resumeReads(); + } + + public int read(final ByteBuffer dst) throws IOException { + return delegate.read(dst); + } + + @Override + public XnioIoThread getIoThread() { + return delegate.getIoThread(); + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/DetachableStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/DetachableStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/DetachableStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,270 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import io.undertow.UndertowMessages; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +/** + * Stream sink channel. When this channel is considered detached it will no longer forward + * calls to the delegate + * + * @author Stuart Douglas + */ +public abstract class DetachableStreamSinkChannel implements StreamSinkChannel { + + + protected final StreamSinkChannel delegate; + protected ChannelListener.SimpleSetter writeSetter; + protected ChannelListener.SimpleSetter closeSetter; + + public DetachableStreamSinkChannel(final StreamSinkChannel delegate) { + this.delegate = delegate; + } + + protected abstract boolean isFinished(); + + @Override + public void suspendWrites() { + if (isFinished()) { + return; + } + delegate.suspendWrites(); + } + + + @Override + public boolean isWriteResumed() { + if (isFinished()) { + return false; + } + return delegate.isWriteResumed(); + } + + @Override + public void shutdownWrites() throws IOException { + if (isFinished()) { + return; + } + delegate.shutdownWrites(); + } + + @Override + public void awaitWritable() throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + delegate.awaitWritable(); + } + + @Override + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + delegate.awaitWritable(time, timeUnit); + } + + @Override + public XnioExecutor getWriteThread() { + return delegate.getWriteThread(); + } + + @Override + public boolean isOpen() { + return !isFinished() && delegate.isOpen(); + } + + @Override + public void close() throws IOException { + if (isFinished()) return; + delegate.close(); + } + + @Override + public boolean flush() throws IOException { + if (isFinished()) { + return true; + } + return delegate.flush(); + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.transferFrom(src, position, count); + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.transferFrom(source, count, throughBuffer); + } + + @Override + public ChannelListener.Setter getWriteSetter() { + if (writeSetter == null) { + writeSetter = new ChannelListener.SimpleSetter<>(); + if (!isFinished()) { + if(delegate instanceof ConduitStreamSinkChannel) { + ((ConduitStreamSinkChannel) delegate).setWriteListener(ChannelListeners.delegatingChannelListener(this, writeSetter)); + } else { + delegate.getWriteSetter().set(ChannelListeners.delegatingChannelListener(this, writeSetter)); + } + } + } + return writeSetter; + } + + @Override + public ChannelListener.Setter getCloseSetter() { + if (closeSetter == null) { + closeSetter = new ChannelListener.SimpleSetter<>(); + if (!isFinished()) { + delegate.getCloseSetter().set(ChannelListeners.delegatingChannelListener(this, closeSetter)); + } + } + return closeSetter; + } + + @Override + public XnioWorker getWorker() { + return delegate.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return delegate.getIoThread(); + } + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.write(srcs, offset, length); + } + + @Override + public long write(final ByteBuffer[] srcs) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.write(srcs); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.writeFinal(src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.writeFinal(srcs, offset, length); + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.writeFinal(srcs); + } + + @Override + public boolean supportsOption(final Option option) { + return delegate.supportsOption(option); + } + + @Override + public T getOption(final Option option) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.getOption(option); + } + + @Override + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.setOption(option, value); + } + + @Override + public int write(final ByteBuffer src) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.write(src); + } + + @Override + public void resumeWrites() { + if (isFinished()) { + return; + } + delegate.resumeWrites(); + } + + @Override + public void wakeupWrites() { + if (isFinished()) { + return; + } + delegate.wakeupWrites(); + } + + public void responseDone() { + if(delegate instanceof ConduitStreamSinkChannel) { + ((ConduitStreamSinkChannel) delegate).setCloseListener(null); + ((ConduitStreamSinkChannel) delegate).setWriteListener(null); + } else { + delegate.getCloseSetter().set(null); + delegate.getWriteSetter().set(null); + } + if (delegate.isWriteResumed()) { + delegate.suspendWrites(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/DetachableStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/DetachableStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/DetachableStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,214 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; + +import io.undertow.UndertowMessages; + +/** + * A stream source channel that can be marked as detached. Once this is marked as detached then + * calls will no longer be forwarded to the delegate. + * + * @author Stuart Douglas + */ +public abstract class DetachableStreamSourceChannel implements StreamSourceChannel{ + + protected final StreamSourceChannel delegate; + + protected ChannelListener.SimpleSetter readSetter; + protected ChannelListener.SimpleSetter closeSetter; + + public DetachableStreamSourceChannel(final StreamSourceChannel delegate) { + this.delegate = delegate; + } + + protected abstract boolean isFinished(); + + @Override + public void resumeReads() { + if (isFinished()) { + return; + } + delegate.resumeReads(); + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + if (isFinished()) { + return -1; + } + return delegate.transferTo(position, count, target); + } + + public void awaitReadable() throws IOException { + if (isFinished()) { + return; + } + delegate.awaitReadable(); + } + + public void suspendReads() { + if (isFinished()) { + return; + } + delegate.suspendReads(); + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + if (isFinished()) { + return -1; + } + return delegate.transferTo(count, throughBuffer, target); + } + + public XnioWorker getWorker() { + return delegate.getWorker(); + } + + public boolean isReadResumed() { + if (isFinished()) { + return false; + } + return delegate.isReadResumed(); + } + + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return delegate.setOption(option, value); + } + + public boolean supportsOption(final Option option) { + return delegate.supportsOption(option); + } + + public void shutdownReads() throws IOException { + if (isFinished()) { + return; + } + delegate.shutdownReads(); + } + + public ChannelListener.Setter getReadSetter() { + if (readSetter == null) { + readSetter = new ChannelListener.SimpleSetter<>(); + if (!isFinished()) { + if(delegate instanceof ConduitStreamSourceChannel) { + ((ConduitStreamSourceChannel)delegate).setReadListener(ChannelListeners.delegatingChannelListener(this, readSetter)); + } else { + delegate.getReadSetter().set(ChannelListeners.delegatingChannelListener(this, readSetter)); + } + } + } + return readSetter; + } + + public boolean isOpen() { + if (isFinished()) { + return false; + } + return delegate.isOpen(); + } + + public long read(final ByteBuffer[] dsts) throws IOException { + if (isFinished()) { + return -1; + } + return delegate.read(dsts); + } + + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + if (isFinished()) { + return -1; + } + return delegate.read(dsts, offset, length); + } + + public void wakeupReads() { + if (isFinished()) { + return; + } + delegate.wakeupReads(); + } + + public XnioExecutor getReadThread() { + return delegate.getReadThread(); + } + + public void awaitReadable(final long time, final TimeUnit timeUnit) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + delegate.awaitReadable(time, timeUnit); + } + + public ChannelListener.Setter getCloseSetter() { + if (closeSetter == null) { + closeSetter = new ChannelListener.SimpleSetter<>(); + if (!isFinished()) { + if(delegate instanceof ConduitStreamSourceChannel) { + ((ConduitStreamSourceChannel)delegate).setCloseListener(ChannelListeners.delegatingChannelListener(this, closeSetter)); + } else { + delegate.getCloseSetter().set(ChannelListeners.delegatingChannelListener(this, closeSetter)); + } + } + } + return closeSetter; + } + + public void close() throws IOException { + if (isFinished()) { + return; + } + delegate.close(); + } + + public T getOption(final Option option) throws IOException { + if (isFinished()) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + return delegate.getOption(option); + } + + public int read(final ByteBuffer dst) throws IOException { + if (isFinished()) { + return -1; + } + return delegate.read(dst); + } + + @Override + public XnioIoThread getIoThread() { + return delegate.getIoThread(); + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/GatedStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/GatedStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/GatedStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,297 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * A 'gated' stream sink channel. + *

+ * This channel has a gate which starts of closed. When the gate is closed writes will return 0. When the gate is opened + * writes will resume as normal. + * + * @author David M. Lloyd + */ +public final class GatedStreamSinkChannel implements StreamSinkChannel { + private final StreamSinkChannel delegate; + private final ChannelListener.SimpleSetter writeSetter = new ChannelListener.SimpleSetter<>(); + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + /** + * Construct a new instance. + * + * @param delegate the channel to wrap + */ + public GatedStreamSinkChannel(final StreamSinkChannel delegate) { + this.delegate = delegate; + } + + @SuppressWarnings("unused") + private int state; + + private static final int FLAG_GATE_OPEN = 1 << 0; + private static final int FLAG_WRITES_RESUMED = 1 << 1; + private static final int FLAG_CLOSE_REQUESTED = 1 << 2; + private static final int FLAG_CLOSED = 1 << 3; + + /** + * Open the gate and allow data to flow. Once opened, the gate cannot be closed other than closing the channel. + *

+ * If the shutdownWrites() or close() method has already been called this will result it in being invoked on the + * delegate. + */ + public void openGate() throws IOException { + int val = state; + if (allAreSet(val, FLAG_GATE_OPEN)) { + return; + } + state |= FLAG_GATE_OPEN; + if (allAreSet(val, FLAG_CLOSED)) { + delegate.close(); + } else { + if (allAreSet(val, FLAG_CLOSE_REQUESTED)) { + delegate.shutdownWrites(); + } + if (allAreSet(val, FLAG_WRITES_RESUMED)) { + delegate.wakeupWrites(); + } + } + } + + public boolean isGateOpen() { + return allAreSet(state, FLAG_GATE_OPEN); + } + + public XnioWorker getWorker() { + return delegate.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return delegate.getIoThread(); + } + + public XnioExecutor getWriteThread() { + return delegate.getWriteThread(); + } + + public ChannelListener.Setter getWriteSetter() { + return writeSetter; + } + + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.writeFinal(src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.writeFinal(srcs, offset, length); + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.writeFinal(srcs); + } + + public int write(final ByteBuffer src) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.write(src); + } + + public long write(final ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.write(srcs, offset, length); + } + + private boolean handleGate() throws ClosedChannelException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_REQUESTED)) { + throw new ClosedChannelException(); + } + if (anyAreClear(val, FLAG_GATE_OPEN)) { + return true; + } + return false; + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.transferFrom(src, position, count); + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (handleGate()) { + return 0; + } + return delegate.transferFrom(source, count, throughBuffer); + } + + public boolean flush() throws IOException { + if (anyAreClear(state, FLAG_GATE_OPEN)) { + return false; + } + if (anyAreSet(state, FLAG_CLOSED)) { + throw new ClosedChannelException(); + } + if (anyAreSet(state, FLAG_CLOSE_REQUESTED)) { + boolean result = delegate.flush(); + if (result) { + state |= FLAG_CLOSED; + } + return result; + } + return delegate.flush(); + } + + public void suspendWrites() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.suspendWrites(); + } else { + state &= ~FLAG_WRITES_RESUMED; + } + } + + public void resumeWrites() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.resumeWrites(); + } else { + state |= FLAG_WRITES_RESUMED; + } + } + + public boolean isWriteResumed() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + return delegate.isWriteResumed(); + } else { + return anyAreSet(state, FLAG_WRITES_RESUMED); + } + } + + public void wakeupWrites() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.wakeupWrites(); + } else { + state |= FLAG_WRITES_RESUMED; + getIoThread().execute(new Runnable() { + @Override + public void run() { + ChannelListeners.invokeChannelListener(GatedStreamSinkChannel.this, writeSetter.get()); + } + }); + } + } + + public void shutdownWrites() throws IOException { + state |= FLAG_CLOSE_REQUESTED; + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.shutdownWrites(); + } + } + + public void close() throws IOException { + if (allAreSet(state, FLAG_CLOSED)) { + return; + } + state |= FLAG_CLOSED; + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.close(); + } + } + + public void awaitWritable() throws IOException { + if (allAreClear(state, FLAG_GATE_OPEN)) { + throw new IllegalStateException();//we don't allow this, as it results in thread safety issues + } + delegate.awaitWritable(); + } + + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + if (allAreClear(state, FLAG_GATE_OPEN)) { + throw new IllegalStateException();//we don't allow this, as it results in thread safety issues + } + delegate.awaitWritable(time, timeUnit); + } + + public boolean isOpen() { + return allAreClear(state, FLAG_CLOSED); + } + + public boolean supportsOption(final Option option) { + return false; + } + + public T getOption(final Option option) throws IOException { + return null; + } + + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + return null; + } + + /** + * Get the underlying channel if the gate is open, else return this channel. + * + * @return the underlying channel, or this channel if the gate is not open + */ + public StreamSinkChannel getChannel() { + return allAreSet(state, FLAG_GATE_OPEN) ? delegate : this; + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/GatedStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/GatedStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/GatedStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,284 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * A 'gated' stream source channel. + *

+ * This channel has a gate which starts of closed. When the gate is closed reads will return 0. When the gate is opened + * reads will resume as normal. + * + * @author David M. Lloyd + */ +public final class GatedStreamSourceChannel implements StreamSourceChannel { + private final StreamSourceChannel delegate; + private final ChannelListener.SimpleSetter readSetter = new ChannelListener.SimpleSetter<>(); + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + /** + * Construct a new instance. + * + * @param delegate the channel to wrap + */ + public GatedStreamSourceChannel(final StreamSourceChannel delegate) { + this.delegate = delegate; + } + + @SuppressWarnings("unused") + private int state; + + private static final int FLAG_GATE_OPEN = 1 << 0; + private static final int FLAG_READS_RESUMED = 1 << 1; + private static final int FLAG_CLOSE_REQUESTED = 1 << 2; + private static final int FLAG_CLOSED = 1 << 3; + + /** + * Open the gate and allow data to flow. Once opened, the gate cannot be closed other than closing the channel. + *

+ * If the shutdownReads() or close() method has already been called this will result it in being invoked on the + * delegate. + */ + public void openGate() throws IOException { + int val = state; + if (allAreSet(val, FLAG_GATE_OPEN)) { + return; + } + state |= FLAG_GATE_OPEN; + if (allAreSet(val, FLAG_CLOSED)) { + delegate.close(); + } else { + if (allAreSet(val, FLAG_CLOSE_REQUESTED)) { + delegate.shutdownReads(); + } + if (allAreSet(val, FLAG_READS_RESUMED)) { + delegate.wakeupReads(); + } + } + } + + public boolean isGateOpen() { + return allAreSet(state, FLAG_GATE_OPEN); + } + + public XnioWorker getWorker() { + return delegate.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return delegate.getIoThread(); + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_REQUESTED)) { + return -1; + } + if (anyAreClear(val, FLAG_GATE_OPEN)) { + return 0; + } + return delegate.transferTo(position, count, target); + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_REQUESTED)) { + return -1; + } + if (anyAreClear(val, FLAG_GATE_OPEN)) { + return 0; + } + return delegate.transferTo(count, throughBuffer, target); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_REQUESTED)) { + return -1; + } + if (anyAreClear(val, FLAG_GATE_OPEN)) { + return 0; + } + return delegate.read(dsts, offset, length); + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_REQUESTED)) { + return -1; + } + if (anyAreClear(val, FLAG_GATE_OPEN)) { + return 0; + } + return delegate.read(dsts); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_REQUESTED)) { + return -1; + } + if (anyAreClear(val, FLAG_GATE_OPEN)) { + return 0; + } + return delegate.read(dst); + } + @Override + public void suspendReads() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.suspendReads(); + } else { + state &= ~FLAG_READS_RESUMED; + } + } + + @Override + public void resumeReads() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.resumeReads(); + } else { + state |= FLAG_READS_RESUMED; + } + } + + @Override + public boolean isReadResumed() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + return delegate.isReadResumed(); + } else { + return anyAreSet(state, FLAG_READS_RESUMED); + } + } + + @Override + public void wakeupReads() { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.resumeReads(); + } else { + state |= FLAG_READS_RESUMED; + getIoThread().execute(new Runnable() { + @Override + public void run() { + ChannelListeners.invokeChannelListener(GatedStreamSourceChannel.this, readSetter.get()); + } + }); + } + } + + @Override + public void shutdownReads() throws IOException { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.shutdownReads(); + } else { + state |= FLAG_CLOSE_REQUESTED; + } + } + + @Override + public void awaitReadable() throws IOException { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.awaitReadable(); + } else { + throw new IllegalStateException(); + } + } + + @Override + public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.awaitReadable(time, timeUnit); + } else { + throw new IllegalStateException(); + } + } + + @Override + public XnioExecutor getReadThread() { + return delegate.getIoThread(); + } + + @Override + public ChannelListener.Setter getReadSetter() { + return readSetter; + } + + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + public void close() throws IOException { + if (allAreSet(state, FLAG_CLOSED)) { + return; + } + state |= FLAG_CLOSED; + if (anyAreSet(state, FLAG_GATE_OPEN)) { + delegate.close(); + } + } + + public boolean isOpen() { + return allAreClear(state, FLAG_CLOSED); + } + + public boolean supportsOption(final Option option) { + return false; + } + + public T getOption(final Option option) throws IOException { + return null; + } + + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + return null; + } + + /** + * Get the underlying channel if the gate is open, else return this channel. + * + * @return the underlying channel, or this channel if the gate is not open + */ + public StreamSourceChannel getChannel() { + return allAreSet(state, FLAG_GATE_OPEN) ? delegate : this; + } + +} Index: 3rdParty_sources/undertow/io/undertow/channels/ReadTimeoutStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/ReadTimeoutStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/ReadTimeoutStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,137 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowLogger; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Options; +import org.xnio.XnioExecutor; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +/** + * Wrapper for read timeout. This should always be the first wrapper applied to the underlying channel. + * + * @author Stuart Douglas + * @see org.xnio.Options#READ_TIMEOUT + */ +public final class ReadTimeoutStreamSourceChannel extends DelegatingStreamSourceChannel { + + private int readTimeout; + private XnioExecutor.Key handle; + + private final Runnable timeoutCommand = new Runnable() { + @Override + public void run() { + UndertowLogger.REQUEST_LOGGER.tracef("Timing out channel %s due to inactivity"); + try { + if (delegate.isReadResumed()) { + ChannelListeners.invokeChannelListener(ReadTimeoutStreamSourceChannel.this, readSetter.get()); + } + } finally { + IoUtils.safeClose(delegate); + } + } + }; + + /** + * @param delegate The underlying channel + * @param readTimeout The read timeout, in milliseconds + */ + public ReadTimeoutStreamSourceChannel(final StreamSourceChannel delegate) { + super(delegate); + try { + Integer timeout = delegate.getOption(Options.READ_TIMEOUT); + if (timeout != null) { + this.readTimeout = timeout; + } else { + this.readTimeout = 0; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void handleReadTimeout(final long ret) { + if (readTimeout > 0) { + if (ret == 0 && handle == null) { + handle = delegate.getReadThread().executeAfter(timeoutCommand, readTimeout, TimeUnit.MILLISECONDS); + } else if (ret > 0 && handle != null) { + handle.remove(); + } + } + } + + @Override + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + long ret = delegate.transferTo(position, count, target); + handleReadTimeout(ret); + return ret; + } + + @Override + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + long ret = delegate.transferTo(count, throughBuffer, target); + handleReadTimeout(ret); + return ret; + } + + @Override + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + long ret = delegate.read(dsts, offset, length); + handleReadTimeout(ret); + return ret; + } + + @Override + public long read(final ByteBuffer[] dsts) throws IOException { + long ret = delegate.read(dsts); + handleReadTimeout(ret); + return ret; + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + int ret = delegate.read(dst); + handleReadTimeout(ret); + return ret; + } + + @Override + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + T ret = super.setOption(option, value); + if (option == Options.READ_TIMEOUT) { + readTimeout = (Integer) value; + if (handle != null) { + handle.remove(); + if (readTimeout > 0) { + getReadThread().executeAfter(timeoutCommand, readTimeout, TimeUnit.MILLISECONDS); + } + } + } + return ret; + } +} Index: 3rdParty_sources/undertow/io/undertow/channels/WriteTimeoutStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/channels/WriteTimeoutStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/channels/WriteTimeoutStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,151 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.channels; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowLogger; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Options; +import org.xnio.XnioExecutor; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +/** + * Wrapper for write timeout. This should always be the first wrapper applied to the underlying channel. + *

+ * + * @author Stuart Douglas + * @see org.xnio.Options#WRITE_TIMEOUT + */ +public final class WriteTimeoutStreamSinkChannel extends DelegatingStreamSinkChannel { + + private int writeTimeout; + private XnioExecutor.Key handle; + + private final Runnable timeoutCommand = new Runnable() { + @Override + public void run() { + UndertowLogger.REQUEST_LOGGER.tracef("Timing out channel %s due to inactivity"); + try { + if (delegate.isWriteResumed()) { + ChannelListeners.invokeChannelListener(WriteTimeoutStreamSinkChannel.this, writeSetter.get()); + } + } finally { + IoUtils.safeClose(delegate); + } + } + }; + + /** + * @param delegate The underlying channel + */ + public WriteTimeoutStreamSinkChannel(final StreamSinkChannel delegate) { + super(delegate); + try { + Integer timeout = delegate.getOption(Options.WRITE_TIMEOUT); + if (timeout != null) { + this.writeTimeout = timeout; + } else { + this.writeTimeout = 0; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private void handleWriteTimeout(final long ret) { + if (writeTimeout > 0) { + if (ret == 0 && handle == null) { + handle = delegate.getWriteThread().executeAfter(timeoutCommand, writeTimeout, TimeUnit.MILLISECONDS); + } else if (ret > 0 && handle != null) { + handle.remove(); + } + } + } + + @Override + public int write(final ByteBuffer src) throws IOException { + int ret = delegate.write(src); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + long ret = delegate.write(srcs, offset, length); + handleWriteTimeout(ret); + return ret; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + int ret = delegate.writeFinal(src); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + long ret = delegate.writeFinal(srcs, offset, length); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + long ret = delegate.writeFinal(srcs); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + long ret = delegate.transferFrom(src, position, count); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + long ret = delegate.transferFrom(source, count, throughBuffer); + handleWriteTimeout(ret); + return ret; + } + + @Override + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + T ret = super.setOption(option, value); + if (option == Options.WRITE_TIMEOUT) { + writeTimeout = (Integer) value; + if (handle != null) { + handle.remove(); + if(writeTimeout > 0) { + getWriteThread().executeAfter(timeoutCommand, writeTimeout, TimeUnit.MILLISECONDS); + } + } + } + return ret; + } +} Index: 3rdParty_sources/undertow/io/undertow/client/ClientCallback.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ClientCallback.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ClientCallback.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import java.io.IOException; + +/** + * @author Emanuel Muckenhuber + */ +public interface ClientCallback { + + /** + * Invoked when an operation completed. + * + * @param result the operation result + */ + void completed(T result); + + /** + * Invoked when the operation failed. + * + * @param e the exception + */ + void failed(IOException e); + +} Index: 3rdParty_sources/undertow/io/undertow/client/ClientConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ClientConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ClientConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,91 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import org.xnio.ChannelListener; +import org.xnio.Option; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; + +/** + * A client connection. This can be used to send requests, or to upgrade the connection. + *

+ * In general these objects are not thread safe, they should only be used by the IO thread + * that is responsible for the connection. As a result this client does not provide a mechanism + * to perform blocking IO, it is designed for async operation only. + * + * @author Stuart Douglas + */ +public interface ClientConnection extends Channel { + + /** + * Sends a client request. The request object should not be modified after it has been submitted to the connection. + *

+ * Request objects can be queued. Once the request is in a state that it is ready to be sent the {@code clientCallback} + * is invoked to provide the caller with the {@link ClientExchange} + *

+ * Note that the request header may not be written out until after the callback has been invoked. This allows the + * client to write out a header with a gathering write if the request contains content. + * + * @param request The request to send. + * @return The resulting client exchange, that can be used to send the request body and read the response + */ + void sendRequest(final ClientRequest request, final ClientCallback clientCallback); + + /** + * Upgrade the connection, if the underlying protocol supports it. This should only be called after an upgrade request + * has been submitted and the target server has accepted the upgrade. + * + * @return The resulting StreamConnection + */ + StreamConnection performUpgrade() throws IOException; + + Pool getBufferPool(); + + SocketAddress getPeerAddress(); + + A getPeerAddress(Class type); + + ChannelListener.Setter getCloseSetter(); + + SocketAddress getLocalAddress(); + + A getLocalAddress(Class type); + + XnioWorker getWorker(); + + XnioIoThread getIoThread(); + + boolean isOpen(); + + boolean supportsOption(Option option); + + T getOption(Option option) throws IOException; + + T setOption(Option option, T value) throws IllegalArgumentException, IOException; + + boolean isUpgraded(); +} Index: 3rdParty_sources/undertow/io/undertow/client/ClientExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ClientExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ClientExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import io.undertow.util.Attachable; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +/** + * @author Stuart Douglas + */ +public interface ClientExchange extends Attachable { + + void setResponseListener(final ClientCallback responseListener); + + void setContinueHandler(final ContinueNotification continueHandler); + + /** + * Returns the request channel that can be used to send data to the server. + * + * @return The request channel + */ + StreamSinkChannel getRequestChannel(); + + /** + * Returns the response channel that can be used to read data from the target server. + * + * @return The response channel + */ + StreamSourceChannel getResponseChannel(); + + ClientRequest getRequest(); + + /** + * + * @return The client response, or null if it has not been received yet + */ + ClientResponse getResponse(); + + /** + * + * @return the result of a HTTP 100-continue response + */ + ClientResponse getContinueResponse(); + + /** + * + * @return The underlying connection + */ + ClientConnection getConnection(); +} Index: 3rdParty_sources/undertow/io/undertow/client/ClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.ssl.XnioSsl; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Set; + +/** + * A client connection provider. This allows the difference between various connection + * providers to be abstracted away (HTTP, AJP etc). + * + * @author Stuart Douglas + */ +public interface ClientProvider { + + Set handlesSchemes(); + + void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options); + + void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options); + + void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options); + + void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options); + +} Index: 3rdParty_sources/undertow/io/undertow/client/ClientRequest.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ClientRequest.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ClientRequest.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import io.undertow.util.AbstractAttachable; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import io.undertow.util.Protocols; + +/** + * A client request. This class should not be modified once it has been submitted to the {@link ClientConnection}. + * + * This class only represents the HTTP header, it does not represent an entity body. If the request needs an entity + * body then this must be specified by either setting a Content-Length or Transfer-Encoding header, otherwise + * the client will assume that the body is empty. + * + * @author Stuart Douglas + */ +public final class ClientRequest extends AbstractAttachable { + + private final HeaderMap requestHeaders = new HeaderMap(); + private String path = "/"; + private HttpString method = Methods.GET; + private HttpString protocol = Protocols.HTTP_1_1; + + public HeaderMap getRequestHeaders() { + return requestHeaders; + } + + public String getPath() { + return path; + } + + public HttpString getMethod() { + return method; + } + + public HttpString getProtocol() { + return protocol; + } + + public ClientRequest setPath(String path) { + this.path = path; + return this; + } + + public ClientRequest setMethod(HttpString method) { + this.method = method; + return this; + } + + public ClientRequest setProtocol(HttpString protocol) { + this.protocol = protocol; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/client/ClientResponse.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ClientResponse.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ClientResponse.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import io.undertow.util.AbstractAttachable; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; + +/** + * A client response. This just contains the parsed response header, the response body + * can be read from the {@link ClientExchange}. + * + * @author Stuart Douglas + */ +public final class ClientResponse extends AbstractAttachable { + + private final HeaderMap responseHeaders; + private final int responseCode; + private final String status; + private final HttpString protocol; + + public ClientResponse(int responseCode, String status, HttpString protocol) { + this.responseCode = responseCode; + this.status = status; + this.protocol = protocol; + this.responseHeaders = new HeaderMap(); + } + + public ClientResponse(int responseCode, String status, HttpString protocol, HeaderMap headers) { + this.responseCode = responseCode; + this.status = status; + this.protocol = protocol; + this.responseHeaders = headers; + } + public HeaderMap getResponseHeaders() { + return responseHeaders; + } + + public HttpString getProtocol() { + return protocol; + } + + public int getResponseCode() { + return responseCode; + } + + public String getStatus() { + return status; + } +} Index: 3rdParty_sources/undertow/io/undertow/client/ContinueNotification.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ContinueNotification.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ContinueNotification.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,30 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +/** + * Callback class that provides a notification of a HTTP 100 Continue response in the client. + * + * @author Stuart Douglas + */ +public interface ContinueNotification { + + void handleContinue(ClientExchange exchange); + +} Index: 3rdParty_sources/undertow/io/undertow/client/ProxiedRequestAttachments.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ProxiedRequestAttachments.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ProxiedRequestAttachments.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import io.undertow.util.AttachmentKey; + +/** + * Additional attachments that are specific to requests that are being proxied from one server to another + * + * @author Stuart Douglas + */ +public class ProxiedRequestAttachments { + + public static final AttachmentKey REMOTE_ADDRESS = AttachmentKey.create(String.class); + public static final AttachmentKey REMOTE_HOST = AttachmentKey.create(String.class); + public static final AttachmentKey SERVER_NAME = AttachmentKey.create(String.class); + public static final AttachmentKey SERVER_PORT = AttachmentKey.create(Integer.class); + public static final AttachmentKey IS_SSL = AttachmentKey.create(Boolean.class); + + public static final AttachmentKey REMOTE_USER = AttachmentKey.create(String.class); + public static final AttachmentKey AUTH_TYPE = AttachmentKey.create(String.class); + public static final AttachmentKey ROUTE = AttachmentKey.create(String.class); + public static final AttachmentKey SSL_CERT = AttachmentKey.create(String.class); + public static final AttachmentKey SSL_CYPHER = AttachmentKey.create(String.class); + public static final AttachmentKey SSL_SESSION_ID = AttachmentKey.create(byte[].class); + public static final AttachmentKey SSL_KEY_SIZE = AttachmentKey.create(Integer.class); + public static final AttachmentKey SECRET = AttachmentKey.create(String.class); + +} Index: 3rdParty_sources/undertow/io/undertow/client/UndertowClient.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/UndertowClient.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/UndertowClient.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,177 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import org.xnio.FutureResult; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.ssl.XnioSsl; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * Undertow client class. This class loads {@link ClientProvider} implementations, and uses them to + * create connections to a target. + * + * @author Stuart Douglas + */ +public final class UndertowClient { + + private final Map clientProviders; + + private static final UndertowClient INSTANCE = new UndertowClient(); + + private UndertowClient() { + this(UndertowClient.class.getClassLoader()); + } + + private UndertowClient(final ClassLoader classLoader) { + ServiceLoader providers = ServiceLoader.load(ClientProvider.class, classLoader); + final Map map = new HashMap<>(); + for (ClientProvider provider : providers) { + for (String scheme : provider.handlesSchemes()) { + map.put(scheme, provider); + } + } + this.clientProviders = Collections.unmodifiableMap(map); + } + + public IoFuture connect(final URI uri, final XnioWorker worker, Pool bufferPool, OptionMap options) { + return connect(uri, worker, null, bufferPool, options); + } + + public IoFuture connect(InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, Pool bufferPool, OptionMap options) { + return connect(bindAddress, uri, worker, null, bufferPool, options); + } + + public IoFuture connect(final URI uri, final XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options) { + return connect((InetSocketAddress) null, uri, worker, ssl, bufferPool, options); + } + + public IoFuture connect(InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options) { + ClientProvider provider = getClientProvider(uri); + final FutureResult result = new FutureResult<>(); + provider.connect(new ClientCallback() { + @Override + public void completed(ClientConnection r) { + result.setResult(r); + } + + @Override + public void failed(IOException e) { + result.setException(e); + } + }, bindAddress, uri, worker, ssl, bufferPool, options); + return result.getIoFuture(); + } + + public IoFuture connect(final URI uri, final XnioIoThread ioThread, Pool bufferPool, OptionMap options) { + return connect((InetSocketAddress) null, uri, ioThread, null, bufferPool, options); + } + + + public IoFuture connect(InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, Pool bufferPool, OptionMap options) { + return connect(bindAddress, uri, ioThread, null, bufferPool, options); + } + + public IoFuture connect(final URI uri, final XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options) { + return connect((InetSocketAddress) null, uri, ioThread, ssl, bufferPool, options); + } + + public IoFuture connect(InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options) { + ClientProvider provider = getClientProvider(uri); + final FutureResult result = new FutureResult<>(); + provider.connect(new ClientCallback() { + @Override + public void completed(ClientConnection r) { + result.setResult(r); + } + + @Override + public void failed(IOException e) { + result.setException(e); + } + }, bindAddress, uri, ioThread, ssl, bufferPool, options); + return result.getIoFuture(); + } + + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, Pool bufferPool, OptionMap options) { + connect(listener, uri, worker, null, bufferPool, options); + } + + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, Pool bufferPool, OptionMap options) { + connect(listener, bindAddress, uri, worker, null, bufferPool, options); + } + + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options) { + ClientProvider provider = getClientProvider(uri); + provider.connect(listener, uri, worker, ssl, bufferPool, options); + } + + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options) { + ClientProvider provider = getClientProvider(uri); + provider.connect(listener, bindAddress, uri, worker, ssl, bufferPool, options); + } + + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, Pool bufferPool, OptionMap options) { + connect(listener, uri, ioThread, null, bufferPool, options); + } + + + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, Pool bufferPool, OptionMap options) { + connect(listener, bindAddress, uri, ioThread, null, bufferPool, options); + } + + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options) { + ClientProvider provider = getClientProvider(uri); + provider.connect(listener, uri, ioThread, ssl, bufferPool, options); + } + + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options) { + ClientProvider provider = getClientProvider(uri); + provider.connect(listener, bindAddress, uri, ioThread, ssl, bufferPool, options); + } + + private ClientProvider getClientProvider(URI uri) { + ClientProvider provider = clientProviders.get(uri.getScheme()); + if (provider == null) { + throw UndertowClientMessages.MESSAGES.unknownScheme(uri); + } + return provider; + } + + public static UndertowClient getInstance() { + return INSTANCE; + } + + public static UndertowClient getInstance(final ClassLoader classLoader) { + return new UndertowClient(classLoader); + } + +} Index: 3rdParty_sources/undertow/io/undertow/client/UndertowClientMessages.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/UndertowClientMessages.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/UndertowClientMessages.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,77 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client; + +import io.undertow.util.HttpString; +import org.jboss.logging.Messages; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageBundle; + +import java.io.IOException; +import java.net.URI; + +/** + * starting from 1000 + * + * @author Emanuel Muckenhuber + */ +@MessageBundle(projectCode = "UT") +public interface UndertowClientMessages { + + UndertowClientMessages MESSAGES = Messages.getBundle(UndertowClientMessages.class); + + // 1000 + @Message(id = 1000, value = "Connection closed") + String connectionClosed(); + + @Message(id = 1001, value = "Request already written") + IllegalStateException requestAlreadyWritten(); + + // 1020 + @Message(id = 1020, value = "Failed to upgrade channel due to response %s (%s)") + String failedToUpgradeChannel(final int responseCode, String reason); + + // 1030 + @Message(id = 1030, value = "invalid content length %d") + IllegalArgumentException illegalContentLength(long length); + + @Message(id = 1031, value = "Unknown scheme in URI %s") + IllegalArgumentException unknownScheme(URI uri); + + @Message(id = 1032, value = "Unknown transfer encoding %s") + IOException unknownTransferEncoding(String transferEncodingString); + + @Message(id = 1033, value = "Invalid connection state") + IOException invalidConnectionState(); + + @Message(id = 1034, value = "Unknown AJP packet type %s") + IOException unknownAjpMessageType(byte packetType); + + @Message(id = 1035, value = "Unknown method type for AJP request %s") + IOException unknownMethod(HttpString method); + + @Message(id = 1036, value = "Data still remaining in chunk %s") + IOException dataStillRemainingInChunk(long remaining); + + @Message(id = 1037, value = "Wrong magic number, expected %s, actual %s") + IOException wrongMagicNumber(String expected, String actual); + + @Message(id = 1038, value = "Received invalid AJP chunk %s with response already complete") + IOException receivedInvalidChunk(byte prefix); +} Index: 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,321 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.ajp; + +import static io.undertow.util.Headers.CLOSE; +import static io.undertow.util.Headers.CONNECTION; +import static io.undertow.util.Headers.CONTENT_LENGTH; +import static io.undertow.util.Headers.TRANSFER_ENCODING; +import static io.undertow.util.Headers.UPGRADE; +import static org.xnio.Bits.anyAreSet; +import static org.xnio.IoUtils.safeClose; + +import java.io.Closeable; +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.Channels; +import org.xnio.channels.StreamSinkChannel; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.UndertowClientMessages; +import io.undertow.protocols.ajp.AbstractAjpClientStreamSourceChannel; +import io.undertow.protocols.ajp.AjpClientChannel; +import io.undertow.protocols.ajp.AjpClientRequestClientStreamSinkChannel; +import io.undertow.protocols.ajp.AjpClientResponseStreamSourceChannel; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.Protocols; + +/** + * @author David M. Lloyd + */ +class AjpClientConnection extends AbstractAttachable implements Closeable, ClientConnection { + + public final ChannelListener requestFinishListener = new ChannelListener() { + @Override + public void handleEvent(AjpClientRequestClientStreamSinkChannel channel) { + currentRequest.terminateRequest(); + } + }; + public final ChannelListener responseFinishedListener = new ChannelListener() { + @Override + public void handleEvent(AjpClientResponseStreamSourceChannel channel) { + currentRequest.terminateResponse(); + } + }; + + private final Deque pendingQueue = new ArrayDeque<>(); + private AjpClientExchange currentRequest; + + private final OptionMap options; + private final AjpClientChannel connection; + + private final Pool bufferPool; + + private static final int UPGRADED = 1 << 28; + private static final int UPGRADE_REQUESTED = 1 << 29; + private static final int CLOSE_REQ = 1 << 30; + private static final int CLOSED = 1 << 31; + + private int state; + + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + private final ClientReceiveListener clientReceiveListener = new ClientReceiveListener(); + + AjpClientConnection(final AjpClientChannel connection, final OptionMap options, final Pool bufferPool) { + this.options = options; + this.connection = connection; + this.bufferPool = bufferPool; + + connection.addCloseTask(new ChannelListener() { + @Override + public void handleEvent(AjpClientChannel channel) { + ChannelListeners.invokeChannelListener(AjpClientConnection.this, closeSetter.get()); + } + }); + connection.getReceiveSetter().set(new ClientReceiveListener()); + connection.resumeReceives(); + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } + + + @Override + public SocketAddress getPeerAddress() { + return connection.getPeerAddress(); + } + + @Override + public A getPeerAddress(Class type) { + return connection.getPeerAddress(type); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public SocketAddress getLocalAddress() { + return connection.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return connection.getLocalAddress(type); + } + + @Override + public XnioWorker getWorker() { + return connection.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return connection.getIoThread(); + } + + @Override + public boolean isOpen() { + return connection.isOpen(); + } + + @Override + public boolean supportsOption(Option option) { + return connection.supportsOption(option); + } + + + @Override + public T getOption(Option option) throws IOException { + return connection.getOption(option); + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return connection.setOption(option, value); + } + + @Override + public boolean isUpgraded() { + return anyAreSet(state, UPGRADE_REQUESTED | UPGRADED); + } + + @Override + public void sendRequest(final ClientRequest request, final ClientCallback clientCallback) { + if (anyAreSet(state, UPGRADE_REQUESTED | UPGRADED | CLOSE_REQ | CLOSED)) { + clientCallback.failed(UndertowClientMessages.MESSAGES.invalidConnectionState()); + return; + } + final AjpClientExchange AjpClientExchange = new AjpClientExchange(clientCallback, request, this); + if (currentRequest == null) { + initiateRequest(AjpClientExchange); + } else { + pendingQueue.add(AjpClientExchange); + } + } + + private void initiateRequest(AjpClientExchange AjpClientExchange) { + currentRequest = AjpClientExchange; + ClientRequest request = AjpClientExchange.getRequest(); + + String connectionString = request.getRequestHeaders().getFirst(CONNECTION); + if (connectionString != null) { + if (CLOSE.equalToString(connectionString)) { + state |= CLOSE_REQ; + } + } else if (request.getProtocol() != Protocols.HTTP_1_1) { + state |= CLOSE_REQ; + } + if (request.getRequestHeaders().contains(UPGRADE)) { + state |= UPGRADE_REQUESTED; + } + + long length = 0; + String fixedLengthString = request.getRequestHeaders().getFirst(CONTENT_LENGTH); + String transferEncodingString = request.getRequestHeaders().getLast(TRANSFER_ENCODING); + + if (fixedLengthString != null) { + length = Long.parseLong(fixedLengthString); + } else if (transferEncodingString != null) { + length = -1; + } + + AjpClientRequestClientStreamSinkChannel sinkChannel = connection.sendRequest(request.getMethod(), request.getPath(), request.getProtocol(), request.getRequestHeaders(), request, requestFinishListener); + currentRequest.setRequestChannel(sinkChannel); + + AjpClientExchange.invokeReadReadyCallback(AjpClientExchange); + if (length == 0) { + //if there is no content we flush the response channel. + //otherwise it is up to the user + try { + sinkChannel.shutdownWrites(); + if (!sinkChannel.flush()) { + handleFailedFlush(sinkChannel); + } + } catch (IOException e) { + handleError(e); + } + } + } + + private void handleFailedFlush(AjpClientRequestClientStreamSinkChannel sinkChannel) { + sinkChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkChannel channel, IOException exception) { + handleError(exception); + } + })); + sinkChannel.resumeWrites(); + } + + private void handleError(IOException exception) { + currentRequest.setFailed(exception); + safeClose(connection); + } + + public StreamConnection performUpgrade() throws IOException { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + public void close() throws IOException { + if (anyAreSet(state, CLOSED)) { + return; + } + state |= CLOSED | CLOSE_REQ; + connection.close(); + } + + /** + * Notification that the current request is finished + */ + public void requestDone() { + currentRequest = null; + + if (anyAreSet(state, CLOSE_REQ)) { + safeClose(connection); + } else if (anyAreSet(state, UPGRADE_REQUESTED)) { + safeClose(connection); //we don't support upgrade, just close the connection to be safe + return; + } + + AjpClientExchange next = pendingQueue.poll(); + + if (next != null) { + initiateRequest(next); + } + } + + public void requestClose() { + state |= CLOSE_REQ; + } + + + class ClientReceiveListener implements ChannelListener { + + public void handleEvent(AjpClientChannel channel) { + try { + AbstractAjpClientStreamSourceChannel result = channel.receive(); + if(result == null) { + return; + } + + if(result instanceof AjpClientResponseStreamSourceChannel) { + AjpClientResponseStreamSourceChannel response = (AjpClientResponseStreamSourceChannel) result; + response.setFinishListener(responseFinishedListener); + ClientResponse cr = new ClientResponse(response.getStatusCode(), response.getReasonPhrase(), currentRequest.getRequest().getProtocol(), response.getHeaders()); + if (response.getStatusCode() == 100) { + currentRequest.setContinueResponse(cr); + } else { + currentRequest.setResponseChannel(response); + currentRequest.setResponse(cr); + } + } else { + //TODO: ping, pong ETC + Channels.drain(result, Long.MAX_VALUE); + } + + } catch (Exception e) { + UndertowLogger.CLIENT_LOGGER.exceptionProcessingRequest(e); + safeClose(connection); + currentRequest.setFailed(e instanceof IOException ? (IOException) e : new IOException(e)); + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,197 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.ajp; + +import io.undertow.channels.DetachableStreamSinkChannel; +import io.undertow.channels.DetachableStreamSourceChannel; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.ContinueNotification; +import io.undertow.protocols.ajp.AjpClientChannel; +import io.undertow.protocols.ajp.AjpClientRequestClientStreamSinkChannel; +import io.undertow.protocols.ajp.AjpClientResponseStreamSourceChannel; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.Headers; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; + +import static org.xnio.Bits.anyAreSet; + +/** + * @author Stuart Douglas + */ +class AjpClientExchange extends AbstractAttachable implements ClientExchange { + + private final ClientRequest request; + private final boolean requiresContinue; + private final AjpClientConnection clientConnection; + + private ClientCallback responseCallback; + private ClientCallback readyCallback; + private ContinueNotification continueNotification; + private AjpClientChannel ajpClientChannel; + + private ClientResponse response; + private ClientResponse continueResponse; + private IOException failedReason; + + private AjpClientResponseStreamSourceChannel responseChannel; + private AjpClientRequestClientStreamSinkChannel requestChannel; + + private int state = 0; + private static final int REQUEST_TERMINATED = 1; + private static final int RESPONSE_TERMINATED = 1 << 1; + + public AjpClientExchange(ClientCallback readyCallback, ClientRequest request, AjpClientConnection clientConnection) { + this.readyCallback = readyCallback; + this.request = request; + this.clientConnection = clientConnection; + boolean reqContinue = false; + if (request.getRequestHeaders().contains(Headers.EXPECT)) { + for (String header : request.getRequestHeaders().get(Headers.EXPECT)) { + if (header.equals("100-continue")) { + reqContinue = true; + } + } + } + this.requiresContinue = reqContinue; + } + + void terminateRequest() { + state |= REQUEST_TERMINATED; + if (anyAreSet(state, RESPONSE_TERMINATED)) { + clientConnection.requestDone(); + } + } + + void terminateResponse() { + state |= RESPONSE_TERMINATED; + if (anyAreSet(state, REQUEST_TERMINATED)) { + clientConnection.requestDone(); + } + } + + public boolean isRequiresContinue() { + return requiresContinue; + } + + + void setContinueResponse(ClientResponse response) { + this.continueResponse = response; + if (continueNotification != null) { + this.continueNotification.handleContinue(this); + } + } + + void setResponse(ClientResponse response) { + this.response = response; + if (responseCallback != null) { + this.responseCallback.completed(this); + } + } + + @Override + public void setResponseListener(ClientCallback listener) { + this.responseCallback = listener; + if (listener != null) { + if (failedReason != null) { + listener.failed(failedReason); + } else if (response != null) { + listener.completed(this); + } + } + } + + @Override + public void setContinueHandler(ContinueNotification continueHandler) { + this.continueNotification = continueHandler; + } + + void setFailed(IOException e) { + this.failedReason = e; + if (readyCallback != null) { + readyCallback.failed(e); + readyCallback = null; + } + if (responseCallback != null) { + responseCallback.failed(e); + responseCallback = null; + } + } + + @Override + public StreamSinkChannel getRequestChannel() { + return new DetachableStreamSinkChannel(requestChannel) { + @Override + protected boolean isFinished() { + return anyAreSet(state, REQUEST_TERMINATED); + } + }; + } + + @Override + public StreamSourceChannel getResponseChannel() { + return new DetachableStreamSourceChannel(responseChannel) { + @Override + protected boolean isFinished() { + return anyAreSet(state, RESPONSE_TERMINATED); + } + }; + } + + @Override + public ClientRequest getRequest() { + return request; + } + + @Override + public ClientResponse getResponse() { + return response; + } + + @Override + public ClientResponse getContinueResponse() { + return continueResponse; + } + + @Override + public ClientConnection getConnection() { + return clientConnection; + } + + void setResponseChannel(AjpClientResponseStreamSourceChannel responseChannel) { + this.responseChannel = responseChannel; + } + + void setRequestChannel(AjpClientRequestClientStreamSinkChannel requestChannel) { + this.requestChannel = requestChannel; + } + + void invokeReadReadyCallback(final ClientExchange result) { + if(readyCallback != null) { + readyCallback.completed(result); + readyCallback = null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/ajp/AjpClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,113 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.ajp; + +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientProvider; +import io.undertow.protocols.ajp.AjpClientChannel; + +import org.xnio.ChannelListener; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.ssl.XnioSsl; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class AjpClientProvider implements ClientProvider { + + @Override + public Set handlesSchemes() { + return new HashSet<>(Arrays.asList(new String[]{"ajp"})); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, worker, ssl, bufferPool, options); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, ioThread, ssl, bufferPool, options); + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + ChannelListener openListener = new ChannelListener() { + @Override + public void handleEvent(StreamConnection connection) { + handleConnected(connection, listener, uri, ssl, bufferPool, options); + } + }; + IoFuture.Notifier notifier = new IoFuture.Notifier() { + @Override + public void notify(IoFuture ioFuture, Object o) { + if (ioFuture.getStatus() == IoFuture.Status.FAILED) { + listener.failed(ioFuture.getException()); + } + } + }; + if(bindAddress == null) { + worker.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 8009 : uri.getPort()), openListener, options).addNotifier(notifier, null); + } else { + worker.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 8009 : uri.getPort()), openListener, null, options).addNotifier(notifier, null); + } + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress,final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + ChannelListener openListener = new ChannelListener() { + @Override + public void handleEvent(StreamConnection connection) { + handleConnected(connection, listener, uri, ssl, bufferPool, options); + } + }; + IoFuture.Notifier notifier = new IoFuture.Notifier() { + @Override + public void notify(IoFuture ioFuture, Object o) { + if (ioFuture.getStatus() == IoFuture.Status.FAILED) { + listener.failed(ioFuture.getException()); + } + } + }; + if(bindAddress == null) { + ioThread.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 8009 : uri.getPort()), openListener, options).addNotifier(notifier, null); + } else { + ioThread.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 8009 : uri.getPort()), openListener, null, options).addNotifier(notifier, null); + } + } + + private void handleConnected(StreamConnection connection, ClientCallback listener, URI uri, XnioSsl ssl, Pool bufferPool, OptionMap options) { + listener.completed(new AjpClientConnection(new AjpClientChannel(connection, bufferPool) , options, bufferPool)); + } + + +} Index: 3rdParty_sources/undertow/io/undertow/client/http/ClientFixedLengthStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/ClientFixedLengthStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/ClientFixedLengthStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.conduits.AbstractFixedLengthStreamSinkConduit; +import org.xnio.conduits.StreamSinkConduit; + +class ClientFixedLengthStreamSinkConduit extends AbstractFixedLengthStreamSinkConduit { + + private final HttpClientExchange exchange; + + /** + * Construct a new instance. + * + * @param next the next channel + * @param contentLength the content length + * @param configurable {@code true} if this instance should pass configuration to the next + * @param propagateClose {@code true} if this instance should pass close to the next + * @param exchange + */ + public ClientFixedLengthStreamSinkConduit(StreamSinkConduit next, long contentLength, boolean configurable, boolean propagateClose, HttpClientExchange exchange) { + super(next, contentLength, configurable, propagateClose); + this.exchange = exchange; + } + + + + @Override + protected void channelFinished() { + exchange.terminateRequest(); + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http/HttpClientConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/HttpClientConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/HttpClientConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,483 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.UndertowLogger; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.UndertowClientMessages; +import io.undertow.conduits.ChunkedStreamSinkConduit; +import io.undertow.conduits.ChunkedStreamSourceConduit; +import io.undertow.conduits.ConduitListener; +import io.undertow.conduits.FixedLengthStreamSourceConduit; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import io.undertow.util.Protocols; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.PushBackStreamSourceConduit; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.Closeable; +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Locale; + +import static io.undertow.client.UndertowClientMessages.MESSAGES; +import static io.undertow.util.Headers.CLOSE; +import static io.undertow.util.Headers.CONNECTION; +import static io.undertow.util.Headers.CONTENT_LENGTH; +import static io.undertow.util.Headers.TRANSFER_ENCODING; +import static io.undertow.util.Headers.UPGRADE; +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreSet; +import static org.xnio.IoUtils.safeClose; + +/** + * @author David M. Lloyd + */ +class HttpClientConnection extends AbstractAttachable implements Closeable, ClientConnection { + + public final ConduitListener requestFinishListener = new ConduitListener() { + @Override + public void handleEvent(StreamSinkConduit channel) { + currentRequest.terminateRequest(); + } + }; + public final ConduitListener responseFinishedListener = new ConduitListener() { + @Override + public void handleEvent(StreamSourceConduit channel) { + currentRequest.terminateResponse(); + } + }; + + private final Deque pendingQueue = new ArrayDeque<>(); + private HttpClientExchange currentRequest; + private HttpResponseBuilder pendingResponse; + + private final OptionMap options; + private final StreamConnection connection; + private final PushBackStreamSourceConduit pushBackStreamSourceConduit; + private final ClientReadListener clientReadListener = new ClientReadListener(); + + private final Pool bufferPool; + private final StreamSinkConduit originalSinkConduit; + + private static final int UPGRADED = 1 << 28; + private static final int UPGRADE_REQUESTED = 1 << 29; + private static final int CLOSE_REQ = 1 << 30; + private static final int CLOSED = 1 << 31; + private int count = 0; + + private int state; + + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + HttpClientConnection(final StreamConnection connection, final OptionMap options, final Pool bufferPool) { + this.options = options; + this.connection = connection; + this.pushBackStreamSourceConduit = new PushBackStreamSourceConduit(connection.getSourceChannel().getConduit()); + this.connection.getSourceChannel().setConduit(pushBackStreamSourceConduit); + this.bufferPool = bufferPool; + this.originalSinkConduit = connection.getSinkChannel().getConduit(); + + connection.getCloseSetter().set(new ChannelListener() { + + public void handleEvent(StreamConnection channel) { + HttpClientConnection.this.state |= CLOSED; + ChannelListeners.invokeChannelListener(HttpClientConnection.this, closeSetter.get()); + } + }); + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } + + + @Override + public SocketAddress getPeerAddress() { + return connection.getPeerAddress(); + } + + StreamConnection getConnection() { + return connection; + } + + @Override + public A getPeerAddress(Class type) { + return connection.getPeerAddress(type); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public SocketAddress getLocalAddress() { + return connection.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return connection.getLocalAddress(type); + } + + @Override + public XnioWorker getWorker() { + return connection.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return connection.getIoThread(); + } + + @Override + public boolean isOpen() { + return connection.isOpen() && allAreClear(state, CLOSE_REQ | CLOSED); + } + + @Override + public boolean supportsOption(Option option) { + return connection.supportsOption(option); + } + + + @Override + public T getOption(Option option) throws IOException { + return connection.getOption(option); + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return connection.setOption(option, value); + } + + @Override + public boolean isUpgraded() { + return anyAreSet(state, UPGRADE_REQUESTED | UPGRADED); + } + + @Override + public void sendRequest(final ClientRequest request, final ClientCallback clientCallback) { + count++; + if (anyAreSet(state, UPGRADE_REQUESTED | UPGRADED | CLOSE_REQ | CLOSED)) { + clientCallback.failed(UndertowClientMessages.MESSAGES.invalidConnectionState()); + return; + } + final HttpClientExchange httpClientExchange = new HttpClientExchange(clientCallback, request, this); + if (currentRequest == null) { + initiateRequest(httpClientExchange); + } else { + pendingQueue.add(httpClientExchange); + } + } + + private void initiateRequest(HttpClientExchange httpClientExchange) { + currentRequest = httpClientExchange; + pendingResponse = new HttpResponseBuilder(); + ClientRequest request = httpClientExchange.getRequest(); + + String connectionString = request.getRequestHeaders().getFirst(CONNECTION); + if (connectionString != null) { + HttpString connectionHttpString = new HttpString(connectionString); + if (connectionHttpString.equals(CLOSE)) { + state |= CLOSE_REQ; + } else if(connectionHttpString.equals(UPGRADE)) { + state |= UPGRADE_REQUESTED; + } + } else if (request.getProtocol() != Protocols.HTTP_1_1) { + state |= CLOSE_REQ; + } + if (request.getRequestHeaders().contains(UPGRADE)) { + state |= UPGRADE_REQUESTED; + } + + //setup the client request conduits + final ConduitStreamSourceChannel sourceChannel = connection.getSourceChannel(); + sourceChannel.setReadListener(clientReadListener); + sourceChannel.resumeReads(); + + ConduitStreamSinkChannel sinkChannel = connection.getSinkChannel(); + StreamSinkConduit conduit = originalSinkConduit; + conduit = new HttpRequestConduit(conduit, bufferPool, request); + + String fixedLengthString = request.getRequestHeaders().getFirst(CONTENT_LENGTH); + String transferEncodingString = request.getRequestHeaders().getLast(TRANSFER_ENCODING); + + boolean hasContent = true; + + if (fixedLengthString != null) { + try { + long length = Long.parseLong(fixedLengthString); + conduit = new ClientFixedLengthStreamSinkConduit(conduit, length, false, false, currentRequest); + hasContent = length != 0; + } catch (NumberFormatException e) { + handleError(new IOException(e)); + return; + } + } else if (transferEncodingString != null) { + if (!transferEncodingString.toLowerCase(Locale.ENGLISH).contains(Headers.CHUNKED.toString())) { + handleError(UndertowClientMessages.MESSAGES.unknownTransferEncoding(transferEncodingString)); + return; + } + conduit = new ChunkedStreamSinkConduit(conduit, httpClientExchange.getConnection().getBufferPool(), false, false, httpClientExchange.getRequest().getRequestHeaders(), requestFinishListener, httpClientExchange); + } else { + conduit = new ClientFixedLengthStreamSinkConduit(conduit, 0, false, false, currentRequest); + hasContent = false; + } + sinkChannel.setConduit(conduit); + + httpClientExchange.invokeReadReadyCallback(httpClientExchange); + if (!hasContent) { + //if there is no content we flush the response channel. + //otherwise it is up to the user + try { + sinkChannel.shutdownWrites(); + if (!sinkChannel.flush()) { + sinkChannel.setWriteListener(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(ConduitStreamSinkChannel channel, IOException exception) { + handleError(exception); + } + })); + } + } catch (IOException e) { + handleError(e); + } + } + } + + private void handleError(IOException exception) { + currentRequest.setFailed(exception); + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + safeClose(connection); + } + + public StreamConnection performUpgrade() throws IOException { + + // Upgrade the connection + // Set the upgraded flag already to prevent new requests after this one + if (allAreSet(state, UPGRADED | CLOSE_REQ | CLOSED)) { + throw new IOException(UndertowClientMessages.MESSAGES.connectionClosed()); + } + state |= UPGRADED; + return connection; + } + + public void close() throws IOException { + if (anyAreSet(state, CLOSED)) { + return; + } + state |= CLOSED | CLOSE_REQ; + connection.close(); + } + + /** + * Notification that the current request is finished + */ + public void requestDone() { + + connection.getSinkChannel().setConduit(originalSinkConduit); + connection.getSourceChannel().setConduit(pushBackStreamSourceConduit); + connection.getSinkChannel().suspendWrites(); + connection.getSinkChannel().setWriteListener(null); + + if (anyAreSet(state, CLOSE_REQ)) { + currentRequest = null; + this.state |= CLOSED; + safeClose(connection); + } else if (anyAreSet(state, UPGRADE_REQUESTED)) { + connection.getSourceChannel().suspendReads(); + currentRequest = null; + return; + } + currentRequest = null; + + HttpClientExchange next = pendingQueue.poll(); + + if (next == null) { + //we resume reads, so if the target goes away we get notified + connection.getSourceChannel().setReadListener(clientReadListener); + connection.getSourceChannel().resumeReads(); + } else { + initiateRequest(next); + } + } + + class ClientReadListener implements ChannelListener { + + public void handleEvent(StreamSourceChannel channel) { + + HttpResponseBuilder builder = pendingResponse; + final Pooled pooled = bufferPool.allocate(); + final ByteBuffer buffer = pooled.getResource(); + boolean free = true; + + try { + + if (builder == null) { + //read ready when no request pending + buffer.clear(); + try { + int res = channel.read(buffer); + if(res == -1) { + UndertowLogger.CLIENT_LOGGER.debugf("Connection to %s was closed by the target server", connection.getPeerAddress()); + safeClose(HttpClientConnection.this); + } else if(res != 0) { + UndertowLogger.CLIENT_LOGGER.debugf("Target server %s sent unexpected data when no request pending, closing connection", connection.getPeerAddress()); + safeClose(HttpClientConnection.this); + } + //otherwise it is a spurious notification + } catch (IOException e) { + if (UndertowLogger.CLIENT_LOGGER.isDebugEnabled()) { + UndertowLogger.CLIENT_LOGGER.debugf(e, "Connection closed with IOException"); + } + safeClose(connection); + } + return; + } + final ResponseParseState state = builder.getParseState(); + int res; + do { + buffer.clear(); + try { + res = channel.read(buffer); + } catch (IOException e) { + if (UndertowLogger.CLIENT_LOGGER.isDebugEnabled()) { + UndertowLogger.CLIENT_LOGGER.debugf(e, "Connection closed with IOException"); + } + safeClose(channel); + currentRequest.setFailed(new IOException(MESSAGES.connectionClosed())); + return; + } + + if (res == 0) { + if (!channel.isReadResumed()) { + channel.getReadSetter().set(this); + channel.resumeReads(); + } + return; + } else if (res == -1) { + channel.suspendReads(); + safeClose(HttpClientConnection.this); + // Cancel the current active request + currentRequest.setFailed(new IOException(MESSAGES.connectionClosed())); + return; + } + + buffer.flip(); + + HttpResponseParser.INSTANCE.handle(buffer, state, builder); + if (buffer.hasRemaining()) { + free = false; + pushBackStreamSourceConduit.pushBack(pooled); + } + + } while (!state.isComplete()); + + final ClientResponse response = builder.build(); + + String connectionString = response.getResponseHeaders().getFirst(CONNECTION); + + //check if an upgrade worked + if (anyAreSet(HttpClientConnection.this.state, UPGRADE_REQUESTED)) { + if ((connectionString == null || !UPGRADE.equalToString(connectionString)) && !response.getResponseHeaders().contains(UPGRADE)) { + //just unset the upgrade requested flag + HttpClientConnection.this.state &= ~UPGRADE_REQUESTED; + } + } + + if(connectionString != null) { + if (HttpString.tryFromString(connectionString).equals(Headers.CLOSE)) { + HttpClientConnection.this.state |= CLOSE_REQ; + } + } + + if (builder.getStatusCode() == 100) { + pendingResponse = new HttpResponseBuilder(); + currentRequest.setContinueResponse(response); + } else { + prepareResponseChannel(response, currentRequest); + channel.getReadSetter().set(null); + channel.suspendReads(); + pendingResponse = null; + currentRequest.setResponse(response); + } + + + } catch (Exception e) { + UndertowLogger.CLIENT_LOGGER.exceptionProcessingRequest(e); + safeClose(connection); + currentRequest.setFailed(new IOException(e)); + } finally { + if (free) pooled.free(); + } + + + } + } + + private void prepareResponseChannel(ClientResponse response, ClientExchange exchange) { + String encoding = response.getResponseHeaders().getLast(TRANSFER_ENCODING); + boolean chunked = encoding != null && Headers.CHUNKED.equals(new HttpString(encoding)); + String length = response.getResponseHeaders().getFirst(CONTENT_LENGTH); + if (exchange.getRequest().getMethod().equals(Methods.HEAD)) { + connection.getSourceChannel().setConduit(new FixedLengthStreamSourceConduit(connection.getSourceChannel().getConduit(), 0, responseFinishedListener)); + } else if (chunked) { + connection.getSourceChannel().setConduit(new ChunkedStreamSourceConduit(connection.getSourceChannel().getConduit(), pushBackStreamSourceConduit, bufferPool, responseFinishedListener, exchange)); + } else if (length != null) { + try { + long contentLength = Long.parseLong(length); + connection.getSourceChannel().setConduit(new FixedLengthStreamSourceConduit(connection.getSourceChannel().getConduit(), contentLength, responseFinishedListener)); + } catch (NumberFormatException e) { + handleError(new IOException(e)); + throw e; + } + } else if (response.getProtocol().equals(Protocols.HTTP_1_1)) { + connection.getSourceChannel().setConduit(new FixedLengthStreamSourceConduit(connection.getSourceChannel().getConduit(), 0, responseFinishedListener)); + } else { + state |= CLOSE_REQ; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http/HttpClientExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/HttpClientExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/HttpClientExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,182 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.channels.DetachableStreamSinkChannel; +import io.undertow.channels.DetachableStreamSourceChannel; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.ContinueNotification; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.Headers; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; + +import static org.xnio.Bits.anyAreSet; + +/** + * @author Stuart Douglas + */ +class HttpClientExchange extends AbstractAttachable implements ClientExchange { + + private final ClientRequest request; + private final boolean requiresContinue; + private final HttpClientConnection clientConnection; + + private ClientCallback responseCallback; + private ClientCallback readyCallback; + private ContinueNotification continueNotification; + + private ClientResponse response; + private ClientResponse continueResponse; + private IOException failedReason; + + private int state = 0; + private static final int REQUEST_TERMINATED = 1; + private static final int RESPONSE_TERMINATED = 1 << 1; + + public HttpClientExchange(ClientCallback readyCallback, ClientRequest request, HttpClientConnection clientConnection) { + this.readyCallback = readyCallback; + this.request = request; + this.clientConnection = clientConnection; + boolean reqContinue = false; + if (request.getRequestHeaders().contains(Headers.EXPECT)) { + for (String header : request.getRequestHeaders().get(Headers.EXPECT)) { + if (header.equals("100-continue")) { + reqContinue = true; + } + } + } + this.requiresContinue = reqContinue; + } + + void terminateRequest() { + state |= REQUEST_TERMINATED; + if (anyAreSet(state, RESPONSE_TERMINATED)) { + clientConnection.requestDone(); + } + } + + void terminateResponse() { + state |= RESPONSE_TERMINATED; + if (anyAreSet(state, REQUEST_TERMINATED)) { + clientConnection.requestDone(); + } + } + + public boolean isRequiresContinue() { + return requiresContinue; + } + + + void setContinueResponse(ClientResponse response) { + this.continueResponse = response; + if (continueNotification != null) { + this.continueNotification.handleContinue(this); + } + } + + void setResponse(ClientResponse response) { + this.response = response; + if (responseCallback != null) { + this.responseCallback.completed(this); + } + } + + @Override + public void setResponseListener(ClientCallback listener) { + this.responseCallback = listener; + if (listener != null) { + if (failedReason != null) { + listener.failed(failedReason); + } else if (response != null) { + listener.completed(this); + } + } + } + + @Override + public void setContinueHandler(ContinueNotification continueHandler) { + this.continueNotification = continueHandler; + } + + void setFailed(IOException e) { + this.failedReason = e; + if (readyCallback != null) { + readyCallback.failed(e); + readyCallback = null; + } + if (responseCallback != null) { + responseCallback.failed(e); + responseCallback = null; + } + } + + @Override + public StreamSinkChannel getRequestChannel() { + return new DetachableStreamSinkChannel(clientConnection.getConnection().getSinkChannel()) { + @Override + protected boolean isFinished() { + return anyAreSet(state, REQUEST_TERMINATED); + } + }; + } + + @Override + public StreamSourceChannel getResponseChannel() { + return new DetachableStreamSourceChannel(clientConnection.getConnection().getSourceChannel()) { + @Override + protected boolean isFinished() { + return anyAreSet(state, RESPONSE_TERMINATED); + } + }; + } + + @Override + public ClientRequest getRequest() { + return request; + } + + @Override + public ClientResponse getResponse() { + return response; + } + + @Override + public ClientResponse getContinueResponse() { + return continueResponse; + } + + @Override + public ClientConnection getConnection() { + return clientConnection; + } + + void invokeReadReadyCallback(final ClientExchange result) { + if(readyCallback != null) { + readyCallback.completed(result); + readyCallback = null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http/HttpClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/HttpClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/HttpClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,156 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientProvider; +import io.undertow.client.http2.Http2ClientProvider; +import io.undertow.client.spdy.SpdyClientProvider; +import org.xnio.ChannelListener; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.ssl.SslConnection; +import org.xnio.ssl.XnioSsl; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class HttpClientProvider implements ClientProvider { + + @Override + public Set handlesSchemes() { + return new HashSet<>(Arrays.asList(new String[]{"http", "https"})); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, worker, ssl, bufferPool, options); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, ioThread, ssl, bufferPool, options); + } + + @Override + public void connect(ClientCallback listener, InetSocketAddress bindAddress, URI uri, XnioWorker worker, XnioSsl ssl, Pool bufferPool, OptionMap options) { + if (uri.getScheme().equals("https")) { + if (ssl == null) { + listener.failed(UndertowMessages.MESSAGES.sslWasNull()); + return; + } + if (bindAddress == null) { + ssl.openSslConnection(worker, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ssl.openSslConnection(worker, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } + } else { + if (bindAddress == null) { + worker.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 80 : uri.getPort()), createOpenListener(listener, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + worker.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 80 : uri.getPort()), createOpenListener(listener, bufferPool, options), null, options).addNotifier(createNotifier(listener), null); + } + } + } + + @Override + public void connect(ClientCallback listener, InetSocketAddress bindAddress, URI uri, XnioIoThread ioThread, XnioSsl ssl, Pool bufferPool, OptionMap options) { + if (uri.getScheme().equals("https")) { + if (ssl == null) { + listener.failed(UndertowMessages.MESSAGES.sslWasNull()); + return; + } + if (bindAddress == null) { + ssl.openSslConnection(ioThread, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ssl.openSslConnection(ioThread, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } + } else { + if (bindAddress == null) { + ioThread.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 80 : uri.getPort()), createOpenListener(listener, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ioThread.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 80 : uri.getPort()), createOpenListener(listener, bufferPool, options), null, options).addNotifier(createNotifier(listener), null); + } + } + } + + private IoFuture.Notifier createNotifier(final ClientCallback listener) { + return new IoFuture.Notifier() { + @Override + public void notify(IoFuture ioFuture, Object o) { + if (ioFuture.getStatus() == IoFuture.Status.FAILED) { + listener.failed(ioFuture.getException()); + } + } + }; + } + + private ChannelListener createOpenListener(final ClientCallback listener, final Pool bufferPool, final OptionMap options) { + return new ChannelListener() { + @Override + public void handleEvent(StreamConnection connection) { + handleConnected(connection, listener, bufferPool, options); + } + }; + } + + + private void handleConnected(final StreamConnection connection, final ClientCallback listener, final Pool bufferPool, final OptionMap options) { + if (options.get(UndertowOptions.ENABLE_SPDY, false) && connection instanceof SslConnection && SpdyClientProvider.isEnabled()) { + try { + SpdyClientProvider.handlePotentialSpdyConnection(connection, listener, bufferPool, options, new ChannelListener() { + @Override + public void handleEvent(SslConnection channel) { + listener.completed(new HttpClientConnection(connection, options, bufferPool)); + } + }); + } catch (Exception e) { + listener.failed(new IOException(e)); + } + } else if (options.get(UndertowOptions.ENABLE_HTTP2, false) && connection instanceof SslConnection && Http2ClientProvider.isEnabled()) { + try { + Http2ClientProvider.handlePotentialHttp2Connection(connection, listener, bufferPool, options, new ChannelListener() { + @Override + public void handleEvent(SslConnection channel) { + listener.completed(new HttpClientConnection(connection, options, bufferPool)); + } + }); + } catch (Exception e) { + listener.failed(new IOException(e)); + } + } else { + listener.completed(new HttpClientConnection(connection, options, bufferPool)); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http/HttpRequestConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/HttpRequestConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/HttpRequestConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,622 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.client.ClientRequest; +import io.undertow.server.TruncatedResponseException; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import org.jboss.logging.Logger; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.Iterator; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; + +/** + * @author David M. Lloyd + * @author Emanuel Muckenhuber + */ +final class HttpRequestConduit extends AbstractStreamSinkConduit { + + private static final Logger log = Logger.getLogger("io.undertow.client.request"); + + private final Pool pool; + + private int state = STATE_START; + + private Iterator nameIterator; + private String string; + private HttpString headerName; + private Iterator valueIterator; + private int charIndex; + private Pooled pooledBuffer; + private final ClientRequest request; + + private static final int STATE_BODY = 0; // Message body, normal pass-through operation + private static final int STATE_START = 1; // No headers written yet + private static final int STATE_HDR_NAME = 2; // Header name indexed by charIndex + private static final int STATE_HDR_D = 3; // Header delimiter ':' + private static final int STATE_HDR_DS = 4; // Header delimiter ': ' + private static final int STATE_HDR_VAL = 5; // Header value + private static final int STATE_HDR_EOL_CR = 6; // Header line CR + private static final int STATE_HDR_EOL_LF = 7; // Header line LF + private static final int STATE_HDR_FINAL_CR = 8; // Final CR + private static final int STATE_HDR_FINAL_LF = 9; // Final LF + private static final int STATE_BUF_FLUSH = 10; // flush the buffer and go to writing body + + private static final int MASK_STATE = 0x0000000F; + private static final int FLAG_SHUTDOWN = 0x00000010; + + HttpRequestConduit(final StreamSinkConduit next, final Pool pool, final ClientRequest request) { + super(next); + this.pool = pool; + this.request = request; + } + + /** + * Handles writing out the header data. It can also take a byte buffer of user + * data, to enable both user data and headers to be written out in a single operation, + * which has a noticeable performance impact. + * + * It is up to the caller to note the current position of this buffer before and after they + * call this method, and use this to figure out how many bytes (if any) have been written. + * @param state + * @param userData + * @return + * @throws java.io.IOException + */ + private int processWrite(int state, final ByteBuffer userData) throws IOException { + if (state == STATE_START) { + pooledBuffer = pool.allocate(); + } + ClientRequest request = this.request; + ByteBuffer buffer = pooledBuffer.getResource(); + Iterator nameIterator = this.nameIterator; + Iterator valueIterator = this.valueIterator; + int charIndex = this.charIndex; + int length; + String string = this.string; + HttpString headerName = this.headerName; + int res; + // BUFFER IS FLIPPED COMING IN + if (state != STATE_START && buffer.hasRemaining()) { + log.trace("Flushing remaining buffer"); + do { + res = next.write(buffer); + if (res == 0) { + return state; + } + } while (buffer.hasRemaining()); + } + buffer.clear(); + // BUFFER IS NOW EMPTY FOR FILLING + for (;;) { + switch (state) { + case STATE_BODY: { + // shouldn't be possible, but might as well do the right thing anyway + return state; + } + case STATE_START: { + log.trace("Starting request"); + // we assume that our buffer has enough space for the initial request line plus one more CR+LF + assert buffer.remaining() >= 0x100; + request.getMethod().appendTo(buffer); + buffer.put((byte) ' '); + string = request.getPath(); + length = string.length(); + for (charIndex = 0; charIndex < length; charIndex ++) { + buffer.put((byte) string.charAt(charIndex)); + } + buffer.put((byte) ' '); + request.getProtocol().appendTo(buffer); + buffer.put((byte) '\r').put((byte) '\n'); + HeaderMap headers = request.getRequestHeaders(); + nameIterator = headers.getHeaderNames().iterator(); + if (! nameIterator.hasNext()) { + log.trace("No request headers"); + buffer.put((byte) '\r').put((byte) '\n'); + buffer.flip(); + while (buffer.hasRemaining()) { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_BUF_FLUSH; + } + } + pooledBuffer.free(); + pooledBuffer = null; + log.trace("Body"); + return STATE_BODY; + } + headerName = nameIterator.next(); + charIndex = 0; + // fall thru + } + case STATE_HDR_NAME: { + log.tracef("Processing header '%s'", headerName); + length = headerName.length(); + while (charIndex < length) { + if (buffer.hasRemaining()) { + buffer.put(headerName.byteAt(charIndex++)); + } else { + log.trace("Buffer flush"); + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerName = headerName; + this.charIndex = charIndex; + this.valueIterator = valueIterator; + this.nameIterator = nameIterator; + log.trace("Continuation"); + return STATE_HDR_NAME; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + } + // fall thru + } + case STATE_HDR_D: { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + this.string = string; + this.headerName = headerName; + this.charIndex = charIndex; + this.valueIterator = valueIterator; + this.nameIterator = nameIterator; + return STATE_HDR_D; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) ':'); + // fall thru + } + case STATE_HDR_DS: { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + this.string = string; + this.headerName = headerName; + this.charIndex = charIndex; + this.valueIterator = valueIterator; + this.nameIterator = nameIterator; + return STATE_HDR_DS; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) ' '); + if(valueIterator == null) { + valueIterator = request.getRequestHeaders().get(headerName).iterator(); + } + assert valueIterator.hasNext(); + string = valueIterator.next(); + charIndex = 0; + // fall thru + } + case STATE_HDR_VAL: { + log.tracef("Processing header value '%s'", string); + length = string.length(); + while (charIndex < length) { + if (buffer.hasRemaining()) { + buffer.put((byte) string.charAt(charIndex++)); + } else { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerName = headerName; + this.charIndex = charIndex; + this.valueIterator = valueIterator; + this.nameIterator = nameIterator; + log.trace("Continuation"); + return STATE_HDR_VAL; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + } + charIndex = 0; + if (! valueIterator.hasNext()) { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_EOL_CR; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 13); // CR + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_EOL_LF; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 10); // LF + if (nameIterator.hasNext()) { + headerName = nameIterator.next(); + valueIterator = null; + state = STATE_HDR_NAME; + break; + } else { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_FINAL_CR; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 13); // CR + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_FINAL_LF; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 10); // LF + this.nameIterator = null; + this.valueIterator = null; + this.string = null; + buffer.flip(); + //for performance reasons we use a gather write if there is user data + if(userData == null) { + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } else { + ByteBuffer[] b = {buffer, userData}; + do { + long r = next.write(b, 0, b.length); + if (r == 0 && buffer.hasRemaining()) { + log.trace("Continuation"); + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } + pooledBuffer.free(); + pooledBuffer = null; + log.trace("Body"); + return STATE_BODY; + } + // not reached + } + // fall thru + } + // Clean-up states + case STATE_HDR_EOL_CR: { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_EOL_CR; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 13); // CR + } + case STATE_HDR_EOL_LF: { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_EOL_LF; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 10); // LF + if(valueIterator.hasNext()) { + state = STATE_HDR_NAME; + break; + } else if (nameIterator.hasNext()) { + headerName = nameIterator.next(); + valueIterator = null; + state = STATE_HDR_NAME; + break; + } + // fall thru + } + case STATE_HDR_FINAL_CR: { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_FINAL_CR; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 13); // CR + // fall thru + } + case STATE_HDR_FINAL_LF: { + if (! buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_HDR_FINAL_LF; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) 10); // LF + this.nameIterator = null; + this.valueIterator = null; + this.string = null; + buffer.flip(); + //for performance reasons we use a gather write if there is user data + if(userData == null) { + do { + res = next.write(buffer); + if (res == 0) { + log.trace("Continuation"); + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } else { + ByteBuffer[] b = {buffer, userData}; + do { + long r = next.write(b, 0, b.length); + if (r == 0 && buffer.hasRemaining()) { + log.trace("Continuation"); + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } + // fall thru + } + case STATE_BUF_FLUSH: { + // buffer was successfully flushed above + pooledBuffer.free(); + pooledBuffer = null; + return STATE_BODY; + } + default: { + throw new IllegalStateException(); + } + } + } + } + + public int write(final ByteBuffer src) throws IOException { + log.trace("write"); + int oldState = this.state; + int state = oldState & MASK_STATE; + int alreadyWritten = 0; + int originalRemaining = - 1; + try { + if (state != 0) { + originalRemaining = src.remaining(); + state = processWrite(state, src); + if (state != 0) { + return 0; + } + alreadyWritten = originalRemaining - src.remaining(); + if (allAreSet(oldState, FLAG_SHUTDOWN)) { + next.terminateWrites(); + throw new ClosedChannelException(); + } + } + if(alreadyWritten != originalRemaining) { + return next.write(src) + alreadyWritten; + } + return alreadyWritten; + } finally { + this.state = oldState & ~MASK_STATE | state; + } + } + + public long write(final ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + log.trace("write"); + if (length == 0) { + return 0L; + } + int oldVal = state; + int state = oldVal & MASK_STATE; + try { + if (state != 0) { + //todo: use gathering write here + state = processWrite(state, null); + if (state != 0) { + return 0; + } + if (allAreSet(oldVal, FLAG_SHUTDOWN)) { + next.terminateWrites(); + throw new ClosedChannelException(); + } + } + return length == 1 ? next.write(srcs[offset]) : next.write(srcs, offset, length); + } finally { + this.state = oldVal & ~MASK_STATE | state; + } + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + log.trace("transfer"); + if (count == 0L) { + return 0L; + } + int oldVal = state; + int state = oldVal & MASK_STATE; + try { + if (state != 0) { + state = processWrite(state, null); + if (state != 0) { + return 0; + } + if (allAreSet(oldVal, FLAG_SHUTDOWN)) { + next.terminateWrites(); + throw new ClosedChannelException(); + } + } + return next.transferFrom(src, position, count); + } finally { + this.state = oldVal & ~MASK_STATE | state; + } + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + log.trace("transfer"); + if (count == 0) { + throughBuffer.clear().limit(0); + return 0L; + } + int oldVal = state; + int state = oldVal & MASK_STATE; + try { + if (state != 0) { + state = processWrite(state, null); + if (state != 0) { + return 0; + } + if (allAreSet(oldVal, FLAG_SHUTDOWN)) { + next.terminateWrites(); + throw new ClosedChannelException(); + } + } + return next.transferFrom(source, count, throughBuffer); + } finally { + this.state = oldVal & ~MASK_STATE | state; + } + } + + public boolean flush() throws IOException { + log.trace("flush"); + int oldVal = state; + int state = oldVal & MASK_STATE; + try { + if (state != 0) { + state = processWrite(state, null); + if (state != 0) { + log.trace("Flush false because headers aren't written yet"); + return false; + } + if (allAreSet(oldVal, FLAG_SHUTDOWN)) { + next.terminateWrites(); + // fall out to the flush + } + } + log.trace("Delegating flush"); + return next.flush(); + } finally { + this.state = oldVal & ~MASK_STATE | state; + } + } + + + public void terminateWrites() throws IOException { + log.trace("shutdown"); + int oldVal = this.state; + if (allAreClear(oldVal, MASK_STATE)) { + next.terminateWrites(); + return; + } + this.state = oldVal | FLAG_SHUTDOWN; + } + + public void truncateWrites() throws IOException { + log.trace("close"); + int oldVal = this.state; + if (allAreClear(oldVal, MASK_STATE)) { + try { + next.truncateWrites(); + } finally { + if (pooledBuffer != null) { + pooledBuffer.free(); + pooledBuffer = null; + } + } + return; + } + this.state = oldVal & ~MASK_STATE | FLAG_SHUTDOWN | STATE_BODY; + throw new TruncatedResponseException(); + } + + public XnioWorker getWorker() { + return next.getWorker(); + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http/HttpResponseBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/HttpResponseBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/HttpResponseBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,76 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.client.ClientResponse; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; + +/** + * A pending http request. + * + * @author Emanuel Muckenhuber + */ +final class HttpResponseBuilder { + + private final ResponseParseState parseState = new ResponseParseState(); + + private int statusCode; + private HttpString protocol; + private String reasonPhrase; + private final HeaderMap responseHeaders = new HeaderMap(); + + public ResponseParseState getParseState() { + return parseState; + } + + HeaderMap getResponseHeaders() { + return responseHeaders; + } + + int getStatusCode() { + return statusCode; + } + + void setStatusCode(final int statusCode) { + this.statusCode = statusCode; + } + + String getReasonPhrase() { + return reasonPhrase; + } + + void setReasonPhrase(final String reasonPhrase) { + this.reasonPhrase = reasonPhrase; + } + + HttpString getProtocol() { + return protocol; + } + + @SuppressWarnings("unused") + void setProtocol(final HttpString protocol) { + this.protocol = protocol; + } + + public ClientResponse build() { + return new ClientResponse(statusCode, reasonPhrase, protocol, responseHeaders); + } + +} Index: 3rdParty_sources/undertow/io/undertow/client/http/HttpResponseParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/HttpResponseParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/HttpResponseParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,374 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.annotationprocessor.HttpResponseParserConfig; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import io.undertow.util.Protocols; + +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import static io.undertow.util.Headers.ACCEPT_RANGES_STRING; +import static io.undertow.util.Headers.AGE_STRING; +import static io.undertow.util.Headers.CACHE_CONTROL_STRING; +import static io.undertow.util.Headers.CONNECTION_STRING; +import static io.undertow.util.Headers.CONTENT_DISPOSITION_STRING; +import static io.undertow.util.Headers.CONTENT_ENCODING_STRING; +import static io.undertow.util.Headers.CONTENT_LANGUAGE_STRING; +import static io.undertow.util.Headers.CONTENT_LENGTH_STRING; +import static io.undertow.util.Headers.CONTENT_LOCATION_STRING; +import static io.undertow.util.Headers.CONTENT_MD5_STRING; +import static io.undertow.util.Headers.CONTENT_RANGE_STRING; +import static io.undertow.util.Headers.CONTENT_TYPE_STRING; +import static io.undertow.util.Headers.DATE_STRING; +import static io.undertow.util.Headers.ETAG_STRING; +import static io.undertow.util.Headers.EXPIRES_STRING; +import static io.undertow.util.Headers.LAST_MODIFIED_STRING; +import static io.undertow.util.Headers.LOCATION_STRING; +import static io.undertow.util.Headers.PRAGMA_STRING; +import static io.undertow.util.Headers.PROXY_AUTHENTICATE_STRING; +import static io.undertow.util.Headers.REFRESH_STRING; +import static io.undertow.util.Headers.RETRY_AFTER_STRING; +import static io.undertow.util.Headers.SERVER_STRING; +import static io.undertow.util.Headers.SET_COOKIE2_STRING; +import static io.undertow.util.Headers.SET_COOKIE_STRING; +import static io.undertow.util.Headers.STRICT_TRANSPORT_SECURITY_STRING; +import static io.undertow.util.Headers.TRAILER_STRING; +import static io.undertow.util.Headers.TRANSFER_ENCODING_STRING; +import static io.undertow.util.Headers.VARY_STRING; +import static io.undertow.util.Headers.VIA_STRING; +import static io.undertow.util.Headers.WARNING_STRING; +import static io.undertow.util.Headers.WWW_AUTHENTICATE_STRING; +import static io.undertow.util.Protocols.HTTP_0_9_STRING; +import static io.undertow.util.Protocols.HTTP_1_0_STRING; +import static io.undertow.util.Protocols.HTTP_1_1_STRING; + +/** + * @author Emanuel Muckenhuber + */ +@HttpResponseParserConfig( + protocols = { + HTTP_0_9_STRING, HTTP_1_0_STRING, HTTP_1_1_STRING + }, + headers = { + ACCEPT_RANGES_STRING, + AGE_STRING, + CACHE_CONTROL_STRING, + CONNECTION_STRING, + CONTENT_DISPOSITION_STRING, + CONTENT_ENCODING_STRING, + CONTENT_LANGUAGE_STRING, + CONTENT_LENGTH_STRING, + CONTENT_LOCATION_STRING, + CONTENT_MD5_STRING, + CONTENT_RANGE_STRING, + CONTENT_TYPE_STRING, + DATE_STRING, + EXPIRES_STRING, + ETAG_STRING, + LAST_MODIFIED_STRING, + LOCATION_STRING, + PRAGMA_STRING, + PROXY_AUTHENTICATE_STRING, + REFRESH_STRING, + RETRY_AFTER_STRING, + SERVER_STRING, + SET_COOKIE_STRING, + SET_COOKIE2_STRING, + STRICT_TRANSPORT_SECURITY_STRING, + TRAILER_STRING, + TRANSFER_ENCODING_STRING, + VARY_STRING, + VIA_STRING, + WARNING_STRING, + WWW_AUTHENTICATE_STRING + }) +abstract class HttpResponseParser { + + public static final HttpResponseParser INSTANCE; + + static { + try { + final Class cls = HttpResponseParser.class.getClassLoader().loadClass(HttpResponseParser.class.getName() + "$$generated"); + INSTANCE = (HttpResponseParser) cls.newInstance(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + abstract void handleHttpVersion(ByteBuffer buffer, ResponseParseState currentState, HttpResponseBuilder builder); + + abstract void handleHeader(ByteBuffer buffer, ResponseParseState currentState, HttpResponseBuilder builder); + + public void handle(final ByteBuffer buffer, final ResponseParseState currentState, final HttpResponseBuilder builder) { + + if (currentState.state == ResponseParseState.VERSION) { + handleHttpVersion(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + if (currentState.state == ResponseParseState.STATUS_CODE) { + handleStatusCode(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + if (currentState.state == ResponseParseState.REASON_PHRASE) { + handleReasonPhrase(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + if (currentState.state == ResponseParseState.AFTER_REASON_PHRASE) { + handleAfterReasonPhrase(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + while (currentState.state != ResponseParseState.PARSE_COMPLETE) { + if (currentState.state == ResponseParseState.HEADER) { + handleHeader(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + if (currentState.state == ResponseParseState.HEADER_VALUE) { + handleHeaderValue(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + } + } + + /** + * Parses the status code. This is called from the generated bytecode. + * + * @param buffer The buffer + * @param state The current state + * @param builder The exchange builder + * @return The number of bytes remaining + */ + @SuppressWarnings("unused") + final void handleStatusCode(ByteBuffer buffer, ResponseParseState state, HttpResponseBuilder builder) { + StringBuilder stringBuilder = state.stringBuilder; + while (buffer.hasRemaining()) { + final char next = (char) buffer.get(); + if (next == ' ' || next == '\t') { + builder.setStatusCode(Integer.parseInt(stringBuilder.toString())); + state.state = ResponseParseState.REASON_PHRASE; + state.stringBuilder.setLength(0); + state.parseState = 0; + state.pos = 0; + state.nextHeader = null; + return; + } else { + stringBuilder.append(next); + } + } + } + + /** + * Parses the reason phrase. This is called from the generated bytecode. + * + * @param buffer The buffer + * @param state The current state + * @param builder The exchange builder + * @return The number of bytes remaining + */ + @SuppressWarnings("unused") + final void handleReasonPhrase(ByteBuffer buffer, ResponseParseState state, HttpResponseBuilder builder) { + StringBuilder stringBuilder = state.stringBuilder; + while (buffer.hasRemaining()) { + final char next = (char) buffer.get(); + if (next == '\n' || next == '\r') { + builder.setReasonPhrase(stringBuilder.toString()); + state.state = ResponseParseState.AFTER_REASON_PHRASE; + state.stringBuilder.setLength(0); + state.parseState = 0; + state.leftOver = (byte) next; + state.pos = 0; + state.nextHeader = null; + return; + } else { + stringBuilder.append(next); + } + } + } + + /** + * The parse states for parsing heading values + */ + private static final int NORMAL = 0; + private static final int WHITESPACE = 1; + private static final int BEGIN_LINE_END = 2; + private static final int LINE_END = 3; + private static final int AWAIT_DATA_END = 4; + + /** + * Parses a header value. This is called from the generated bytecode. + * + * @param buffer The buffer + * @param state The current state + * @param builder The exchange builder + * @return The number of bytes remaining + */ + @SuppressWarnings("unused") + final void handleHeaderValue(ByteBuffer buffer, ResponseParseState state, HttpResponseBuilder builder) { + StringBuilder stringBuilder = state.stringBuilder; + if (stringBuilder == null) { + stringBuilder = new StringBuilder(); + state.parseState = 0; + } + + int parseState = state.parseState; + while (buffer.hasRemaining()) { + final byte next = buffer.get(); + switch (parseState) { + case NORMAL: { + if (next == '\r') { + parseState = BEGIN_LINE_END; + } else if (next == '\n') { + parseState = LINE_END; + } else if (next == ' ' || next == '\t') { + parseState = WHITESPACE; + } else { + stringBuilder.append((char) next); + } + break; + } + case WHITESPACE: { + if (next == '\r') { + parseState = BEGIN_LINE_END; + } else if (next == '\n') { + parseState = LINE_END; + } else if (next == ' ' || next == '\t') { + } else { + if (stringBuilder.length() > 0) { + stringBuilder.append(' '); + } + stringBuilder.append((char) next); + parseState = NORMAL; + } + break; + } + case LINE_END: + case BEGIN_LINE_END: { + if (next == '\n' && parseState == BEGIN_LINE_END) { + parseState = LINE_END; + } else if (next == '\t' || + next == ' ') { + //this is a continuation + parseState = WHITESPACE; + } else { + //we have a header + HttpString nextStandardHeader = state.nextHeader; + String headerValue = stringBuilder.toString(); + + //TODO: we need to decode this according to RFC-2047 if we have seen a =? symbol + builder.getResponseHeaders().add(nextStandardHeader, headerValue); + + state.nextHeader = null; + + state.leftOver = next; + state.stringBuilder.setLength(0); + if (next == '\r') { + parseState = AWAIT_DATA_END; + } else { + state.state = ResponseParseState.HEADER; + state.parseState = 0; + return; + } + } + break; + } + case AWAIT_DATA_END: { + state.state = ResponseParseState.PARSE_COMPLETE; + return; + } + } + } + //we only write to the state if we did not finish parsing + state.parseState = parseState; + } + + protected void handleAfterReasonPhrase(ByteBuffer buffer, ResponseParseState state, HttpResponseBuilder builder) { + boolean newLine = state.leftOver == '\n'; + while (buffer.hasRemaining()) { + final byte next = buffer.get(); + if (newLine) { + if (next == '\n') { + state.state = ResponseParseState.PARSE_COMPLETE; + return; + } else { + state.state = ResponseParseState.HEADER; + state.leftOver = next; + return; + } + } else { + if (next == '\n') { + newLine = true; + } else if (next != '\r' && next != ' ' && next != '\t') { + state.state = ResponseParseState.HEADER; + state.leftOver = next; + return; + } + } + } + if (newLine) { + state.leftOver = '\n'; + } + } + + /** + * This is a bit of hack to enable the parser to get access to the HttpString's that are sorted + * in the static fields of the relevant classes. This means that in most cases a HttpString comparison + * will take the fast path == route, as they will be the same object + * + * @return + */ + protected static Map httpStrings() { + final Map results = new HashMap<>(); + final Class[] classs = {Headers.class, Methods.class, Protocols.class}; + + for (Class c : classs) { + for (Field field : c.getDeclaredFields()) { + if (field.getType().equals(HttpString.class)) { + field.setAccessible(true); + HttpString result = null; + try { + result = (HttpString) field.get(null); + results.put(result.toString(), result); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + return results; + + } + +} Index: 3rdParty_sources/undertow/io/undertow/client/http/ResponseParseState.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http/ResponseParseState.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http/ResponseParseState.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,96 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http; + +import io.undertow.util.HttpString; + +/** + * @author Emanuel Muckenhuber + */ +class ResponseParseState { + + //parsing states + public static final int VERSION = 0; + public static final int STATUS_CODE = 1; + public static final int REASON_PHRASE = 2; + public static final int AFTER_REASON_PHRASE = 3; + public static final int HEADER = 4; + public static final int HEADER_VALUE = 5; + public static final int PARSE_COMPLETE = 6; + + /** + * The actual state of request parsing + */ + int state; + + /** + * The current state in the tokenizer state machine. + */ + int parseState; + + /** + * If this state is a prefix or terminal match state this is set to the string + * that is a candidate to be matched + */ + HttpString current; + + /** + * The bytes version of {@link #current} + */ + byte[] currentBytes; + + /** + * If this state is a prefix match state then this holds the current position in the string. + */ + int pos; + + /** + * If this is in {@link #NO_STATE} then this holds the current token that has been read so far. + */ + final StringBuilder stringBuilder = new StringBuilder(); + + /** + * This has different meanings depending on the current state. + *

+ * In state {@link #HEADER} it is a the first character of the header, that was read by + * {@link #HEADER_VALUE} to see if this was a continuation. + *

+ * In state {@link #HEADER_VALUE} if represents the last character that was seen. + */ + byte leftOver; + + /** + * This is used to store the next header value when parsing header key / value pairs, + */ + HttpString nextHeader; + + public ResponseParseState() { + this.parseState = 0; + this.pos = 0; + } + + public boolean isComplete() { + return state == PARSE_COMPLETE; + } + + public final void parseComplete() { + state = PARSE_COMPLETE; + } +} + Index: 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClearClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClearClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClearClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,199 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http2; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.xnio.ChannelListener; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.BoundChannel; +import org.xnio.http.HttpUpgrade; +import org.xnio.ssl.XnioSsl; + +import io.undertow.UndertowOptions; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientProvider; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.protocols.http2.Http2Setting; +import io.undertow.util.FlexBase64; +import io.undertow.util.Headers; + +/** + * HTTP2 client provider that uses HTTP upgrade rather than ALPN + * + * @author Stuart Douglas + */ +public class Http2ClearClientProvider implements ClientProvider { + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, worker, ssl, bufferPool, options); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, ioThread, ssl, bufferPool, options); + } + + @Override + public Set handlesSchemes() { + return new HashSet<>(Arrays.asList(new String[]{"h2c"})); + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + Map headers = createHeaders(options, bufferPool, uri); + HttpUpgrade.performUpgrade(worker, bindAddress, uri, headers, new Http2ClearOpenListener(bufferPool, options, listener), null, options, null).addNotifier(new FailedNotifier(listener), null); + } + + @Override + public void connect(final ClientCallback listener, final InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + + if (bindAddress != null) { + ioThread.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort()), new ChannelListener() { + @Override + public void handleEvent(StreamConnection channel) { + Map headers = createHeaders(options, bufferPool, uri); + HttpUpgrade.performUpgrade(channel, uri, headers, new Http2ClearOpenListener(bufferPool, options, listener), null).addNotifier(new FailedNotifier(listener), null); + } + }, new ChannelListener() { + @Override + public void handleEvent(BoundChannel channel) { + + } + }, options).addNotifier(new FailedNotifier(listener), null); + } else { + ioThread.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort()), new ChannelListener() { + @Override + public void handleEvent(StreamConnection channel) { + Map headers = createHeaders(options, bufferPool, uri); + HttpUpgrade.performUpgrade(channel, uri, headers, new Http2ClearOpenListener(bufferPool, options, listener), null).addNotifier(new FailedNotifier(listener), null); + } + }, new ChannelListener() { + @Override + public void handleEvent(BoundChannel channel) { + + } + }, options).addNotifier(new FailedNotifier(listener), null); + } + + } + + private Map createHeaders(OptionMap options, Pool bufferPool, URI uri) { + Map headers = new HashMap<>(); + headers.put("HTTP2-Settings", createSettingsFrame(options, bufferPool)); + headers.put(Headers.UPGRADE_STRING, Http2Channel.CLEARTEXT_UPGRADE_STRING); + headers.put(Headers.CONNECTION_STRING, "Upgrade, HTTP2-Settings"); + headers.put(Headers.HOST_STRING, uri.getHost()); + headers.put("X-HTTP2-connect-only", "connect"); //undertow specific header that tells the remote server that this request should + return headers; + } + + + private String createSettingsFrame(OptionMap options, Pool bufferPool) { + Pooled b = bufferPool.allocate(); + try { + ByteBuffer currentBuffer = b.getResource(); + + if (options.contains(UndertowOptions.HTTP2_SETTINGS_HEADER_TABLE_SIZE)) { + pushOption(currentBuffer, Http2Setting.SETTINGS_HEADER_TABLE_SIZE, options.get(UndertowOptions.HTTP2_SETTINGS_HEADER_TABLE_SIZE)); + } + if (options.contains(UndertowOptions.HTTP2_SETTINGS_ENABLE_PUSH)) { + pushOption(currentBuffer, Http2Setting.SETTINGS_ENABLE_PUSH, options.get(UndertowOptions.HTTP2_SETTINGS_ENABLE_PUSH) ? 1 : 0); + } + + if (options.contains(UndertowOptions.HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS)) { + pushOption(currentBuffer, Http2Setting.SETTINGS_MAX_CONCURRENT_STREAMS, options.get(UndertowOptions.HTTP2_SETTINGS_MAX_CONCURRENT_STREAMS)); + } + + if (options.contains(UndertowOptions.HTTP2_SETTINGS_INITIAL_WINDOW_SIZE)) { + pushOption(currentBuffer, Http2Setting.SETTINGS_INITIAL_WINDOW_SIZE, options.get(UndertowOptions.HTTP2_SETTINGS_INITIAL_WINDOW_SIZE)); + } + + if (options.contains(UndertowOptions.HTTP2_SETTINGS_MAX_FRAME_SIZE)) { + pushOption(currentBuffer, Http2Setting.SETTINGS_MAX_FRAME_SIZE, options.get(UndertowOptions.HTTP2_SETTINGS_MAX_FRAME_SIZE)); + } + + if (options.contains(UndertowOptions.HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE)) { + pushOption(currentBuffer, Http2Setting.SETTINGS_MAX_HEADER_LIST_SIZE, options.get(UndertowOptions.HTTP2_SETTINGS_MAX_HEADER_LIST_SIZE)); + } + currentBuffer.flip(); + return FlexBase64.encodeString(currentBuffer, false); + } finally { + b.free(); + } + } + + private static void pushOption(ByteBuffer currentBuffer, int id, int value) { + currentBuffer.put((byte) ((id >> 8) & 0xFF)); + currentBuffer.put((byte) (id & 0xFF)); + currentBuffer.put((byte) ((value >> 24) & 0xFF)); + currentBuffer.put((byte) ((value >> 16) & 0xFF)); + currentBuffer.put((byte) ((value >> 8) & 0xFF)); + currentBuffer.put((byte) (value & 0xFF)); + } + + private static class Http2ClearOpenListener implements ChannelListener { + private final Pool bufferPool; + private final OptionMap options; + private final ClientCallback listener; + + public Http2ClearOpenListener(Pool bufferPool, OptionMap options, ClientCallback listener) { + this.bufferPool = bufferPool; + this.options = options; + this.listener = listener; + } + + @Override + public void handleEvent(StreamConnection channel) { + Http2Channel http2Channel = new Http2Channel(channel, bufferPool, null, true, true, options); + Http2ClientConnection http2ClientConnection = new Http2ClientConnection(http2Channel, true); + + listener.completed(http2ClientConnection); + } + } + + private static class FailedNotifier implements IoFuture.Notifier { + private final ClientCallback listener; + + public FailedNotifier(ClientCallback listener) { + this.listener = listener; + } + + @Override + public void notify(IoFuture ioFuture, Object attachment) { + if (ioFuture.getStatus() == IoFuture.Status.FAILED) { + listener.failed(ioFuture.getException()); + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,345 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http2; + +import static io.undertow.util.Headers.CONTENT_LENGTH; +import static io.undertow.util.Headers.TRANSFER_ENCODING; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.Channels; +import org.xnio.channels.StreamSinkChannel; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ProxiedRequestAttachments; +import io.undertow.protocols.http2.AbstractHttp2StreamSourceChannel; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.protocols.http2.Http2HeadersStreamSinkChannel; +import io.undertow.protocols.http2.Http2PingStreamSourceChannel; +import io.undertow.protocols.http2.Http2RstStreamStreamSourceChannel; +import io.undertow.protocols.http2.Http2StreamSourceChannel; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +/** + * @author Stuart Douglas + */ +public class Http2ClientConnection implements ClientConnection { + + + static final HttpString METHOD = new HttpString(":method"); + static final HttpString PATH = new HttpString(":path"); + static final HttpString SCHEME = new HttpString(":scheme"); + static final HttpString VERSION = new HttpString(":version"); + static final HttpString HOST = new HttpString(":host"); + static final HttpString STATUS = new HttpString(":status"); + + private final Http2Channel http2Channel; + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + private final Map currentExchanges = new ConcurrentHashMap<>(); + + private boolean initialUpgradeRequest; + + public Http2ClientConnection(Http2Channel http2Channel, boolean initialUpgradeRequest) { + this.http2Channel = http2Channel; + http2Channel.getReceiveSetter().set(new Http2ReceiveListener()); + http2Channel.resumeReceives(); + http2Channel.addCloseTask(new ChannelListener() { + @Override + public void handleEvent(Http2Channel channel) { + ChannelListeners.invokeChannelListener(Http2ClientConnection.this, closeSetter.get()); + } + }); + this.initialUpgradeRequest = initialUpgradeRequest; + } + + @Override + public void sendRequest(ClientRequest request, ClientCallback clientCallback) { + request.getRequestHeaders().put(PATH, request.getPath()); + request.getRequestHeaders().put(SCHEME, "https"); + request.getRequestHeaders().put(VERSION, request.getProtocol().toString()); + request.getRequestHeaders().put(METHOD, request.getMethod().toString()); + request.getRequestHeaders().put(HOST, request.getRequestHeaders().getFirst(Headers.HOST)); + request.getRequestHeaders().remove(Headers.HOST); + + + boolean hasContent = true; + + String fixedLengthString = request.getRequestHeaders().getFirst(CONTENT_LENGTH); + String transferEncodingString = request.getRequestHeaders().getLast(TRANSFER_ENCODING); + if (fixedLengthString != null) { + try { + long length = Long.parseLong(fixedLengthString); + hasContent = length != 0; + } catch (NumberFormatException e) { + handleError(new IOException(e)); + return; + } + } else if (transferEncodingString == null) { + hasContent = false; + } + + request.getRequestHeaders().remove(Headers.CONNECTION); + request.getRequestHeaders().remove(Headers.KEEP_ALIVE); + request.getRequestHeaders().remove(Headers.TRANSFER_ENCODING); + + //setup the X-Forwarded-* headers + String peer = request.getAttachment(ProxiedRequestAttachments.REMOTE_HOST); + if(peer != null) { + request.getRequestHeaders().put(Headers.X_FORWARDED_FOR, peer); + } + Boolean proto = request.getAttachment(ProxiedRequestAttachments.IS_SSL); + if(proto == null || !proto) { + request.getRequestHeaders().put(Headers.X_FORWARDED_PROTO, "http"); + } else { + request.getRequestHeaders().put(Headers.X_FORWARDED_PROTO, "https"); + } + String hn = request.getAttachment(ProxiedRequestAttachments.SERVER_NAME); + if(hn != null) { + request.getRequestHeaders().put(Headers.X_FORWARDED_HOST, hn); + } + Integer port = request.getAttachment(ProxiedRequestAttachments.SERVER_PORT); + if(port != null) { + request.getRequestHeaders().put(Headers.X_FORWARDED_PORT, port); + } + + + Http2HeadersStreamSinkChannel sinkChannel; + try { + sinkChannel = http2Channel.createStream(request.getRequestHeaders()); + } catch (IOException e) { + clientCallback.failed(e); + return; + } + Http2ClientExchange exchange = new Http2ClientExchange(this, sinkChannel, request); + currentExchanges.put(sinkChannel.getStreamId(), exchange); + + + if(clientCallback != null) { + clientCallback.completed(exchange); + } + if (!hasContent) { + //if there is no content we flush the response channel. + //otherwise it is up to the user + try { + sinkChannel.shutdownWrites(); + if (!sinkChannel.flush()) { + sinkChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkChannel channel, IOException exception) { + handleError(exception); + } + })); + sinkChannel.resumeWrites(); + } + } catch (IOException e) { + handleError(e); + } + } else if (!sinkChannel.isWriteResumed()) { + try { + //TODO: this needs some more thought + if (!sinkChannel.flush()) { + sinkChannel.getWriteSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSinkChannel channel) { + try { + if (channel.flush()) { + channel.suspendWrites(); + } + } catch (IOException e) { + handleError(e); + } + } + }); + sinkChannel.resumeWrites(); + } + } catch (IOException e) { + handleError(e); + } + } + } + + private void handleError(IOException e) { + + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(Http2ClientConnection.this); + for (Map.Entry entry : currentExchanges.entrySet()) { + try { + entry.getValue().failed(e); + } catch (Exception ex) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(ex)); + } + } + } + + @Override + public StreamConnection performUpgrade() throws IOException { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + public Pool getBufferPool() { + return http2Channel.getBufferPool(); + } + + @Override + public SocketAddress getPeerAddress() { + return http2Channel.getPeerAddress(); + } + + @Override + public A getPeerAddress(Class type) { + return http2Channel.getPeerAddress(type); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public SocketAddress getLocalAddress() { + return http2Channel.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return http2Channel.getLocalAddress(type); + } + + @Override + public XnioWorker getWorker() { + return http2Channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return http2Channel.getIoThread(); + } + + @Override + public boolean isOpen() { + return http2Channel.isOpen(); + } + + @Override + public void close() throws IOException { + http2Channel.sendGoAway(0); + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option option) throws IOException { + return null; + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return null; + } + + @Override + public boolean isUpgraded() { + return false; + } + + private class Http2ReceiveListener implements ChannelListener { + + @Override + public void handleEvent(Http2Channel channel) { + try { + AbstractHttp2StreamSourceChannel result = channel.receive(); + if (result instanceof Http2StreamSourceChannel) { + Http2ClientExchange request = currentExchanges.remove(((Http2StreamSourceChannel) result).getStreamId()); + if (request == null && initialUpgradeRequest) { + Channels.drain(result, Long.MAX_VALUE); + initialUpgradeRequest = false; + return; + } else if(request == null) { + + //server side initiated stream, we can't deal with that at the moment + //just fail + //TODO: either handle this properly or at the very least send RST_STREAM + IoUtils.safeClose(channel); + return; + } + request.responseReady((Http2StreamSourceChannel) result); + } else if (result instanceof Http2PingStreamSourceChannel) { + handlePing((Http2PingStreamSourceChannel) result); + } else if (result instanceof Http2RstStreamStreamSourceChannel) { + int stream = ((Http2RstStreamStreamSourceChannel)result).getStreamId(); + UndertowLogger.REQUEST_LOGGER.debugf("Client received RST_STREAM for stream %s", stream); + Http2ClientExchange exchange = currentExchanges.get(stream); + + if(exchange != null) { + exchange.failed(UndertowMessages.MESSAGES.http2StreamWasReset()); + } + Channels.drain(result, Long.MAX_VALUE); + } else if(!channel.isOpen()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } else if(result != null) { + Channels.drain(result, Long.MAX_VALUE); + } + + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(Http2ClientConnection.this); + for (Map.Entry entry : currentExchanges.entrySet()) { + try { + entry.getValue().failed(e); + } catch (Exception ex) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(ex)); + } + } + } + + } + + private void handlePing(Http2PingStreamSourceChannel frame) { + byte[] id = frame.getData(); + if (!frame.isAck()) { + //server side ping, return it + frame.getHttp2Channel().sendPing(id); + } + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,125 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http2; + +import java.io.IOException; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.ContinueNotification; +import io.undertow.protocols.http2.Http2StreamSinkChannel; +import io.undertow.protocols.http2.Http2StreamSourceChannel; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; + +/** + * @author Stuart Douglas + */ +public class Http2ClientExchange extends AbstractAttachable implements ClientExchange { + private ClientCallback responseListener; + private ContinueNotification continueNotification; + private Http2StreamSourceChannel response; + private ClientResponse clientResponse; + private final ClientConnection clientConnection; + private final Http2StreamSinkChannel request; + private final ClientRequest clientRequest; + private IOException failedReason; + + public Http2ClientExchange(ClientConnection clientConnection, Http2StreamSinkChannel request, ClientRequest clientRequest) { + this.clientConnection = clientConnection; + this.request = request; + this.clientRequest = clientRequest; + } + + + @Override + public void setResponseListener(ClientCallback responseListener) { + this.responseListener = responseListener; + if(failedReason != null) { + responseListener.failed(failedReason); + } + } + + @Override + public void setContinueHandler(ContinueNotification continueHandler) { + String expect = clientRequest.getRequestHeaders().getFirst(Headers.EXPECT); + if ("100-continue".equalsIgnoreCase(expect)) { + continueHandler.handleContinue(this); + } + } + + @Override + public StreamSinkChannel getRequestChannel() { + return request; + } + + @Override + public StreamSourceChannel getResponseChannel() { + return response; + } + + @Override + public ClientRequest getRequest() { + return clientRequest; + } + + @Override + public ClientResponse getResponse() { + return clientResponse; + } + + @Override + public ClientResponse getContinueResponse() { + return null; + } + + @Override + public ClientConnection getConnection() { + return clientConnection; + } + + void failed(final IOException e) { + failedReason = e; + if(responseListener != null) { + responseListener.failed(e); + } + } + + void responseReady(Http2StreamSourceChannel result) { + this.response = result; + HeaderMap headers = result.getHeaders(); + final String status = result.getHeaders().getFirst(Http2ClientConnection.STATUS); + int statusCode = 500; + if (status != null && status.length() > 3) { + statusCode = Integer.parseInt(status.substring(0, 3)); + } + headers.remove(Http2ClientConnection.VERSION); + headers.remove(Http2ClientConnection.STATUS); + clientResponse = new ClientResponse(statusCode, status != null ? status.substring(3) : "", clientRequest.getProtocol(), headers); + if (responseListener != null) { + responseListener.completed(this); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/http2/Http2ClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,281 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.http2; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.net.ssl.SSLEngine; +import org.eclipse.jetty.alpn.ALPN; +import org.xnio.ChannelListener; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.PushBackStreamSourceConduit; +import org.xnio.ssl.JsseXnioSsl; +import org.xnio.ssl.SslConnection; +import org.xnio.ssl.XnioSsl; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientProvider; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.util.ImmediatePooled; + +/** + * Plaintext HTTP2 client provider that works using HTTP upgrade + * + * @author Stuart Douglas + */ +public class Http2ClientProvider implements ClientProvider { + + private static final String PROTOCOL_KEY = Http2ClientProvider.class.getName() + ".protocol"; + + private static final String HTTP2 = "h2-14"; + private static final String HTTP_1_1 = "http/1.1"; + + private static final List PROTOCOLS = Collections.unmodifiableList(Arrays.asList(new String[]{HTTP2, HTTP_1_1})); + + private static final Method ALPN_PUT_METHOD; + + static { + Method npnPutMethod; + try { + Class npnClass = Http2ClientProvider.class.getClassLoader().loadClass("org.eclipse.jetty.alpn.ALPN"); + npnPutMethod = npnClass.getDeclaredMethod("put", SSLEngine.class, Http2ClientProvider.class.getClassLoader().loadClass("org.eclipse.jetty.alpn.ALPN$Provider")); + } catch (Exception e) { + UndertowLogger.CLIENT_LOGGER.jettyALPNNotFound(); + npnPutMethod = null; + } + ALPN_PUT_METHOD = npnPutMethod; + } + + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, worker, ssl, bufferPool, options); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, ioThread, ssl, bufferPool, options); + } + + @Override + public Set handlesSchemes() { + return new HashSet<>(Arrays.asList(new String[]{"h2"})); + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + if(ALPN_PUT_METHOD == null) { + listener.failed(UndertowMessages.MESSAGES.jettyNPNNotAvailable()); + return; + } + if (ssl == null) { + listener.failed(UndertowMessages.MESSAGES.sslWasNull()); + return; + } + if(bindAddress == null) { + ssl.openSslConnection(worker, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ssl.openSslConnection(worker, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } + + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + if(ALPN_PUT_METHOD == null) { + listener.failed(UndertowMessages.MESSAGES.jettyNPNNotAvailable()); + return; + } + if (ssl == null) { + listener.failed(UndertowMessages.MESSAGES.sslWasNull()); + return; + } + if(bindAddress == null) { + ssl.openSslConnection(ioThread, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ssl.openSslConnection(ioThread, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } + + } + + private IoFuture.Notifier createNotifier(final ClientCallback listener) { + return new IoFuture.Notifier() { + @Override + public void notify(IoFuture ioFuture, Object o) { + if (ioFuture.getStatus() == IoFuture.Status.FAILED) { + listener.failed(ioFuture.getException()); + } + } + }; + } + + private ChannelListener createOpenListener(final ClientCallback listener, final URI uri, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + return new ChannelListener() { + @Override + public void handleEvent(StreamConnection connection) { + handleConnected(connection, listener, uri, ssl, bufferPool, options); + } + }; + } + + private void handleConnected(StreamConnection connection, final ClientCallback listener, URI uri, XnioSsl ssl, Pool bufferPool, OptionMap options) { + handlePotentialHttp2Connection(connection, listener, bufferPool, options, new ChannelListener() { + @Override + public void handleEvent(SslConnection channel) { + listener.failed(UndertowMessages.MESSAGES.spdyNotSupported()); + } + }); + } + + public static boolean isEnabled() { + return ALPN_PUT_METHOD != null; + } + + /** + * Not really part of the public API, but is used by the HTTP client to initiate a HTTP2 connection for HTTPS requests. + */ + public static void handlePotentialHttp2Connection(final StreamConnection connection, final ClientCallback listener, final Pool bufferPool, final OptionMap options, final ChannelListener http2FailedListener) { + + final SslConnection sslConnection = (SslConnection) connection; + final SSLEngine sslEngine = JsseXnioSsl.getSslEngine(sslConnection); + + String existing = (String) sslEngine.getSession().getValue(PROTOCOL_KEY); + if(existing != null) { + if (existing.equals(HTTP2)) { + listener.completed(createHttp2Channel(connection, bufferPool, options)); + } else { + sslConnection.getSourceChannel().suspendReads(); + http2FailedListener.handleEvent(sslConnection); + } + } else { + + final SpdySelectionProvider spdySelectionProvider = new SpdySelectionProvider(sslEngine); + try { + ALPN_PUT_METHOD.invoke(null, sslEngine, spdySelectionProvider); + } catch (Exception e) { + http2FailedListener.handleEvent(sslConnection); + return; + } + + try { + sslConnection.startHandshake(); + sslConnection.getSourceChannel().getReadSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSourceChannel channel) { + + if (spdySelectionProvider.selected != null) { + if (spdySelectionProvider.selected.equals(HTTP_1_1)) { + sslConnection.getSourceChannel().suspendReads(); + http2FailedListener.handleEvent(sslConnection); + return; + } else if (spdySelectionProvider.selected.equals(HTTP2)) { + listener.completed(createHttp2Channel(connection, bufferPool, options)); + } + } else { + ByteBuffer buf = ByteBuffer.allocate(100); + try { + int read = channel.read(buf); + if (read > 0) { + PushBackStreamSourceConduit pb = new PushBackStreamSourceConduit(connection.getSourceChannel().getConduit()); + pb.pushBack(new ImmediatePooled<>(buf)); + connection.getSourceChannel().setConduit(pb); + } + if ((spdySelectionProvider.selected == null && read > 0) || HTTP_1_1.equals(spdySelectionProvider.selected)) { + sslConnection.getSourceChannel().suspendReads(); + http2FailedListener.handleEvent(sslConnection); + return; + } else if (spdySelectionProvider.selected != null) { + //we have spdy + if (spdySelectionProvider.selected.equals(HTTP2)) { + listener.completed(createHttp2Channel(connection, bufferPool, options)); + } + } + } catch (IOException e) { + listener.failed(e); + } + } + } + + }); + sslConnection.getSourceChannel().resumeReads(); + } catch (IOException e) { + listener.failed(e); + } + } + + } + + private static Http2ClientConnection createHttp2Channel(StreamConnection connection, Pool bufferPool, OptionMap options) { + Http2Channel http2Channel = new Http2Channel(connection, bufferPool, null, true, false, options); + return new Http2ClientConnection(http2Channel, false); + } + + private static class SpdySelectionProvider implements ALPN.ClientProvider { + private String selected; + private final SSLEngine sslEngine; + + private SpdySelectionProvider(SSLEngine sslEngine) { + this.sslEngine = sslEngine; + } + + @Override + public boolean supports() { + return true; + } + + @Override + public List protocols() { + return PROTOCOLS; + } + + @Override + public void unsupported() { + selected = HTTP_1_1; + } + + @Override + public void selected(String s) { + + ALPN.remove(sslEngine); + selected = s; + sslEngine.getSession().putValue(PROTOCOL_KEY, selected); + } + + private String getSelected() { + return selected; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,305 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.spdy; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.protocols.spdy.SpdyChannel; +import io.undertow.protocols.spdy.SpdyPingStreamSourceChannel; +import io.undertow.protocols.spdy.SpdyRstStreamStreamSourceChannel; +import io.undertow.protocols.spdy.SpdyStreamSourceChannel; +import io.undertow.protocols.spdy.SpdySynReplyStreamSourceChannel; +import io.undertow.protocols.spdy.SpdySynStreamStreamSinkChannel; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import static io.undertow.util.Headers.CONTENT_LENGTH; +import static io.undertow.util.Headers.TRANSFER_ENCODING; + +/** + * @author Stuart Douglas + */ +public class SpdyClientConnection implements ClientConnection { + + + static final HttpString METHOD = new HttpString(":method"); + static final HttpString PATH = new HttpString(":path"); + static final HttpString SCHEME = new HttpString(":scheme"); + static final HttpString VERSION = new HttpString(":version"); + static final HttpString HOST = new HttpString(":host"); + static final HttpString STATUS = new HttpString(":status"); + + private final SpdyChannel spdyChannel; + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + private final Map currentExchanges = new ConcurrentHashMap<>(); + + public SpdyClientConnection(SpdyChannel spdyChannel) { + this.spdyChannel = spdyChannel; + spdyChannel.getReceiveSetter().set(new SpdyReceiveListener()); + spdyChannel.resumeReceives(); + spdyChannel.addCloseTask(new ChannelListener() { + @Override + public void handleEvent(SpdyChannel channel) { + ChannelListeners.invokeChannelListener(SpdyClientConnection.this, closeSetter.get()); + } + }); + } + + @Override + public void sendRequest(ClientRequest request, ClientCallback clientCallback) { + request.getRequestHeaders().put(PATH, request.getPath()); + request.getRequestHeaders().put(SCHEME, "https"); + request.getRequestHeaders().put(VERSION, request.getProtocol().toString()); + request.getRequestHeaders().put(METHOD, request.getMethod().toString()); + request.getRequestHeaders().put(HOST, request.getRequestHeaders().getFirst(Headers.HOST)); + request.getRequestHeaders().remove(Headers.HOST); + + SpdySynStreamStreamSinkChannel sinkChannel; + try { + sinkChannel = spdyChannel.createStream(request.getRequestHeaders()); + } catch (IOException e) { + clientCallback.failed(e); + return; + } + SpdyClientExchange exchange = new SpdyClientExchange(this, sinkChannel, request); + currentExchanges.put(sinkChannel.getStreamId(), exchange); + + + boolean hasContent = true; + + String fixedLengthString = request.getRequestHeaders().getFirst(CONTENT_LENGTH); + String transferEncodingString = request.getRequestHeaders().getLast(TRANSFER_ENCODING); + if (fixedLengthString != null) { + try { + long length = Long.parseLong(fixedLengthString); + hasContent = length != 0; + } catch (NumberFormatException e) { + handleError(new IOException(e)); + return; + } + } else if (transferEncodingString == null) { + hasContent = false; + } + if(clientCallback != null) { + clientCallback.completed(exchange); + } + if (!hasContent) { + //if there is no content we flush the response channel. + //otherwise it is up to the user + try { + sinkChannel.shutdownWrites(); + if (!sinkChannel.flush()) { + sinkChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkChannel channel, IOException exception) { + handleError(exception); + } + })); + sinkChannel.resumeWrites(); + } + } catch (IOException e) { + handleError(e); + } + } else if (!sinkChannel.isWriteResumed()) { + try { + //TODO: this needs some more thought + if (!sinkChannel.flush()) { + sinkChannel.getWriteSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSinkChannel channel) { + try { + if (channel.flush()) { + channel.suspendWrites(); + } + } catch (IOException e) { + handleError(e); + } + } + }); + sinkChannel.resumeWrites(); + } + } catch (IOException e) { + handleError(e); + } + } + } + + private void handleError(IOException e) { + + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(SpdyClientConnection.this); + for (Map.Entry entry : currentExchanges.entrySet()) { + try { + entry.getValue().failed(e); + } catch (Exception ex) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(ex)); + } + } + } + + @Override + public StreamConnection performUpgrade() throws IOException { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + public Pool getBufferPool() { + return spdyChannel.getBufferPool(); + } + + @Override + public SocketAddress getPeerAddress() { + return spdyChannel.getPeerAddress(); + } + + @Override + public A getPeerAddress(Class type) { + return spdyChannel.getPeerAddress(type); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public SocketAddress getLocalAddress() { + return spdyChannel.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return spdyChannel.getLocalAddress(type); + } + + @Override + public XnioWorker getWorker() { + return spdyChannel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return spdyChannel.getIoThread(); + } + + @Override + public boolean isOpen() { + return spdyChannel.isOpen(); + } + + @Override + public void close() throws IOException { + spdyChannel.sendGoAway(SpdyChannel.CLOSE_OK); + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option option) throws IOException { + return null; + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return null; + } + + @Override + public boolean isUpgraded() { + return false; + } + + private class SpdyReceiveListener implements ChannelListener { + + @Override + public void handleEvent(SpdyChannel channel) { + try { + SpdyStreamSourceChannel result = channel.receive(); + if (result instanceof SpdySynReplyStreamSourceChannel) { + SpdyClientExchange request = currentExchanges.remove(((SpdySynReplyStreamSourceChannel) result).getStreamId()); + if (request == null) { + //server side initiated stream, we can't deal with that at the moment + //just fail + //TODO: either handle this properly or at the very least send RST_STREAM + IoUtils.safeClose(SpdyClientConnection.this); + return; + } + request.responseReady((SpdySynReplyStreamSourceChannel) result); + + } else if (result instanceof SpdyPingStreamSourceChannel) { + handlePing((SpdyPingStreamSourceChannel) result); + } else if (result instanceof SpdyRstStreamStreamSourceChannel) { + int stream = ((SpdyRstStreamStreamSourceChannel)result).getStreamId(); + UndertowLogger.REQUEST_LOGGER.debugf("Client received RST_STREAM for stream %s", stream); + SpdyClientExchange exchange = currentExchanges.get(stream); + if(exchange != null) { + exchange.failed(UndertowMessages.MESSAGES.spdyStreamWasReset()); + } + } else if(!channel.isOpen()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(SpdyClientConnection.this); + for (Map.Entry entry : currentExchanges.entrySet()) { + try { + entry.getValue().failed(e); + } catch (Exception ex) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(ex)); + } + } + } + + } + + private void handlePing(SpdyPingStreamSourceChannel frame) { + int id = frame.getId(); + if (id % 2 == 0) { + //server side ping, return it + frame.getSpdyChannel().sendPing(id); + } + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,129 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.spdy; + +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.ContinueNotification; +import io.undertow.protocols.spdy.SpdyStreamSinkChannel; +import io.undertow.protocols.spdy.SpdyStreamSourceChannel; +import io.undertow.protocols.spdy.SpdySynReplyStreamSourceChannel; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; + +/** + * @author Stuart Douglas + */ +public class SpdyClientExchange extends AbstractAttachable implements ClientExchange { + private ClientCallback responseListener; + private ContinueNotification continueNotification; + private SpdyStreamSourceChannel response; + private ClientResponse clientResponse; + private final ClientConnection clientConnection; + private final SpdyStreamSinkChannel request; + private final ClientRequest clientRequest; + private IOException failedReason; + + public SpdyClientExchange(ClientConnection clientConnection, SpdyStreamSinkChannel request, ClientRequest clientRequest) { + this.clientConnection = clientConnection; + this.request = request; + this.clientRequest = clientRequest; + } + + @Override + public void setResponseListener(ClientCallback responseListener) { + this.responseListener = responseListener; + if (responseListener != null) { + if (failedReason != null) { + responseListener.failed(failedReason); + } else if (clientResponse != null) { + responseListener.completed(this); + } + } + } + + @Override + public void setContinueHandler(ContinueNotification continueHandler) { + String expect = clientRequest.getRequestHeaders().getFirst(Headers.EXPECT); + if ("100-continue".equalsIgnoreCase(expect)) { + continueHandler.handleContinue(this); + } + } + + @Override + public StreamSinkChannel getRequestChannel() { + return request; + } + + @Override + public StreamSourceChannel getResponseChannel() { + return response; + } + + @Override + public ClientRequest getRequest() { + return clientRequest; + } + + @Override + public ClientResponse getResponse() { + return clientResponse; + } + + @Override + public ClientResponse getContinueResponse() { + return null; + } + + @Override + public ClientConnection getConnection() { + return clientConnection; + } + + void failed(final IOException e) { + this.failedReason = e; + if(responseListener != null) { + responseListener.failed(e); + } + } + + void responseReady(SpdySynReplyStreamSourceChannel result) { + this.response = result; + HeaderMap headers = result.getHeaders(); + final String status = result.getHeaders().getFirst(SpdyClientConnection.STATUS); + int statusCode = 500; + if (status != null && status.length() > 3) { + statusCode = Integer.parseInt(status.substring(0, 3)); + } + headers.remove(SpdyClientConnection.VERSION); + headers.remove(SpdyClientConnection.STATUS); + clientResponse = new ClientResponse(statusCode, status != null ? status.substring(3) : "", clientRequest.getProtocol(), headers); + if (responseListener != null) { + responseListener.completed(this); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/client/spdy/SpdyClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,309 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.client.spdy; + +import java.io.IOException; +import java.lang.reflect.Method; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import javax.net.ssl.SSLEngine; +import org.eclipse.jetty.alpn.ALPN; +import org.xnio.BufferAllocator; +import org.xnio.ByteBufferSlicePool; +import org.xnio.ChannelListener; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.PushBackStreamSourceConduit; +import org.xnio.ssl.JsseXnioSsl; +import org.xnio.ssl.SslConnection; +import org.xnio.ssl.XnioSsl; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientProvider; +import io.undertow.protocols.spdy.SpdyChannel; +import io.undertow.util.ImmediatePooled; + +/** + * Dedicated SPDY client that will never fall back to HTTPS + * + * @author Stuart Douglas + */ +public class SpdyClientProvider implements ClientProvider { + + private static final String PROTOCOL_KEY = SpdyClientProvider.class.getName() + ".protocol"; + + private static final String SPDY_3 = "spdy/3"; + private static final String SPDY_3_1 = "spdy/3.1"; + private static final String HTTP_1_1 = "http/1.1"; + + private static final List PROTOCOLS = Collections.unmodifiableList(Arrays.asList(new String[]{SPDY_3_1, HTTP_1_1})); + + private static final Method ALPN_PUT_METHOD; + + static { + Method npnPutMethod; + try { + Class npnClass = SpdyClientProvider.class.getClassLoader().loadClass("org.eclipse.jetty.alpn.ALPN"); + npnPutMethod = npnClass.getDeclaredMethod("put", SSLEngine.class, SpdyClientProvider.class.getClassLoader().loadClass("org.eclipse.jetty.alpn.ALPN$Provider")); + } catch (Exception e) { + UndertowLogger.CLIENT_LOGGER.jettyALPNNotFound(); + npnPutMethod = null; + } + ALPN_PUT_METHOD = npnPutMethod; + } + + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, worker, ssl, bufferPool, options); + } + + @Override + public void connect(final ClientCallback listener, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + connect(listener, null, uri, ioThread, ssl, bufferPool, options); + } + + @Override + public Set handlesSchemes() { + return new HashSet<>(Arrays.asList(new String[]{"spdy", "spdy-plain"})); + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioWorker worker, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + if(uri.getScheme().equals("spdy-plain")) { + + if(bindAddress == null) { + worker.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + worker.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), null, options).addNotifier(createNotifier(listener), null); + } + return; + } + + + if(ALPN_PUT_METHOD == null) { + listener.failed(UndertowMessages.MESSAGES.jettyNPNNotAvailable()); + return; + } + if (ssl == null) { + listener.failed(UndertowMessages.MESSAGES.sslWasNull()); + return; + } + if(bindAddress == null) { + ssl.openSslConnection(worker, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ssl.openSslConnection(worker, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } + + } + + @Override + public void connect(final ClientCallback listener, InetSocketAddress bindAddress, final URI uri, final XnioIoThread ioThread, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + if(uri.getScheme().equals("spdy-plain")) { + + if(bindAddress == null) { + ioThread.openStreamConnection(new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ioThread.openStreamConnection(bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), null, options).addNotifier(createNotifier(listener), null); + } + return; + } + + if(ALPN_PUT_METHOD == null) { + listener.failed(UndertowMessages.MESSAGES.jettyNPNNotAvailable()); + return; + } + if (ssl == null) { + listener.failed(UndertowMessages.MESSAGES.sslWasNull()); + return; + } + if(bindAddress == null) { + ssl.openSslConnection(ioThread, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } else { + ssl.openSslConnection(ioThread, bindAddress, new InetSocketAddress(uri.getHost(), uri.getPort() == -1 ? 443 : uri.getPort()), createOpenListener(listener, uri, ssl, bufferPool, options), options).addNotifier(createNotifier(listener), null); + } + + } + + private IoFuture.Notifier createNotifier(final ClientCallback listener) { + return new IoFuture.Notifier() { + @Override + public void notify(IoFuture ioFuture, Object o) { + if (ioFuture.getStatus() == IoFuture.Status.FAILED) { + listener.failed(ioFuture.getException()); + } + } + }; + } + + private ChannelListener createOpenListener(final ClientCallback listener, final URI uri, final XnioSsl ssl, final Pool bufferPool, final OptionMap options) { + return new ChannelListener() { + @Override + public void handleEvent(StreamConnection connection) { + handleConnected(connection, listener, uri, ssl, bufferPool, options); + } + }; + } + + private void handleConnected(StreamConnection connection, final ClientCallback listener, URI uri, XnioSsl ssl, Pool bufferPool, OptionMap options) { + if(connection instanceof SslConnection) { + handlePotentialSpdyConnection(connection, listener, bufferPool, options, new ChannelListener() { + @Override + public void handleEvent(SslConnection channel) { + listener.failed(UndertowMessages.MESSAGES.spdyNotSupported()); + } + }); + } else { + listener.completed(createSpdyChannel(connection, bufferPool)); + } + } + + public static boolean isEnabled() { + return ALPN_PUT_METHOD != null; + } + + /** + * Not really part of the public API, but is used by the HTTP client to initiate a SPDY connection for HTTPS requests. + */ + public static void handlePotentialSpdyConnection(final StreamConnection connection, final ClientCallback listener, final Pool bufferPool, final OptionMap options, final ChannelListener spdyFailedListener) { + + final SslConnection sslConnection = (SslConnection) connection; + final SSLEngine sslEngine = JsseXnioSsl.getSslEngine(sslConnection); + + String existing = (String) sslEngine.getSession().getValue(PROTOCOL_KEY); + if(existing != null) { + if (existing.equals(SPDY_3) || existing.equals(SPDY_3_1)) { + listener.completed(createSpdyChannel(connection, bufferPool)); + } else { + sslConnection.getSourceChannel().suspendReads(); + spdyFailedListener.handleEvent(sslConnection); + } + } else { + + final SpdySelectionProvider spdySelectionProvider = new SpdySelectionProvider(sslEngine); + try { + ALPN_PUT_METHOD.invoke(null, sslEngine, spdySelectionProvider); + } catch (Exception e) { + spdyFailedListener.handleEvent(sslConnection); + return; + } + + try { + sslConnection.startHandshake(); + sslConnection.getSourceChannel().getReadSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSourceChannel channel) { + + if (spdySelectionProvider.selected != null) { + if (spdySelectionProvider.selected.equals(HTTP_1_1)) { + sslConnection.getSourceChannel().suspendReads(); + spdyFailedListener.handleEvent(sslConnection); + return; + } else if (spdySelectionProvider.selected.equals(SPDY_3) || spdySelectionProvider.selected.equals(SPDY_3_1)) { + listener.completed(createSpdyChannel(connection, bufferPool)); + } + } else { + ByteBuffer buf = ByteBuffer.allocate(100); + try { + int read = channel.read(buf); + if (read > 0) { + PushBackStreamSourceConduit pb = new PushBackStreamSourceConduit(connection.getSourceChannel().getConduit()); + pb.pushBack(new ImmediatePooled<>(buf)); + connection.getSourceChannel().setConduit(pb); + } + if ((spdySelectionProvider.selected == null && read > 0) || HTTP_1_1.equals(spdySelectionProvider.selected)) { + sslConnection.getSourceChannel().suspendReads(); + spdyFailedListener.handleEvent(sslConnection); + return; + } else if (spdySelectionProvider.selected != null) { + //we have spdy + if (spdySelectionProvider.selected.equals(SPDY_3) || spdySelectionProvider.selected.equals(SPDY_3_1)) { + listener.completed(createSpdyChannel(connection, bufferPool)); + } + } + } catch (IOException e) { + listener.failed(e); + } + } + } + + }); + sslConnection.getSourceChannel().resumeReads(); + } catch (IOException e) { + listener.failed(e); + } + } + + } + + private static SpdyClientConnection createSpdyChannel(StreamConnection connection, Pool bufferPool) { + SpdyChannel spdyChannel = new SpdyChannel(connection, bufferPool, null, new ByteBufferSlicePool(BufferAllocator.BYTE_BUFFER_ALLOCATOR, 8192, 8192), true); + return new SpdyClientConnection(spdyChannel); + } + + private static class SpdySelectionProvider implements ALPN.ClientProvider { + private String selected; + private final SSLEngine sslEngine; + + private SpdySelectionProvider(SSLEngine sslEngine) { + this.sslEngine = sslEngine; + } + + @Override + public boolean supports() { + return true; + } + + @Override + public List protocols() { + return PROTOCOLS; + } + + @Override + public void unsupported() { + selected = HTTP_1_1; + } + + @Override + public void selected(String s) { + + ALPN.remove(sslEngine); + selected = s; + sslEngine.getSession().putValue(PROTOCOL_KEY, selected); + } + + private String getSelected() { + return selected; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/AbstractFixedLengthStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/AbstractFixedLengthStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/AbstractFixedLengthStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,324 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import org.xnio.Buffers; +import org.xnio.channels.FixedLengthOverflowException; +import org.xnio.channels.FixedLengthUnderflowException; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import static java.lang.Math.min; +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreSet; +import static org.xnio.Bits.longBitMask; + +/** + * A channel which writes a fixed amount of data. A listener is called once the data has been written. + * + * @author David M. Lloyd + */ +public abstract class AbstractFixedLengthStreamSinkConduit extends AbstractStreamSinkConduit { + private int config; + + private long state; + + private boolean broken = false; + + private static final int CONF_FLAG_CONFIGURABLE = 1 << 0; + private static final int CONF_FLAG_PASS_CLOSE = 1 << 1; + + private static final long FLAG_CLOSE_REQUESTED = 1L << 63L; + private static final long FLAG_CLOSE_COMPLETE = 1L << 62L; + private static final long FLAG_FINISHED_CALLED = 1L << 61L; + private static final long MASK_COUNT = longBitMask(0, 60); + + /** + * Construct a new instance. + * + * @param next the next channel + * @param contentLength the content length + * @param configurable {@code true} if this instance should pass configuration to the next + * @param propagateClose {@code true} if this instance should pass close to the next + */ + public AbstractFixedLengthStreamSinkConduit(final StreamSinkConduit next, final long contentLength, final boolean configurable, final boolean propagateClose) { + super(next); + if (contentLength < 0L) { + throw new IllegalArgumentException("Content length must be greater than or equal to zero"); + } else if (contentLength > MASK_COUNT) { + throw new IllegalArgumentException("Content length is too long"); + } + config = (configurable ? CONF_FLAG_CONFIGURABLE : 0) | (propagateClose ? CONF_FLAG_PASS_CLOSE : 0); + this.state = contentLength; + } + + protected void reset(long contentLength, boolean propagateClose) { + this.state = contentLength; + if (propagateClose) { + config |= CONF_FLAG_PASS_CLOSE; + } else { + config &= ~CONF_FLAG_PASS_CLOSE; + } + } + + public int write(final ByteBuffer src) throws IOException { + long val = state; + final long remaining = val & MASK_COUNT; + if (!src.hasRemaining()) { + return 0; + } + if (allAreSet(val, FLAG_CLOSE_REQUESTED)) { + throw new ClosedChannelException(); + } + int oldLimit = src.limit(); + if (remaining == 0) { + throw new FixedLengthOverflowException(); + } else if (src.remaining() > remaining) { + src.limit((int) (src.position() + remaining)); + } + int res = 0; + try { + return res = next.write(src); + } catch (IOException e) { + broken = true; + throw e; + } finally { + src.limit(oldLimit); + exitWrite(val, (long) res); + } + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if (length == 0) { + return 0L; + } else if (length == 1) { + return write(srcs[offset]); + } + long val = state; + final long remaining = val & MASK_COUNT; + if (allAreSet(val, FLAG_CLOSE_REQUESTED)) { + throw new ClosedChannelException(); + } + long toWrite = Buffers.remaining(srcs, offset, length); + if (remaining == 0) { + throw new FixedLengthOverflowException(); + } + int[] limits = null; + if (toWrite > remaining) { + limits = new int[length]; + long r = remaining; + for (int i = offset; i < offset + length; ++i) { + limits[i - offset] = srcs[i].limit(); + int br = srcs[i].remaining(); + if(br < r) { + r -= br; + } else { + srcs[i].limit((int) (srcs[i].position() + r)); + r = 0; + } + } + } + long res = 0L; + try { + return res = next.write(srcs, offset, length); + } catch (IOException e) { + broken = true; + throw e; + } finally { + if (limits != null) { + for (int i = offset; i < offset + length; ++i) { + srcs[i].limit(limits[i - offset]); + } + } + exitWrite(val, res); + } + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + try { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } catch (IOException e) { + broken = true; + throw e; + } + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + try { + return Conduits.writeFinalBasic(this, src); + } catch (IOException e) { + broken = true; + throw e; + } + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (count == 0L) return 0L; + long val = state; + if (allAreSet(val, FLAG_CLOSE_REQUESTED)) { + throw new ClosedChannelException(); + } + if (allAreClear(val, MASK_COUNT)) { + throw new FixedLengthOverflowException(); + } + long res = 0L; + try { + return res = next.transferFrom(src, position, min(count, (val & MASK_COUNT))); + } catch (IOException e) { + broken = true; + throw e; + } finally { + exitWrite(val, res); + } + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (count == 0L) return 0L; + long val = state; + if (allAreSet(val, FLAG_CLOSE_REQUESTED)) { + throw new ClosedChannelException(); + } + if (allAreClear(val, MASK_COUNT)) { + throw new FixedLengthOverflowException(); + } + long res = 0L; + try { + return res = next.transferFrom(source, min(count, (val & MASK_COUNT)), throughBuffer); + } catch (IOException e) { + broken = true; + throw e; + } finally { + exitWrite(val, res); + } + } + + public boolean flush() throws IOException { + long val = state; + if (anyAreSet(val, FLAG_CLOSE_COMPLETE)) { + return true; + } + boolean flushed = false; + try { + return flushed = next.flush(); + } catch (IOException e) { + broken = true; + throw e; + } finally { + exitFlush(val, flushed); + } + } + + public boolean isWriteResumed() { + // not perfect but not provably wrong either... + return allAreClear(state, FLAG_CLOSE_COMPLETE) && next.isWriteResumed(); + } + + public void wakeupWrites() { + long val = state; + if (anyAreSet(val, FLAG_CLOSE_COMPLETE)) { + return; + } + next.wakeupWrites(); + } + + public void terminateWrites() throws IOException { + final long val = enterShutdown(); + if (anyAreSet(val, MASK_COUNT) && !broken) { + try { + throw new FixedLengthUnderflowException((val & MASK_COUNT) + " bytes remaining"); + } finally { + next.truncateWrites(); + } + } else if (allAreSet(config, CONF_FLAG_PASS_CLOSE)) { + next.terminateWrites(); + } + + } + + public void awaitWritable() throws IOException { + next.awaitWritable(); + } + + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + next.awaitWritable(time, timeUnit); + } + + + /** + * Get the number of remaining bytes in this fixed length channel. + * + * @return the number of remaining bytes + */ + public long getRemaining() { + return state & MASK_COUNT; + } + + private void exitWrite(long oldVal, long consumed) { + long newVal = oldVal - consumed; + state = newVal; + } + + private void exitFlush(long oldVal, boolean flushed) { + long newVal = oldVal; + boolean callFinish = false; + if ((anyAreSet(oldVal, FLAG_CLOSE_REQUESTED) || (newVal & MASK_COUNT) == 0L) && flushed) { + newVal |= FLAG_CLOSE_COMPLETE; + + if (!anyAreSet(oldVal, FLAG_FINISHED_CALLED) && (newVal & MASK_COUNT) == 0L) { + newVal |= FLAG_FINISHED_CALLED; + callFinish = true; + } + state = newVal; + if (callFinish) { + channelFinished(); + } + } + } + + protected void channelFinished() { + } + + private long enterShutdown() { + long oldVal, newVal; + oldVal = state; + if (anyAreSet(oldVal, FLAG_CLOSE_REQUESTED | FLAG_CLOSE_COMPLETE)) { + // no action necessary + return oldVal; + } + newVal = oldVal | FLAG_CLOSE_REQUESTED; + if (anyAreSet(oldVal, MASK_COUNT)) { + // error: channel not filled. set both close flags. + newVal |= FLAG_CLOSE_COMPLETE; + } + state = newVal; + return oldVal; + } + +} Index: 3rdParty_sources/undertow/io/undertow/conduits/AbstractFramedStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/AbstractFramedStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/AbstractFramedStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,319 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowMessages; +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayDeque; +import java.util.Deque; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * Utility class to ease the implementation of framed protocols. This call provides a queue of frames, and a callback + * that can be invoked when a frame event occurs. + *

+ * When a write takes place all frames are attempted to be written out at once via a gathering write. Frames can be + * queued via {@link #queueFrame(io.undertow.conduits.AbstractFramedStreamSinkConduit.FrameCallBack, java.nio.ByteBuffer...)}. + * + * @author Stuart Douglas + */ +public class AbstractFramedStreamSinkConduit extends AbstractStreamSinkConduit { + + private final Deque frameQueue = new ArrayDeque<>(); + /** + * The total amount of data that has been queued to be written out + */ + private long queuedData = 0; + /** + * The total number of buffers that have been queued to be written out + */ + private int bufferCount = 0; + + private int state; + + private static final int FLAG_WRITES_TERMINATED = 1; + private static final int FLAG_DELEGATE_SHUTDOWN = 2; + + /** + * Construct a new instance. + * + * @param next the delegate conduit to set + */ + protected AbstractFramedStreamSinkConduit(StreamSinkConduit next) { + super(next); + } + + /** + * Queues a frame for sending. + * + * @param callback + * @param data + */ + protected void queueFrame(FrameCallBack callback, ByteBuffer... data) { + queuedData += Buffers.remaining(data); + bufferCount += data.length; + frameQueue.add(new Frame(callback, data, 0, data.length)); + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public int write(ByteBuffer src) throws IOException { + if (anyAreSet(state, FLAG_WRITES_TERMINATED)) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return (int) doWrite(new ByteBuffer[]{src}, 0, 1); + } + + @Override + public long write(ByteBuffer[] srcs, int offs, int len) throws IOException { + if (anyAreSet(state, FLAG_WRITES_TERMINATED)) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + return doWrite(srcs, offs, len); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offs, int len) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offs, len); + } + + + private long doWrite(ByteBuffer[] additionalData, int offs, int len) throws IOException { + ByteBuffer[] buffers = new ByteBuffer[bufferCount + (additionalData == null ? 0 : len)]; + int count = 0; + for (Frame frame : frameQueue) { + for (int i = frame.offs; i < frame.offs + frame.len; ++i) { + buffers[count++] = frame.data[i]; + } + } + long totalData = queuedData; + long userData = 0; + if (additionalData != null) { + for (int i = offs; i < offs + len; ++i) { + buffers[count++] = additionalData[i]; + userData += additionalData[i].remaining(); + } + } + totalData += userData; + try { + long written = next.write(buffers, 0, buffers.length); + if (written > this.queuedData) { + this.queuedData = 0; + } else { + this.queuedData -= written; + } + long toAllocate = written; + Frame frame = frameQueue.peek(); + while (frame != null) { + if (frame.remaining > toAllocate) { + frame.remaining -= toAllocate; + return 0; + } else { + frameQueue.poll(); //this frame is done, remove it + //note that after we start calling done() we can't re-use the buffers[] array + //as pooled buffers may have been returned to the pool and re-used + FrameCallBack cb = frame.callback; + if (cb != null) { + cb.done(); + } + bufferCount -= frame.len; + toAllocate -= frame.remaining; + } + frame = frameQueue.peek(); + } + return toAllocate; + + } catch (IOException e) { + //on exception we fail every item in the frame queue + try { + for (Frame frame : frameQueue) { + FrameCallBack cb = frame.callback; + if (cb != null) { + cb.failed(e); + } + } + frameQueue.clear(); + bufferCount = 0; + queuedData = 0; + } finally { + throw e; + } + } + } + + protected long queuedDataLength() { + return queuedData; + } + + + @Override + public void terminateWrites() throws IOException { + if (anyAreSet(state, FLAG_WRITES_TERMINATED)) { + return; + } + queueCloseFrames(); + state |= FLAG_WRITES_TERMINATED; + if (queuedData == 0) { + state |= FLAG_DELEGATE_SHUTDOWN; + doTerminateWrites(); + finished(); + } + } + + protected void doTerminateWrites() throws IOException { + next.terminateWrites(); + } + + @Override + public boolean flush() throws IOException { + if (queuedData > 0) { + doWrite(null, 0, 0); + } + if (queuedData > 0) { + return false; + } + if (anyAreSet(state, FLAG_WRITES_TERMINATED) && allAreClear(state, FLAG_DELEGATE_SHUTDOWN)) { + doTerminateWrites(); + state |= FLAG_DELEGATE_SHUTDOWN; + finished(); + } + return next.flush(); + } + + @Override + public void truncateWrites() throws IOException { + for (Frame frame : frameQueue) { + FrameCallBack cb = frame.callback; + if (cb != null) { + cb.failed(UndertowMessages.MESSAGES.channelIsClosed()); + } + } + } + + protected void queueCloseFrames() { + + } + + protected void finished() { + + } + + /** + * Interface that is called when a frame event takes place. The events are: + *

+ *

+ */ + public interface FrameCallBack { + + void done(); + + void failed(final IOException e); + + } + + private class Frame { + + final FrameCallBack callback; + final ByteBuffer[] data; + final int offs; + final int len; + long remaining; + + private Frame(FrameCallBack callback, ByteBuffer[] data, int offs, int len) { + this.callback = callback; + this.data = data; + this.offs = offs; + this.len = len; + this.remaining = Buffers.remaining(data, offs, len); + } + } + + protected class PooledBufferFrameCallback implements FrameCallBack { + + private final Pooled buffer; + + public PooledBufferFrameCallback(Pooled buffer) { + this.buffer = buffer; + } + + @Override + public void done() { + buffer.free(); + } + + @Override + public void failed(IOException e) { + buffer.free(); + } + } + + + protected class PooledBuffersFrameCallback implements FrameCallBack { + + private final Pooled[] buffers; + + public PooledBuffersFrameCallback(Pooled... buffers) { + this.buffers = buffers; + } + + @Override + public void done() { + for (Pooled buffer : buffers) { + buffer.free(); + } + } + + @Override + public void failed(IOException e) { + done(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/BrokenStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/BrokenStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/BrokenStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.StreamSourceConduit; + +/** + * @author Stuart Douglas + */ +public class BrokenStreamSourceConduit extends AbstractStreamSourceConduit { + + private final IOException exception; + + /** + * Construct a new instance. + * + * @param next the delegate conduit to set + * @param exception + */ + public BrokenStreamSourceConduit(final StreamSourceConduit next, final IOException exception) { + super(next); + this.exception = exception; + } + + @Override + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + throw exception; + } + + @Override + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + throw exception; + } + + + @Override + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + throw exception; + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + throw exception; + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/ChunkReader.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/ChunkReader.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/ChunkReader.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,262 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowMessages; +import io.undertow.util.Attachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import org.xnio.conduits.Conduit; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreSet; +import static org.xnio.Bits.longBitMask; + +/** + * Utility class for reading chunked streams. + * + * @author Stuart Douglas + */ +class ChunkReader { + + private static final long FLAG_FINISHED = 1L << 62L; + private static final long FLAG_READING_LENGTH = 1L << 61L; + private static final long FLAG_READING_TILL_END_OF_LINE = 1L << 60L; + private static final long FLAG_READING_NEWLINE = 1L << 59L; + private static final long FLAG_READING_AFTER_LAST = 1L << 58L; + + private static final long MASK_COUNT = longBitMask(0, 56); + + private long state; + private final Attachable attachable; + private final AttachmentKey trailerAttachmentKey; + /** + * The trailer parser that stores the trailer parse state. If this class is not null it means + * that we are in the middle of parsing trailers. + */ + private TrailerParser trailerParser; + + private final ConduitListener finishListener; + private final T conduit; + + public ChunkReader(final Attachable attachable, final AttachmentKey trailerAttachmentKey, ConduitListener finishListener, T conduit) { + this.attachable = attachable; + this.trailerAttachmentKey = trailerAttachmentKey; + this.finishListener = finishListener; + this.conduit = conduit; + this.state = FLAG_READING_LENGTH; + } + + public long readChunk(final ByteBuffer buf) throws IOException { + long oldVal = state; + long chunkRemaining = state & MASK_COUNT; + + if (chunkRemaining > 0 && !anyAreSet(state, FLAG_READING_AFTER_LAST | FLAG_READING_LENGTH | FLAG_READING_NEWLINE | FLAG_READING_TILL_END_OF_LINE)) { + return chunkRemaining; + } + long newVal = oldVal & ~MASK_COUNT; + try { + if (anyAreSet(oldVal, FLAG_READING_AFTER_LAST)) { + int ret = handleChunkedRequestEnd(buf); + if (ret == -1) { + newVal |= FLAG_FINISHED & ~FLAG_READING_AFTER_LAST; + return -1; + } + return 0; + } + + while (anyAreSet(newVal, FLAG_READING_NEWLINE)) { + while (buf.hasRemaining()) { + byte b = buf.get(); + if (b == '\n') { + newVal = newVal & ~FLAG_READING_NEWLINE | FLAG_READING_LENGTH; + break; + } + } + if (anyAreSet(newVal, FLAG_READING_NEWLINE)) { + return 0; + } + } + + while (anyAreSet(newVal, FLAG_READING_LENGTH)) { + while (buf.hasRemaining()) { + byte b = buf.get(); + if ((b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')) { + chunkRemaining <<= 4; //shift it 4 bytes and then add the next value to the end + chunkRemaining += Character.digit((char) b, 16); + } else { + if (b == '\n') { + newVal = newVal & ~FLAG_READING_LENGTH; + } else { + newVal = newVal & ~FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE; + } + break; + } + } + if (anyAreSet(newVal, FLAG_READING_LENGTH)) { + return 0; + } + } + while (anyAreSet(newVal, FLAG_READING_TILL_END_OF_LINE)) { + while (buf.hasRemaining()) { + if (buf.get() == '\n') { + newVal = newVal & ~FLAG_READING_TILL_END_OF_LINE; + break; + } + } + if (anyAreSet(newVal, FLAG_READING_TILL_END_OF_LINE)) { + return 0; + } + } + + //we have our chunk size, check to make sure it was not the last chunk + if (allAreClear(newVal, FLAG_READING_NEWLINE | FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE) && chunkRemaining == 0) { + newVal |= FLAG_READING_AFTER_LAST; + int ret = handleChunkedRequestEnd(buf); + if (ret == -1) { + newVal |= FLAG_FINISHED & ~FLAG_READING_AFTER_LAST; + return -1; + } + return 0; + } + return chunkRemaining; + } finally { + state = newVal | chunkRemaining; + + if (allAreClear(oldVal, FLAG_FINISHED) && allAreSet(newVal, FLAG_FINISHED)) { + if (finishListener != null) { + finishListener.handleEvent(conduit); + } + } + } + } + + public long getChunkRemaining() { + if (anyAreSet(state, FLAG_FINISHED)) { + return -1; + } + if(anyAreSet(state, FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE | FLAG_READING_NEWLINE | FLAG_READING_AFTER_LAST)) { + return 0; + } + return state & MASK_COUNT; + } + + public void setChunkRemaining(final long remaining) { + if (remaining < 0 || anyAreSet(state, FLAG_READING_LENGTH | FLAG_READING_TILL_END_OF_LINE | FLAG_READING_NEWLINE | FLAG_READING_AFTER_LAST)) { + return; + } + long old = state; + long oldRemaining = old & MASK_COUNT; + if (remaining == 0 && oldRemaining != 0) { + //if oldRemaining is zero it could be that no data has been read yet + //and the correct state is READING_LENGTH + old |= FLAG_READING_NEWLINE; + } + state = (old & ~MASK_COUNT) | remaining; + } + + private int handleChunkedRequestEnd(ByteBuffer buffer) throws IOException { + if (trailerParser != null) { + return trailerParser.handle(buffer); + } + while (buffer.hasRemaining()) { + byte b = buffer.get(); + if (b == '\n') { + return -1; + } else if (b != '\r') { + buffer.position(buffer.position() - 1); + trailerParser = new TrailerParser(); + return trailerParser.handle(buffer); + } + } + return 0; + } + + /** + * Class that parses HTTP trailers. We don't just re-use the http parser code because it is complicated enough + * already, and this is not used very often so the performance benefits should not matter. + */ + private final class TrailerParser { + + private HeaderMap headerMap = new HeaderMap(); + private StringBuilder builder = new StringBuilder(); + private HttpString httpString; + int state = 0; + + private static final int STATE_TRAILER_NAME = 0; + private static final int STATE_TRAILER_VALUE = 1; + private static final int STATE_ENDING = 2; + + + public int handle(ByteBuffer buf) throws IOException { + while (buf.hasRemaining()) { + final byte b = buf.get(); + if (state == STATE_TRAILER_NAME) { + if (b == '\r') { + if (builder.length() == 0) { + state = STATE_ENDING; + } else { + throw UndertowMessages.MESSAGES.couldNotDecodeTrailers(); + } + } else if (b == '\n') { + if (builder.length() == 0) { + attachable.putAttachment(trailerAttachmentKey, headerMap); + return -1; + } else { + throw UndertowMessages.MESSAGES.couldNotDecodeTrailers(); + } + } else if (b == ':') { + httpString = HttpString.tryFromString(builder.toString().trim()); + state = STATE_TRAILER_VALUE; + builder.setLength(0); + } else { + builder.append((char) b); + } + } else if (state == STATE_TRAILER_VALUE) { + if (b == '\n') { + headerMap.put(httpString, builder.toString().trim()); + httpString = null; + builder.setLength(0); + state = STATE_TRAILER_NAME; + } else if (b != '\r') { + builder.append((char) b); + } + } else if (state == STATE_ENDING) { + if (b == '\n') { + if (attachable != null) { + attachable.putAttachment(trailerAttachmentKey, headerMap); + } + return -1; + } else { + throw UndertowMessages.MESSAGES.couldNotDecodeTrailers(); + } + } else { + throw new IllegalStateException(); + } + } + return 0; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/conduits/ChunkedStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/ChunkedStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/ChunkedStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,396 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.http.HttpAttachments; +import io.undertow.util.Attachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.ImmediatePooled; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * Channel that implements HTTP chunked transfer coding. + * + * @author Stuart Douglas + */ +public class ChunkedStreamSinkConduit extends AbstractStreamSinkConduit { + + /** + * Trailers that are to be attached to the end of the HTTP response. Note that it is the callers responsibility + * to make sure the client understands trailers (i.e. they have provided a TE header), and to set the 'Trailers:' + * header appropriately. + *

+ * This attachment must be set before the {@link #terminateWrites()} method is called. + */ + @Deprecated + public static final AttachmentKey TRAILERS = HttpAttachments.RESPONSE_TRAILERS; + + private final HeaderMap responseHeaders; + + private final ConduitListener finishListener; + private final int config; + + private final Pool bufferPool; + + /** + * "0\r\n" as bytes in US ASCII encoding. + */ + private static final byte[] LAST_CHUNK = new byte[] {(byte) 48, (byte) 13, (byte) 10}; + + /** + * "\r\n" as bytes in US ASCII encoding. + */ + private static final byte[] CRLF = new byte[] {(byte) 13, (byte) 10}; + + private final Attachable attachable; + private int state; + private int chunkleft = 0; + + private final ByteBuffer chunkingBuffer = ByteBuffer.allocate(12); //12 is the most + private final ByteBuffer chunkingSepBuffer; + private Pooled lastChunkBuffer; + + + private static final int CONF_FLAG_CONFIGURABLE = 1 << 0; + private static final int CONF_FLAG_PASS_CLOSE = 1 << 1; + + /** + * Flag that is set when {@link #terminateWrites()} or @{link #close()} is called + */ + private static final int FLAG_WRITES_SHUTDOWN = 1; + private static final int FLAG_NEXT_SHUTDOWN = 1 << 2; + private static final int FLAG_WRITTEN_FIRST_CHUNK = 1 << 3; + private static final int FLAG_FIRST_DATA_WRITTEN = 1 << 4; //set on first flush or write call + private static final int FLAG_FINISHED = 1 << 5; + + int written = 0; + + /** + * Construct a new instance. + * + * @param next the channel to wrap + * @param configurable {@code true} to allow configuration of the next channel, {@code false} otherwise + * @param passClose {@code true} to close the underlying channel when this channel is closed, {@code false} otherwise + * @param responseHeaders The response headers + * @param finishListener The finish listener + * @param attachable The attachable + */ + public ChunkedStreamSinkConduit(final StreamSinkConduit next, final Pool bufferPool, final boolean configurable, final boolean passClose, HeaderMap responseHeaders, final ConduitListener finishListener, final Attachable attachable) { + super(next); + this.bufferPool = bufferPool; + this.responseHeaders = responseHeaders; + this.finishListener = finishListener; + this.attachable = attachable; + config = (configurable ? CONF_FLAG_CONFIGURABLE : 0) | (passClose ? CONF_FLAG_PASS_CLOSE : 0); + chunkingSepBuffer = ByteBuffer.allocate(2); + chunkingSepBuffer.flip(); + } + + @Override + public int write(final ByteBuffer src) throws IOException { + return doWrite(src); + } + + + int doWrite(final ByteBuffer src) throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + throw new ClosedChannelException(); + } + if(src.remaining() == 0) { + return 0; + } + this.state |= FLAG_FIRST_DATA_WRITTEN; + int oldLimit = src.limit(); + if (chunkleft == 0 && !chunkingSepBuffer.hasRemaining()) { + chunkingBuffer.clear(); + written += src.remaining(); + putIntAsHexString(chunkingBuffer, src.remaining()); + chunkingBuffer.put(CRLF); + chunkingBuffer.flip(); + chunkingSepBuffer.clear(); + chunkingSepBuffer.put(CRLF); + chunkingSepBuffer.flip(); + state |= FLAG_WRITTEN_FIRST_CHUNK; + chunkleft = src.remaining(); + } else { + if (src.remaining() > chunkleft) { + src.limit(chunkleft + src.position()); + } + } + try { + int chunkingSize = chunkingBuffer.remaining(); + int chunkingSepSize = chunkingSepBuffer.remaining(); + if (chunkingSize > 0 || chunkingSepSize > 0 || lastChunkBuffer != null) { + int originalRemaining = src.remaining(); + long result; + if (lastChunkBuffer == null) { + final ByteBuffer[] buf = new ByteBuffer[]{chunkingBuffer, src, chunkingSepBuffer}; + result = next.write(buf, 0, buf.length); + } else { + final ByteBuffer[] buf = new ByteBuffer[]{chunkingBuffer, src, lastChunkBuffer.getResource()}; + if (anyAreSet(state, CONF_FLAG_PASS_CLOSE)) { + result = next.writeFinal(buf, 0, buf.length); + } else { + result = next.write(buf, 0, buf.length); + } + if (!src.hasRemaining()) { + state |= FLAG_WRITES_SHUTDOWN; + } + if (!lastChunkBuffer.getResource().hasRemaining()) { + state |= FLAG_NEXT_SHUTDOWN; + lastChunkBuffer.free(); + } + } + int srcWritten = originalRemaining - src.remaining(); + chunkleft -= srcWritten; + if (result < chunkingSize) { + return 0; + } else { + return srcWritten; + } + } else { + int result = next.write(src); + chunkleft -= result; + return result; + + } + } finally { + src.limit(oldLimit); + } + + } + + @Override + public void truncateWrites() throws IOException { + if(lastChunkBuffer != null) { + lastChunkBuffer.free(); + } + super.truncateWrites(); + } + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + for (int i = offset; i < length; ++i) { + if (srcs[i].hasRemaining()) { + return write(srcs[i]); + } + } + return 0; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + //todo: we could optimise this to just set a content length if no data has been written + if(!src.hasRemaining()) { + terminateWrites(); + return 0; + } + if (lastChunkBuffer == null) { + createLastChunk(true); + } + return doWrite(src); + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + throw new ClosedChannelException(); + } + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + throw new ClosedChannelException(); + } + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public boolean flush() throws IOException { + this.state |= FLAG_FIRST_DATA_WRITTEN; + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + if (anyAreSet(state, FLAG_NEXT_SHUTDOWN)) { + boolean val = next.flush(); + if (val && allAreClear(state, FLAG_FINISHED)) { + invokeFinishListener(); + } + return val; + } else { + next.write(lastChunkBuffer.getResource()); + if (!lastChunkBuffer.getResource().hasRemaining()) { + lastChunkBuffer.free(); + if (anyAreSet(config, CONF_FLAG_PASS_CLOSE)) { + next.terminateWrites(); + } + state |= FLAG_NEXT_SHUTDOWN; + boolean val = next.flush(); + if (val && allAreClear(state, FLAG_FINISHED)) { + invokeFinishListener(); + } + return val; + } else { + return false; + } + } + } else { + return next.flush(); + } + } + + private void invokeFinishListener() { + state |= FLAG_FINISHED; + if (finishListener != null) { + finishListener.handleEvent(this); + } + } + + @Override + public void terminateWrites() throws IOException { + if(anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + return; + } + if (this.chunkleft != 0) { + throw UndertowMessages.MESSAGES.chunkedChannelClosedMidChunk(); + } + if (!anyAreSet(state, FLAG_FIRST_DATA_WRITTEN)) { + //if no data was actually sent we just remove the transfer encoding header, and set content length 0 + //TODO: is this the best way to do it? + //todo: should we make this behaviour configurable? + responseHeaders.put(Headers.CONTENT_LENGTH, "0"); //according to the spec we don't actually need this, but better to be safe + responseHeaders.remove(Headers.TRANSFER_ENCODING); + state |= FLAG_NEXT_SHUTDOWN | FLAG_WRITES_SHUTDOWN; + if(anyAreSet(state, CONF_FLAG_PASS_CLOSE)) { + next.terminateWrites(); + } + } else { + createLastChunk(false); + state |= FLAG_WRITES_SHUTDOWN; + } + } + + private void createLastChunk(final boolean writeFinal) throws UnsupportedEncodingException { + Pooled lastChunkBufferPooled = bufferPool.allocate(); + ByteBuffer lastChunkBuffer = lastChunkBufferPooled.getResource(); + if (writeFinal) { + lastChunkBuffer.put(CRLF); + } else if(chunkingSepBuffer.hasRemaining()) { + //the end of chunk /r/n has not been written yet + //just add it to this buffer to make managing state easier + lastChunkBuffer.put(chunkingSepBuffer); + } + lastChunkBuffer.put(LAST_CHUNK); + //we just assume it will fit + HeaderMap trailers = attachable.getAttachment(HttpAttachments.RESPONSE_TRAILERS); + if (trailers != null && trailers.size() != 0) { + for (HeaderValues trailer : trailers) { + for (String val : trailer) { + trailer.getHeaderName().appendTo(lastChunkBuffer); + lastChunkBuffer.put((byte) ':'); + lastChunkBuffer.put((byte) ' '); + lastChunkBuffer.put(val.getBytes("US-ASCII")); + lastChunkBuffer.put(CRLF); + } + } + lastChunkBuffer.put(CRLF); + } else { + lastChunkBuffer.put(CRLF); + } + //horrible hack + //there is a situation where we can get a buffer leak here if the connection is terminated abnormaly + //this should be fixed once this channel has its lifecycle tied to the connection, same as fixed length + lastChunkBuffer.flip(); + ByteBuffer data = ByteBuffer.allocate(lastChunkBuffer.remaining()); + data.put(lastChunkBuffer); + data.flip(); + this.lastChunkBuffer = new ImmediatePooled<>(data); + + lastChunkBufferPooled.free(); + } + + @Override + public void awaitWritable() throws IOException { + next.awaitWritable(); + } + + @Override + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + next.awaitWritable(time, timeUnit); + } + + private static void putIntAsHexString(final ByteBuffer buf, final int v) { + byte int3 = (byte) (v >> 24); + byte int2 = (byte) (v >> 16); + byte int1 = (byte) (v >> 8); + byte int0 = (byte) (v ); + boolean nonZeroFound = false; + if (int3 != 0) { + buf.put(DIGITS[(0xF0 & int3) >>> 4]) + .put(DIGITS[0x0F & int3]); + nonZeroFound = true; + } + if (nonZeroFound || int2 != 0) { + buf.put(DIGITS[(0xF0 & int2) >>> 4]) + .put(DIGITS[0x0F & int2]); + nonZeroFound = true; + } + if (nonZeroFound || int1 != 0) { + buf.put(DIGITS[(0xF0 & int1) >>> 4]) + .put(DIGITS[0x0F & int1]); + } + buf.put(DIGITS[(0xF0 & int0) >>> 4]) + .put(DIGITS[0x0F & int0]); + } + + /** + * hexadecimal digits "0123456789abcdef" as bytes in US ASCII encoding. + */ + private static final byte[] DIGITS = new byte[] { + (byte) 48, (byte) 49, (byte) 50, (byte) 51, (byte) 52, (byte) 53, + (byte) 54, (byte) 55, (byte) 56, (byte) 57, (byte) 97, (byte) 98, + (byte) 99, (byte) 100, (byte) 101, (byte) 102}; + +} Index: 3rdParty_sources/undertow/io/undertow/conduits/ChunkedStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/ChunkedStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/ChunkedStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,263 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowMessages; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.protocol.http.HttpAttachments; +import io.undertow.server.protocol.http.HttpServerConnection; +import io.undertow.util.Attachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderMap; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.ConduitReadableByteChannel; +import org.xnio.conduits.PushBackStreamSourceConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; + +/** + * Channel to de-chunkify data + * + * @author Stuart Douglas + */ +public class ChunkedStreamSourceConduit extends AbstractStreamSourceConduit { + + /** + * If the response has HTTP footers they are attached to the exchange under this key. They will only be available once the exchange has been fully read. + */ + @Deprecated + public static final AttachmentKey TRAILERS = HttpAttachments.REQUEST_TRAILERS; + + private final BufferWrapper bufferWrapper; + private final ConduitListener finishListener; + private final HttpServerExchange exchange; + + private boolean closed; + + private long remainingAllowed; + private final ChunkReader chunkReader; + + public ChunkedStreamSourceConduit(final StreamSourceConduit next, final PushBackStreamSourceConduit channel, final Pool pool, final ConduitListener finishListener, Attachable attachable) { + this(next, new BufferWrapper() { + @Override + public Pooled allocate() { + return pool.allocate(); + } + + @Override + public void pushBack(Pooled pooled) { + channel.pushBack(pooled); + } + }, finishListener, attachable, null); + } + + public ChunkedStreamSourceConduit(final StreamSourceConduit next, final HttpServerExchange exchange, final ConduitListener finishListener) { + this(next, new BufferWrapper() { + @Override + public Pooled allocate() { + return exchange.getConnection().getBufferPool().allocate(); + } + + @Override + public void pushBack(Pooled pooled) { + ((HttpServerConnection) exchange.getConnection()).ungetRequestBytes(pooled); + } + }, finishListener, exchange, exchange); + } + + protected ChunkedStreamSourceConduit(final StreamSourceConduit next, final BufferWrapper bufferWrapper, final ConduitListener finishListener, final Attachable attachable, final HttpServerExchange exchange) { + super(next); + this.bufferWrapper = bufferWrapper; + this.finishListener = finishListener; + this.remainingAllowed = Long.MIN_VALUE; + this.chunkReader = new ChunkReader<>(attachable, HttpAttachments.REQUEST_TRAILERS, finishListener, this); + this.exchange = exchange; + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + return target.transferFrom(new ConduitReadableByteChannel(this), position, count); + } + + private void updateRemainingAllowed(final int written) throws IOException { + if (remainingAllowed == Long.MIN_VALUE) { + if (exchange == null) { + return; + } else { + long maxEntitySize = exchange.getMaxEntitySize(); + if (maxEntitySize <= 0) { + return; + } + remainingAllowed = maxEntitySize; + } + } + remainingAllowed -= written; + if (remainingAllowed < 0) { + //max entity size is exceeded + Connectors.terminateRequest(exchange); + closed = true; + exchange.setPersistent(false); + finishListener.handleEvent(this); + throw UndertowMessages.MESSAGES.requestEntityWasTooLarge(exchange.getMaxEntitySize()); + } + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + return IoUtils.transfer(new ConduitReadableByteChannel(this), count, throughBuffer, target); + } + + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + for (int i = offset; i < length; ++i) { + if (dsts[i].hasRemaining()) { + return read(dsts[i]); + } + } + return 0; + } + + @Override + public void terminateReads() throws IOException { + if (!isFinished()) { + super.terminateReads(); + throw UndertowMessages.MESSAGES.chunkedChannelClosedMidChunk(); + } + } + + public int read(final ByteBuffer dst) throws IOException { + long chunkRemaining = chunkReader.getChunkRemaining(); + //we have read the last chunk, we just return EOF + if (chunkRemaining == -1) { + return -1; + } + if (closed) { + throw new ClosedChannelException(); + } + Pooled pooled = bufferWrapper.allocate(); + ByteBuffer buf = pooled.getResource(); + try { + int r = next.read(buf); + buf.flip(); + if (r == -1) { + //Channel is broken, not sure how best to report it + throw new ClosedChannelException(); + } else if (r == 0) { + return 0; + } + if (chunkRemaining == 0) { + chunkRemaining = chunkReader.readChunk(buf); + if (chunkRemaining <= 0) { + return (int) chunkRemaining; + } + } + + + final int originalLimit = dst.limit(); + try { + //now we may have some stuff in the raw buffer + //or the raw buffer may be exhausted, and we should read directly into the destination buffer + //from the next + + int read = 0; + long chunkInBuffer = Math.min(buf.remaining(), chunkRemaining); + int remaining = dst.remaining(); + if (chunkInBuffer > remaining) { + //it won't fit + int orig = buf.limit(); + buf.limit(buf.position() + remaining); + dst.put(buf); + buf.limit(orig); + chunkRemaining -= remaining; + updateRemainingAllowed(remaining); + return remaining; + } else if (buf.hasRemaining()) { + int old = buf.limit(); + buf.limit((int) Math.min(old, buf.position() + chunkInBuffer)); + try { + dst.put(buf); + } finally { + buf.limit(old); + } + read += chunkInBuffer; + chunkRemaining -= chunkInBuffer; + } + //there is still more to read + //we attempt to just read it directly into the destination buffer + //adjusting the limit as necessary to make sure we do not read too much + if (chunkRemaining > 0) { + int old = dst.limit(); + try { + if (chunkRemaining < dst.remaining()) { + dst.limit((int) (dst.position() + chunkRemaining)); + } + int c = 0; + do { + c = next.read(dst); + if (c > 0) { + read += c; + chunkRemaining -= c; + } + } while (c > 0 && chunkRemaining > 0); + if (c == -1) { + throw new ClosedChannelException(); + } + } finally { + dst.limit(old); + } + } + updateRemainingAllowed(read); + return read; + + } finally { + //buffer will be freed if not needed in exitRead + dst.limit(originalLimit); + } + + } finally { + if(chunkRemaining >= 0) { + chunkReader.setChunkRemaining(chunkRemaining); + } + if (buf.hasRemaining()) { + bufferWrapper.pushBack(pooled); + } else { + pooled.free(); + } + } + + } + + public boolean isFinished() { + return chunkReader.getChunkRemaining() == -1; + } + + interface BufferWrapper { + + Pooled allocate(); + + void pushBack(Pooled pooled); + + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/ConduitListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/ConduitListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/ConduitListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import java.util.EventListener; + +import org.xnio.conduits.Conduit; + +/** + * @author Stuart Douglas + */ +public interface ConduitListener extends EventListener { + + /** + * Handle the event on this conduit. + * + * @param channel the channel event + */ + void handleEvent(T channel); +} Index: 3rdParty_sources/undertow/io/undertow/conduits/DebuggingStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/DebuggingStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/DebuggingStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,114 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Conduit that saves all the data that is written through it and can dump it to the console + *

+ * Obviously this should not be used in production. + * + * @author Stuart Douglas + */ +public class DebuggingStreamSinkConduit extends AbstractStreamSinkConduit { + + private static final List data = new CopyOnWriteArrayList<>(); + + /** + * Construct a new instance. + * + * @param next the delegate conduit to set + */ + public DebuggingStreamSinkConduit(StreamSinkConduit next) { + super(next); + } + + @Override + public int write(ByteBuffer src) throws IOException { + int pos = src.position(); + int res = super.write(src); + if (res > 0) { + byte[] d = new byte[res]; + for (int i = 0; i < res; ++i) { + d[i] = src.get(i + pos); + } + data.add(d); + } + return res; + } + + @Override + public long write(ByteBuffer[] dsts, int offs, int len) throws IOException { + for (int i = offs; i < len; ++i) { + if (dsts[i].hasRemaining()) { + return write(dsts[i]); + } + } + return 0; + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + public static void dump() { + + for (int i = 0; i < data.size(); ++i) { + System.out.println("Write Buffer " + i); + StringBuilder sb = new StringBuilder(); + try { + Buffers.dump(ByteBuffer.wrap(data.get(i)), sb, 0, 20); + } catch (IOException e) { + new RuntimeException(e); + } + System.out.println(sb); + System.out.println(); + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/DebuggingStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/DebuggingStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/DebuggingStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,101 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.ConduitReadableByteChannel; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Conduit that saves all the data that is written through it and can dump it to the console + *

+ * Obviously this should not be used in production. + * + * @author Stuart Douglas + */ +public class DebuggingStreamSourceConduit extends AbstractStreamSourceConduit { + + private static final List data = new CopyOnWriteArrayList<>(); + + /** + * Construct a new instance. + * + * @param next the delegate conduit to set + */ + public DebuggingStreamSourceConduit(StreamSourceConduit next) { + super(next); + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + return target.transferFrom(new ConduitReadableByteChannel(this), position, count); + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + return IoUtils.transfer(new ConduitReadableByteChannel(this), count, throughBuffer, target); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int pos = dst.position(); + int res = super.read(dst); + if (res > 0) { + byte[] d = new byte[res]; + for (int i = 0; i < res; ++i) { + d[i] = dst.get(i + pos); + } + data.add(d); + } + return res; + } + + @Override + public long read(ByteBuffer[] dsts, int offs, int len) throws IOException { + for (int i = offs; i < len; ++i) { + if (dsts[i].hasRemaining()) { + return read(dsts[i]); + } + } + return 0; + } + + public static void dump() { + + for (int i = 0; i < data.size(); ++i) { + System.out.println("Buffer " + i); + StringBuilder sb = new StringBuilder(); + try { + Buffers.dump(ByteBuffer.wrap(data.get(i)), sb, 0, 20); + } catch (IOException e) { + new RuntimeException(e); + } + System.out.println(sb); + System.out.println(); + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/DeflatingStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/DeflatingStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/DeflatingStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,490 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreSet; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; +import java.util.zip.Deflater; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.WriteReadyHandler; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConduitFactory; +import io.undertow.util.Headers; + +/** + * Channel that handles deflate compression + * + * @author Stuart Douglas + */ +public class DeflatingStreamSinkConduit implements StreamSinkConduit { + + protected final Deflater deflater; + private final ConduitFactory conduitFactory; + private final HttpServerExchange exchange; + + private StreamSinkConduit next; + private WriteReadyHandler writeReadyHandler; + + + /** + * The streams buffer. This is freed when the next is shutdown + */ + protected Pooled currentBuffer; + /** + * there may have been some additional data that did not fit into the first buffer + */ + private ByteBuffer additionalBuffer; + + private int state = 0; + + private static final int SHUTDOWN = 1; + private static final int NEXT_SHUTDOWN = 1 << 1; + private static final int FLUSHING_BUFFER = 1 << 2; + private static final int WRITES_RESUMED = 1 << 3; + private static final int CLOSED = 1 << 4; + private static final int WRITTEN_TRAILER = 1 << 5; + + public DeflatingStreamSinkConduit(final ConduitFactory conduitFactory, final HttpServerExchange exchange) { + this(conduitFactory, exchange, Deflater.DEFLATED); + } + + protected DeflatingStreamSinkConduit(final ConduitFactory conduitFactory, final HttpServerExchange exchange, int deflateLevel) { + deflater = new Deflater(deflateLevel, true); + this.currentBuffer = exchange.getConnection().getBufferPool().allocate(); + this.exchange = exchange; + this.conduitFactory = conduitFactory; + } + + @Override + public int write(final ByteBuffer src) throws IOException { + if (anyAreSet(state, SHUTDOWN | CLOSED) || currentBuffer == null) { + throw new ClosedChannelException(); + } + try { + if (!performFlushIfRequired()) { + return 0; + } + if (src.remaining() == 0) { + return 0; + } + //we may already have some input, if so compress it + if (!deflater.needsInput()) { + deflateData(); + if (!deflater.needsInput()) { + return 0; + } + } + byte[] data = new byte[src.remaining()]; + src.get(data); + preDeflate(data); + deflater.setInput(data); + deflateData(); + return data.length; + } catch (IOException e) { + freeBuffer(); + throw e; + } + } + + protected void preDeflate(byte[] data) { + + } + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if (anyAreSet(state, SHUTDOWN | CLOSED) || currentBuffer == null) { + throw new ClosedChannelException(); + } + try { + int total = 0; + for (int i = offset; i < offset + length; ++i) { + if (srcs[i].hasRemaining()) { + int ret = write(srcs[i]); + total += ret; + if (ret == 0) { + return total; + } + } + } + return total; + } catch (IOException e) { + freeBuffer(); + throw e; + } + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (anyAreSet(state, SHUTDOWN | CLOSED)) { + throw new ClosedChannelException(); + } + if (!performFlushIfRequired()) { + return 0; + } + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (anyAreSet(state, SHUTDOWN | CLOSED)) { + throw new ClosedChannelException(); + } + if (!performFlushIfRequired()) { + return 0; + } + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public XnioWorker getWorker() { + return exchange.getConnection().getWorker(); + } + + @Override + public void suspendWrites() { + if (next == null) { + state = state & ~WRITES_RESUMED; + } else { + next.suspendWrites(); + } + } + + + @Override + public boolean isWriteResumed() { + if (next == null) { + return anyAreSet(state, WRITES_RESUMED); + } else { + return next.isWriteResumed(); + } + } + + @Override + public void wakeupWrites() { + if (next == null) { + resumeWrites(); + } else { + next.wakeupWrites(); + } + } + + @Override + public void resumeWrites() { + if (next == null) { + state |= WRITES_RESUMED; + queueWriteListener(); + } else { + next.resumeWrites(); + } + } + + private void queueWriteListener() { + exchange.getConnection().getIoThread().execute(new Runnable() { + @Override + public void run() { + if (writeReadyHandler != null) { + try { + writeReadyHandler.writeReady(); + } finally { + //if writes are still resumed queue up another one + if (next == null && isWriteResumed()) { + queueWriteListener(); + } + } + } + } + }); + } + + @Override + public void terminateWrites() throws IOException { + deflater.finish(); + state |= SHUTDOWN; + } + + @Override + public boolean isWriteShutdown() { + return anyAreSet(state, SHUTDOWN); + } + + @Override + public void awaitWritable() throws IOException { + if (next == null) { + return; + } else { + next.awaitWritable(); + } + } + + @Override + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + if (next == null) { + return; + } else { + next.awaitWritable(time, timeUnit); + } + } + + @Override + public XnioIoThread getWriteThread() { + return exchange.getConnection().getIoThread(); + } + + @Override + public void setWriteReadyHandler(final WriteReadyHandler handler) { + this.writeReadyHandler = handler; + } + + @Override + public boolean flush() throws IOException { + if (currentBuffer == null) { + if (anyAreSet(state, NEXT_SHUTDOWN)) { + return next.flush(); + } else { + return true; + } + } + try { + boolean nextCreated = false; + try { + if (anyAreSet(state, SHUTDOWN)) { + if (anyAreSet(state, NEXT_SHUTDOWN)) { + return next.flush(); + } else { + if (!performFlushIfRequired()) { + return false; + } + //if the deflater has not been fully flushed we need to flush it + if (!deflater.finished()) { + deflateData(); + //if could not fully flush + if (!deflater.finished()) { + return false; + } + } + final ByteBuffer buffer = currentBuffer.getResource(); + if (allAreClear(state, WRITTEN_TRAILER)) { + state |= WRITTEN_TRAILER; + byte[] data = getTrailer(); + if (data != null) { + if (data.length <= buffer.remaining()) { + buffer.put(data); + } else if (additionalBuffer == null) { + additionalBuffer = ByteBuffer.wrap(data); + } else { + byte[] newData = new byte[additionalBuffer.remaining() + data.length]; + int pos = 0; + while (additionalBuffer.hasRemaining()) { + newData[pos++] = additionalBuffer.get(); + } + for (byte aData : data) { + newData[pos++] = aData; + } + this.additionalBuffer = ByteBuffer.wrap(newData); + } + } + } + + //ok the deflater is flushed, now we need to flush the buffer + if (!anyAreSet(state, FLUSHING_BUFFER)) { + buffer.flip(); + state |= FLUSHING_BUFFER; + if (next == null) { + nextCreated = true; + this.next = createNextChannel(); + } + } + if (performFlushIfRequired()) { + state |= NEXT_SHUTDOWN; + freeBuffer(); + next.terminateWrites(); + return next.flush(); + } else { + return false; + } + } + } else { + return performFlushIfRequired(); + } + } finally { + if (nextCreated) { + if (anyAreSet(state, WRITES_RESUMED) && !anyAreSet(state ,NEXT_SHUTDOWN)) { + try { + next.resumeWrites(); + } catch (Exception e) { + UndertowLogger.REQUEST_LOGGER.debug("Failed to resume", e); + } + } + } + } + } catch (IOException e) { + freeBuffer(); + throw e; + } + } + + /** + * called before the stream is finally flushed. + */ + protected byte[] getTrailer() { + return null; + } + + /** + * The we are in the flushing state then we flush to the underlying stream, otherwise just return true + * + * @return false if there is still more to flush + */ + private boolean performFlushIfRequired() throws IOException { + if (anyAreSet(state, FLUSHING_BUFFER)) { + final ByteBuffer[] bufs = new ByteBuffer[additionalBuffer == null ? 1 : 2]; + long totalLength = 0; + bufs[0] = currentBuffer.getResource(); + totalLength += bufs[0].remaining(); + if (additionalBuffer != null) { + bufs[1] = additionalBuffer; + totalLength += bufs[1].remaining(); + } + if (totalLength > 0) { + long total = 0; + long res = 0; + do { + res = next.write(bufs, 0, bufs.length); + total += res; + if (res == 0) { + return false; + } + } while (total < totalLength); + } + additionalBuffer = null; + currentBuffer.getResource().clear(); + state = state & ~FLUSHING_BUFFER; + } + return true; + } + + + private StreamSinkConduit createNextChannel() { + if (deflater.finished() && allAreSet(state, WRITTEN_TRAILER)) { + //the deflater was fully flushed before we created the channel. This means that what is in the buffer is + //all there is + int remaining = currentBuffer.getResource().remaining(); + if (additionalBuffer != null) { + remaining += additionalBuffer.remaining(); + } + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, Integer.toString(remaining)); + } else { + exchange.getResponseHeaders().remove(Headers.CONTENT_LENGTH); + } + return conduitFactory.create(); + } + + /** + * Runs the current data through the deflater. As much as possible this will be buffered in the current output + * stream. + * + * @throws IOException + */ + private void deflateData() throws IOException { + //we don't need to flush here, as this should have been called already by the time we get to + //this point + boolean nextCreated = false; + try { + Pooled pooled = this.currentBuffer; + final ByteBuffer outputBuffer = pooled.getResource(); + + final boolean shutdown = anyAreSet(state, SHUTDOWN); + + byte[] buffer = new byte[1024]; //TODO: we should pool this and make it configurable or something + while (!deflater.needsInput() || (shutdown && !deflater.finished())) { + int count = deflater.deflate(buffer); + if (count != 0) { + int remaining = outputBuffer.remaining(); + if (remaining > count) { + outputBuffer.put(buffer, 0, count); + } else { + if (remaining == count) { + outputBuffer.put(buffer, 0, count); + } else { + outputBuffer.put(buffer, 0, remaining); + additionalBuffer = ByteBuffer.wrap(buffer, remaining, count - remaining); + } + outputBuffer.flip(); + this.state |= FLUSHING_BUFFER; + if (next == null) { + nextCreated = true; + this.next = createNextChannel(); + } + if (!performFlushIfRequired()) { + return; + } + } + } + } + } finally { + if (nextCreated) { + if (anyAreSet(state, WRITES_RESUMED)) { + next.resumeWrites(); + } + } + } + } + + + @Override + public void truncateWrites() throws IOException { + freeBuffer(); + state |= CLOSED; + next.truncateWrites(); + } + + private void freeBuffer() { + if (currentBuffer != null) { + currentBuffer.free(); + currentBuffer = null; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/EmptyStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/EmptyStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/EmptyStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,131 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.ReadReadyHandler; +import org.xnio.conduits.StreamSourceConduit; + +/** + * A stream source conduit which is always empty. + * + * Temporary copy from XNIO, see https://issues.jboss.org/browse/XNIO-199 + * + * @author David M. Lloyd + */ +public final class EmptyStreamSourceConduit implements StreamSourceConduit { + private final XnioWorker worker; + private final XnioIoThread readThread; + private ReadReadyHandler readReadyHandler; + private boolean shutdown; + private boolean resumed; + + /** + * Construct a new instance. + * + * @param readThread the read thread for this conduit + */ + public EmptyStreamSourceConduit(final XnioIoThread readThread) { + this.worker = readThread.getWorker(); + this.readThread = readThread; + } + + public void setReadReadyHandler(final ReadReadyHandler handler) { + readReadyHandler = handler; + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + return 0; + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + resumed = false; + return -1L; + } + + public int read(final ByteBuffer dst) throws IOException { + resumed = false; + return -1; + } + + public long read(final ByteBuffer[] dsts, final int offs, final int len) throws IOException { + resumed = false; + return -1L; + } + + public boolean isReadShutdown() { + return shutdown; + } + + public void resumeReads() { + resumed = true; + readThread.execute(new Runnable() { + public void run() { + final ReadReadyHandler handler = readReadyHandler; + if (handler != null) { + handler.readReady(); + } + } + }); + } + + public void suspendReads() { + resumed = false; + } + + public void wakeupReads() { + resumeReads(); + } + + public boolean isReadResumed() { + return resumed; + } + + public void awaitReadable() throws IOException { + // always ready + } + + public void awaitReadable(final long time, final TimeUnit timeUnit) throws IOException { + // always ready + } + + public void terminateReads() throws IOException { + if (! shutdown) { + shutdown = true; + if(readReadyHandler != null) { + readReadyHandler.terminated(); + } + } + } + + public XnioIoThread getReadThread() { + return readThread; + } + + public XnioWorker getWorker() { + return worker; + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/FinishableStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/FinishableStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/FinishableStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,90 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.xnio.Buffers; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.StreamSinkConduit; + +/** + * @author David M. Lloyd + */ +public final class FinishableStreamSinkConduit extends AbstractStreamSinkConduit { + private final ConduitListener finishListener; + + //0 = open + //1 = writes shutdown + //2 = finish listener invoked + private int shutdownState = 0; + + public FinishableStreamSinkConduit(final StreamSinkConduit delegate, final ConduitListener finishListener) { + super(delegate); + this.finishListener = finishListener; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + int res = next.writeFinal(src); + if(!src.hasRemaining()) { + if (shutdownState == 0) { + shutdownState = 1; + } + } + return res; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + long res = next.writeFinal(srcs, offset, length); + if(!Buffers.hasRemaining(srcs, offset, length)) { + if (shutdownState == 0) { + shutdownState = 1; + } + } + return res; + } + + public void terminateWrites() throws IOException { + super.terminateWrites(); + if (shutdownState == 0) { + shutdownState = 1; + } + } + + @Override + public void truncateWrites() throws IOException { + next.truncateWrites(); + if (shutdownState != 2) { + shutdownState = 2; + finishListener.handleEvent(this); + } + } + + public boolean flush() throws IOException { + final boolean val = next.flush(); + if (val && shutdownState == 1) { + shutdownState = 2; + finishListener.handleEvent(this); + } + return val; + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/FinishableStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/FinishableStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/FinishableStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,94 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * A conduit that calls a finish listener when there is no data left in the underlying conduit. + * + * @author Stuart Douglas + */ +public final class FinishableStreamSourceConduit extends AbstractStreamSourceConduit { + + private final ConduitListener finishListener; + + private boolean finishCalled = false; + + public FinishableStreamSourceConduit(final StreamSourceConduit next, final ConduitListener finishListener) { + super(next); + this.finishListener = finishListener; + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + long res = 0; + try { + return res = next.transferTo(position, count, target); + } finally { + exitRead(res); + } + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + long res = 0; + try { + return res = next.transferTo(count, throughBuffer, target); + } finally { + exitRead(res); + } + } + + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + long res = 0; + try { + return res = next.read(dsts, offset, length); + } finally { + exitRead(res); + } + } + + public int read(final ByteBuffer dst) throws IOException { + int res = 0; + try { + return res = next.read(dst); + } finally { + exitRead(res); + } + } + + /** + * Exit a read method. + * + * @param consumed the number of bytes consumed by this call (may be 0) + */ + private void exitRead(long consumed) { + if (consumed == -1) { + if (!finishCalled) { + finishCalled = true; + finishListener.handleEvent(this); + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/FixedLengthStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/FixedLengthStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/FixedLengthStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,328 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowMessages; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import static java.lang.Math.min; +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreClear; +import static org.xnio.Bits.anyAreSet; +import static org.xnio.Bits.longBitMask; + +/** + * A channel which reads data of a fixed length and calls a finish listener. When the finish listener is called, + * it should examine the result of {@link #getRemaining()} to see if more bytes were pending when the channel was + * closed. + * + * @author David M. Lloyd + */ +/* + * Implementation notes + * -------------------- + * The {@code exhausted} flag is set once a method returns -1 and signifies that the read listener should no longer be + * called. The {@code finishListener} is called when remaining is reduced to 0 or when the channel is closed explicitly. + * If there are 0 remaining bytes but {@code FLAG_FINISHED} has not yet been set, the channel is considered "ready" until + * the EOF -1 value is read or the channel is closed. Since this is a half-duplex channel, shutting down reads is + * identical to closing the channel. + */ +public final class FixedLengthStreamSourceConduit extends AbstractStreamSourceConduit { + + private final ConduitListener finishListener; + + @SuppressWarnings("unused") + private long state; + + private static final long FLAG_CLOSED = 1L << 63L; + private static final long FLAG_FINISHED = 1L << 62L; + private static final long FLAG_LENGTH_CHECKED = 1L << 61L; + private static final long MASK_COUNT = longBitMask(0, 60); + + private final HttpServerExchange exchange; + + /** + * Construct a new instance. The given listener is called once all the bytes are read from the stream + * or the stream is closed. This listener should cause the remaining data to be drained from the + * underlying stream if the underlying stream is to be reused. + *

+ * Calling this constructor will replace the read listener of the underlying channel. The listener should be + * restored from the {@code finishListener} object. The underlying stream should not be closed while this wrapper + * stream is active. + * + * @param next the stream source channel to read from + * @param contentLength the amount of content to read + * @param finishListener the listener to call once the stream is exhausted or closed + * @param exchange The server exchange. This is used to determine the max size + */ + public FixedLengthStreamSourceConduit(final StreamSourceConduit next, final long contentLength, final ConduitListener finishListener, final HttpServerExchange exchange) { + super(next); + this.finishListener = finishListener; + if (contentLength < 0L) { + throw new IllegalArgumentException("Content length must be greater than or equal to zero"); + } else if (contentLength > MASK_COUNT) { + throw new IllegalArgumentException("Content length is too long"); + } + state = contentLength; + this.exchange = exchange; + } + + /** + * Construct a new instance. The given listener is called once all the bytes are read from the stream + * or the stream is closed. This listener should cause the remaining data to be drained from the + * underlying stream if the underlying stream is to be reused. + *

+ * Calling this constructor will replace the read listener of the underlying channel. The listener should be + * restored from the {@code finishListener} object. The underlying stream should not be closed while this wrapper + * stream is active. + * + * @param next the stream source channel to read from + * @param contentLength the amount of content to read + * @param finishListener the listener to call once the stream is exhausted or closed + */ + public FixedLengthStreamSourceConduit(final StreamSourceConduit next, final long contentLength, final ConduitListener finishListener) { + this(next, contentLength, finishListener, null); + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + long val = state; + checkMaxSize(val); + if (anyAreSet(val, FLAG_CLOSED | FLAG_FINISHED) || allAreClear(val, MASK_COUNT)) { + if (allAreClear(val, FLAG_FINISHED)) { + invokeFinishListener(); + } + return -1L; + } + long res = 0L; + try { + return res = next.transferTo(position, min(count, val & MASK_COUNT), target); + } finally { + exitRead(res); + } + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + if (count == 0L) { + return 0L; + } + long val = state; + checkMaxSize(val); + if (anyAreSet(val, FLAG_CLOSED | FLAG_FINISHED) || allAreClear(val, MASK_COUNT)) { + if (allAreClear(val, FLAG_FINISHED)) { + invokeFinishListener(); + } + return -1; + } + long res = 0L; + try { + return res = next.transferTo(min(count, val & MASK_COUNT), throughBuffer, target); + } finally { + exitRead(res == -1L ? val & MASK_COUNT : res + throughBuffer.remaining()); + } + } + + private void checkMaxSize(long state) throws IOException { + if (anyAreClear(state, FLAG_LENGTH_CHECKED)) { + HttpServerExchange exchange = this.exchange; + if (exchange != null) { + if (exchange.getMaxEntitySize() > 0 && exchange.getMaxEntitySize() < (state & MASK_COUNT)) { + //max entity size is exceeded + //we need to forcibly close the read side + Connectors.terminateRequest(exchange); + exchange.setPersistent(false); + finishListener.handleEvent(this); + this.state |= FLAG_FINISHED | FLAG_CLOSED; + throw UndertowMessages.MESSAGES.requestEntityWasTooLarge(exchange.getMaxEntitySize()); + } + } + this.state |= FLAG_LENGTH_CHECKED; + } + } + + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + if (length == 0) { + return 0L; + } else if (length == 1) { + return read(dsts[offset]); + } + long val = state; + checkMaxSize(val); + if (allAreSet(val, FLAG_CLOSED) || allAreClear(val, MASK_COUNT)) { + if (allAreClear(val, FLAG_FINISHED)) { + invokeFinishListener(); + } + return -1; + } + long res = 0L; + try { + if ((val & MASK_COUNT) == 0L) { + return -1L; + } + int lim; + // The total amount of buffer space discovered so far. + long t = 0L; + for (int i = 0; i < length; i++) { + final ByteBuffer buffer = dsts[i + offset]; + // Grow the discovered buffer space by the remaining size of the current buffer. + // We want to capture the limit so we calculate "remaining" ourselves. + t += (lim = buffer.limit()) - buffer.position(); + if (t > (val & MASK_COUNT)) { + // only read up to this point, and trim the last buffer by the number of extra bytes + buffer.limit(lim - (int) (t - (val & MASK_COUNT))); + try { + return res = next.read(dsts, offset, i + 1); + } finally { + // restore the original limit + buffer.limit(lim); + } + } + } + // the total buffer space is less than the remaining count. + return res = next.read(dsts, offset, length); + } finally { + exitRead(res == -1L ? val & MASK_COUNT : res); + } + } + + public long read(final ByteBuffer[] dsts) throws IOException { + return read(dsts, 0, dsts.length); + } + + public int read(final ByteBuffer dst) throws IOException { + long val = state; + checkMaxSize(val); + if (allAreSet(val, FLAG_CLOSED) || allAreClear(val, MASK_COUNT)) { + if (allAreClear(val, FLAG_FINISHED)) { + invokeFinishListener(); + } + return -1; + } + int res = 0; + final long remaining = val & MASK_COUNT; + try { + final int lim = dst.limit(); + final int pos = dst.position(); + if (lim - pos > remaining) { + dst.limit((int) (remaining + (long) pos)); + try { + return res = next.read(dst); + } finally { + dst.limit(lim); + } + } else { + return res = next.read(dst); + } + } finally { + exitRead(res == -1 ? remaining : (long) res); + } + } + + public boolean isReadResumed() { + return allAreClear(state, FLAG_CLOSED) && next.isReadResumed(); + } + + public void wakeupReads() { + long val = state; + if (anyAreSet(val, FLAG_CLOSED | FLAG_FINISHED) || allAreClear(val, MASK_COUNT)) { + return; + } + next.wakeupReads(); + } + + @Override + public void terminateReads() throws IOException { + long val = enterShutdownReads(); + if (allAreSet(val, FLAG_CLOSED)) { + return; + } + exitShutdownReads(val); + } + + public void awaitReadable() throws IOException { + final long val = state; + if (allAreSet(val, FLAG_CLOSED) || val == 0L) { + return; + } + next.awaitReadable(); + } + + public void awaitReadable(final long time, final TimeUnit timeUnit) throws IOException { + final long val = state; + if (allAreSet(val, FLAG_CLOSED) || val == 0L) { + return; + } + next.awaitReadable(time, timeUnit); + } + + /** + * Get the number of remaining bytes. + * + * @return the number of remaining bytes + */ + public long getRemaining() { + return state & MASK_COUNT; + } + + private long enterShutdownReads() { + long oldVal, newVal; + oldVal = state; + if (anyAreSet(oldVal, FLAG_CLOSED)) { + return oldVal; + } + newVal = oldVal | FLAG_CLOSED; + state = newVal; + return oldVal; + } + + private void exitShutdownReads(long oldVal) { + if (!allAreClear(oldVal, MASK_COUNT)) { + invokeFinishListener(); + } + } + + /** + * Exit a read method. + * + * @param consumed the number of bytes consumed by this call (may be 0) + */ + private void exitRead(long consumed) { + long oldVal = state; + long newVal = oldVal - consumed; + state = newVal; + if (anyAreSet(oldVal, MASK_COUNT) && allAreClear(newVal, MASK_COUNT)) { + invokeFinishListener(); + } + } + + private void invokeFinishListener() { + this.state |= FLAG_FINISHED; + finishListener.handleEvent(this); + } + +} Index: 3rdParty_sources/undertow/io/undertow/conduits/GzipStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/GzipStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/GzipStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,83 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.StreamSinkConduit; + +import java.util.zip.CRC32; +import java.util.zip.Deflater; + +/** + * @author Stuart Douglas + */ +public class GzipStreamSinkConduit extends DeflatingStreamSinkConduit { + + /* + * GZIP header magic number. + */ + private static final int GZIP_MAGIC = 0x8b1f; + + /** + * CRC-32 of uncompressed data. + */ + protected CRC32 crc = new CRC32(); + + public GzipStreamSinkConduit(ConduitFactory conduitFactory, HttpServerExchange exchange) { + super(conduitFactory, exchange, Deflater.DEFAULT_COMPRESSION); + writeHeader(); + } + + private void writeHeader() { + currentBuffer.getResource().put(new byte[]{ + (byte) GZIP_MAGIC, // Magic number (short) + (byte) (GZIP_MAGIC >> 8), // Magic number (short) + Deflater.DEFLATED, // Compression method (CM) + 0, // Flags (FLG) + 0, // Modification time MTIME (int) + 0, // Modification time MTIME (int) + 0, // Modification time MTIME (int) + 0, // Modification time MTIME (int) + 0, // Extra flags (XFLG) + 0 // Operating system (OS) + }); + } + + @Override + protected void preDeflate(byte[] data) { + crc.update(data); + } + + @Override + protected byte[] getTrailer() { + byte[] ret = new byte[8]; + int checksum = (int) crc.getValue(); + int total = deflater.getTotalIn(); + ret[0] = (byte) ((checksum) & 0xFF); + ret[1] = (byte) ((checksum >> 8) & 0xFF); + ret[2] = (byte) ((checksum >> 16) & 0xFF); + ret[3] = (byte) ((checksum >> 24) & 0xFF); + ret[4] = (byte) ((total) & 0xFF); + ret[5] = (byte) ((total >> 8) & 0xFF); + ret[6] = (byte) ((total >> 16) & 0xFF); + ret[7] = (byte) ((total >> 24) & 0xFF); + return ret; + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/HeadStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/HeadStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/HeadStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,185 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; + +import org.xnio.IoUtils; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * A conduit that discards all data written to it. This allows head requests to 'just work', as all data written + * will be discarded. + * + * @author Stuart Douglas + */ +public final class HeadStreamSinkConduit extends AbstractStreamSinkConduit { + + private final ConduitListener finishListener; + + private int state; + + private static final int FLAG_CLOSE_REQUESTED = 1; + private static final int FLAG_CLOSE_COMPLETE = 1 << 1; + private static final int FLAG_FINISHED_CALLED = 1 << 2; + + /** + * Construct a new instance. + * + * @param next the next channel + * @param finishListener the listener to call when the channel is closed or the length is reached + */ + public HeadStreamSinkConduit(final StreamSinkConduit next, final ConduitListener finishListener) { + super(next); + this.finishListener = finishListener; + } + + + public int write(final ByteBuffer src) throws IOException { + if (anyAreSet(state, FLAG_CLOSE_COMPLETE)) { + throw new ClosedChannelException(); + } + int remaining = src.remaining(); + src.position(src.position() + remaining); + return remaining; + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if (anyAreSet(state, FLAG_CLOSE_COMPLETE)) { + throw new ClosedChannelException(); + } + long total = 0; + for (int i = offset; i < offset + length; ++i) { + ByteBuffer src = srcs[i]; + int remaining = src.remaining(); + total += remaining; + src.position(src.position() + remaining); + } + return total; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (anyAreSet(state, FLAG_CLOSE_COMPLETE)) { + throw new ClosedChannelException(); + } + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (anyAreSet(state, FLAG_CLOSE_COMPLETE)) { + throw new ClosedChannelException(); + } + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + public boolean flush() throws IOException { + int val = state; + if (anyAreSet(val, FLAG_CLOSE_COMPLETE)) { + return true; + } + boolean flushed = false; + try { + return flushed = next.flush(); + } finally { + exitFlush(val, flushed); + } + } + + public void suspendWrites() { + long val = state; + if (anyAreSet(val, FLAG_CLOSE_COMPLETE)) { + return; + } + next.suspendWrites(); + } + + public void resumeWrites() { + long val = state; + if (anyAreSet(val, FLAG_CLOSE_COMPLETE)) { + return; + } + next.resumeWrites(); + } + + public boolean isWriteResumed() { + // not perfect but not provably wrong either... + return allAreClear(state, FLAG_CLOSE_COMPLETE) && next.isWriteResumed(); + } + + public void wakeupWrites() { + long val = state; + if (anyAreSet(val, FLAG_CLOSE_COMPLETE)) { + return; + } + next.wakeupWrites(); + } + + public void terminateWrites() throws IOException { + int oldVal, newVal; + oldVal = state; + if (anyAreSet(oldVal, FLAG_CLOSE_REQUESTED | FLAG_CLOSE_COMPLETE)) { + // no action necessary + return; + } + newVal = oldVal | FLAG_CLOSE_REQUESTED; + state = newVal; + } + + private void exitFlush(int oldVal, boolean flushed) { + int newVal = oldVal; + boolean callFinish = false; + if (anyAreSet(oldVal, FLAG_CLOSE_REQUESTED) && flushed) { + newVal |= FLAG_CLOSE_COMPLETE; + if (!anyAreSet(oldVal, FLAG_FINISHED_CALLED)) { + newVal |= FLAG_FINISHED_CALLED; + callFinish = true; + } + state = newVal; + if (callFinish) { + if (finishListener != null) { + finishListener.handleEvent(this); + } + } + } + } + + +} Index: 3rdParty_sources/undertow/io/undertow/conduits/IdleTimeoutConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/IdleTimeoutConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/IdleTimeoutConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,334 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowLogger; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ReadReadyHandler; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceConduit; +import org.xnio.conduits.WriteReadyHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +/** + * Conduit that adds support to close a channel once for a specified time no + * reads and no writes were performed. + * + * @author Norman Maurer + */ +public class IdleTimeoutConduit implements StreamSinkConduit, StreamSourceConduit { + + private static final int DELTA = 100; + private volatile XnioExecutor.Key handle; + private volatile long idleTimeout; + private volatile long expireTime = -1; + private volatile boolean timedOut = true; + + private final StreamSinkConduit sink; + private final StreamSourceConduit source; + + private volatile WriteReadyHandler writeReadyHandler; + private volatile ReadReadyHandler readReadyHandler; + + private final Runnable timeoutCommand = new Runnable() { + @Override + public void run() { + handle = null; + if(expireTime == -1) { + return; + } + long current = System.currentTimeMillis(); + if(current < expireTime) { + //timeout has been bumped, re-schedule + handle = sink.getWriteThread().executeAfter(timeoutCommand, (expireTime - current) + DELTA, TimeUnit.MILLISECONDS); + return; + } + + UndertowLogger.REQUEST_LOGGER.tracef("Timing out channel %s due to inactivity"); + timedOut = true; + doClose(); + if (sink.isWriteResumed()) { + if(writeReadyHandler != null) { + writeReadyHandler.writeReady(); + } + } + if (source.isReadResumed()) { + if(readReadyHandler != null) { + readReadyHandler.readReady(); + } + } + } + }; + + protected void doClose() { + safeClose(sink); + safeClose(source); + } + + public IdleTimeoutConduit(StreamSinkConduit sink, StreamSourceConduit source) { + this.sink = sink; + this.source = source; + } + + private void handleIdleTimeout() throws ClosedChannelException { + if(timedOut) { + return; + } + long idleTimeout = this.idleTimeout; + if(idleTimeout <= 0) { + return; + } + long currentTime = System.currentTimeMillis(); + long expireTimeVar = expireTime; + if(expireTimeVar != -1 && currentTime > expireTimeVar) { + timedOut = true; + doClose(); + throw new ClosedChannelException(); + } + expireTime = currentTime + idleTimeout; + XnioExecutor.Key key = handle; + if (key == null) { + handle = sink.getWriteThread().executeAfter(timeoutCommand, idleTimeout, TimeUnit.MILLISECONDS); + } + } + + @Override + public int write(ByteBuffer src) throws IOException { + handleIdleTimeout(); + int w = sink.write(src); + return w; + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + handleIdleTimeout(); + long w = sink.write(srcs, offset, length); + return w; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + handleIdleTimeout(); + int w = sink.writeFinal(src); + return w; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + handleIdleTimeout(); + long w = sink.writeFinal(srcs, offset, length); + return w; + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + handleIdleTimeout(); + long w = source.transferTo(position, count, target); + return w; + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + handleIdleTimeout(); + long w = source.transferTo(count, throughBuffer, target); + return w; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + handleIdleTimeout(); + long r = source.read(dsts, offset, length); + return r; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + handleIdleTimeout(); + int r = source.read(dst); + return r; + } + + @Override + public long transferFrom(FileChannel src, long position, long count) throws IOException { + handleIdleTimeout(); + long r = sink.transferFrom(src, position, count); + return r; + } + + @Override + public long transferFrom(StreamSourceChannel source, long count, ByteBuffer throughBuffer) throws IOException { + handleIdleTimeout(); + long r = sink.transferFrom(source, count, throughBuffer); + return r; + } + + @Override + public void suspendReads() { + source.suspendReads(); + } + + @Override + public void terminateReads() throws IOException { + source.terminateReads(); + } + + @Override + public boolean isReadShutdown() { + return source.isReadShutdown(); + } + + @Override + public void resumeReads() { + source.resumeReads(); + } + + @Override + public boolean isReadResumed() { + return source.isReadResumed(); + } + + @Override + public void wakeupReads() { + source.wakeupReads(); + } + @Override + public void awaitReadable() throws IOException { + source.awaitReadable(); + } + + @Override + public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { + source.awaitReadable(time, timeUnit); + } + + @Override + public XnioIoThread getReadThread() { + return source.getReadThread(); + } + + @Override + public void setReadReadyHandler(ReadReadyHandler handler) { + this.readReadyHandler = handler; + source.setReadReadyHandler(handler); + } + + private static void safeClose(final StreamSourceConduit sink) { + try { + sink.terminateReads(); + } catch (IOException e) { + } + } + + private static void safeClose(final StreamSinkConduit sink) { + try { + sink.truncateWrites(); + } catch (IOException e) { + } + } + + @Override + public void terminateWrites() throws IOException { + sink.terminateWrites(); + } + + @Override + public boolean isWriteShutdown() { + return sink.isWriteShutdown(); + } + + @Override + public void resumeWrites() { + sink.resumeWrites(); + } + + @Override + public void suspendWrites() { + sink.suspendWrites(); + } + + @Override + public void wakeupWrites() { + sink.wakeupWrites(); + } + + @Override + public boolean isWriteResumed() { + return sink.isWriteResumed(); + } + + @Override + public void awaitWritable() throws IOException { + sink.awaitWritable(); + } + + @Override + public void awaitWritable(long time, TimeUnit timeUnit) throws IOException { + sink.awaitWritable(); + } + + @Override + public XnioIoThread getWriteThread() { + return sink.getWriteThread(); + } + + @Override + public void setWriteReadyHandler(WriteReadyHandler handler) { + this.writeReadyHandler = handler; + sink.setWriteReadyHandler(handler); + } + + @Override + public void truncateWrites() throws IOException { + sink.truncateWrites(); + } + + @Override + public boolean flush() throws IOException { + return sink.flush(); + } + + @Override + public XnioWorker getWorker() { + return sink.getWorker(); + } + + public long getIdleTimeout() { + return idleTimeout; + } + + public void setIdleTimeout(long idleTimeout) { + this.idleTimeout = idleTimeout; + if(idleTimeout > 0) { + expireTime = System.currentTimeMillis() + idleTimeout; + } else { + expireTime = -1; + } + if (idleTimeout > 0 && handle == null) { + handle = sink.getWriteThread().executeAfter(timeoutCommand, idleTimeout + DELTA, TimeUnit.MILLISECONDS); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/PreChunkedStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/PreChunkedStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/PreChunkedStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,212 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.http.HttpAttachments; +import io.undertow.util.Attachable; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * Channel that implements HTTP chunked transfer coding for data streams that already have chunk markers. + * + * @author Stuart Douglas + */ +public class PreChunkedStreamSinkConduit extends AbstractStreamSinkConduit { + + private final ConduitListener finishListener; + + /** + * Flag that is set when {@link #terminateWrites()} or @{link #close()} is called + */ + private static final int FLAG_WRITES_SHUTDOWN = 1; + private static final int FLAG_FINISHED = 1 << 2; + + int state = 0; + final ChunkReader chunkReader; + + /** + * Construct a new instance. + * + * @param next the channel to wrap + * @param finishListener The finish listener + * @param attachable The attachable + */ + public PreChunkedStreamSinkConduit(final StreamSinkConduit next, final ConduitListener finishListener, final Attachable attachable) { + super(next); + //we don't want the reader to call the finish listener, so we pass null + this.chunkReader = new ChunkReader<>(attachable, HttpAttachments.RESPONSE_TRAILERS, null, this); + this.finishListener = finishListener; + } + + @Override + public int write(final ByteBuffer src) throws IOException { + return doWrite(src); + } + + + int doWrite(final ByteBuffer src) throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + throw new ClosedChannelException(); + } + if (chunkReader.getChunkRemaining() == -1) { + throw UndertowMessages.MESSAGES.extraDataWrittenAfterChunkEnd(); + } + if (src.remaining() == 0) { + return 0; + } + int oldPos = src.position(); + int oldLimit = src.limit(); + int ret = next.write(src); + if(ret == 0) { + return ret; + } + int newPos = src.position(); + src.position(oldPos); + src.limit(oldPos + ret); + try { + while (true) { + long chunkRemaining = chunkReader.readChunk(src); + if (chunkRemaining == -1) { + if (src.remaining() == 0) { + return ret; + } else { + throw UndertowMessages.MESSAGES.extraDataWrittenAfterChunkEnd(); + } + } else if(chunkRemaining == 0) { + return ret; + } + int remaining; + if (src.remaining() >= chunkRemaining) { + src.position((int) (src.position() + chunkRemaining)); + remaining = 0; + } else { + remaining = (int) (chunkRemaining - src.remaining()); + src.position(src.limit()); + } + chunkReader.setChunkRemaining(remaining); + if (!src.hasRemaining()) { + break; + } + } + } finally { + src.position(newPos); + src.limit(oldLimit); + } + return ret; + } + + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + for (int i = offset; i < length; ++i) { + if (srcs[i].hasRemaining()) { + return write(srcs[i]); + } + } + return 0; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + if (!src.hasRemaining()) { + terminateWrites(); + return 0; + } + int ret = doWrite(src); + terminateWrites(); + return ret; + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + throw new ClosedChannelException(); + } + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + throw new ClosedChannelException(); + } + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public boolean flush() throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + boolean val = next.flush(); + if (val && allAreClear(state, FLAG_FINISHED)) { + invokeFinishListener(); + } + return val; + } else { + return next.flush(); + } + } + + private void invokeFinishListener() { + state |= FLAG_FINISHED; + if (finishListener != null) { + finishListener.handleEvent(this); + } + } + + @Override + public void terminateWrites() throws IOException { + if (anyAreSet(state, FLAG_WRITES_SHUTDOWN)) { + return; + } + if (chunkReader.getChunkRemaining() != -1) { + throw UndertowMessages.MESSAGES.chunkedChannelClosedMidChunk(); + } + state |= FLAG_WRITES_SHUTDOWN; + } + + @Override + public void awaitWritable() throws IOException { + next.awaitWritable(); + } + + @Override + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + next.awaitWritable(time, timeUnit); + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/ReadDataStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/ReadDataStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/ReadDataStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,112 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.server.AbstractServerConnection; +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.ConduitReadableByteChannel; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +/** + * @author Stuart Douglas + */ +public class ReadDataStreamSourceConduit extends AbstractStreamSourceConduit { + + private final AbstractServerConnection connection; + + public ReadDataStreamSourceConduit(final StreamSourceConduit next, final AbstractServerConnection connection) { + super(next); + this.connection = connection; + } + + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + return target.transferFrom(new ConduitReadableByteChannel(this), position, count); + } + + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + return IoUtils.transfer(new ConduitReadableByteChannel(this), count, throughBuffer, target); + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + Pooled eb = connection.getExtraBytes(); + if (eb != null) { + final ByteBuffer buffer = eb.getResource(); + int result = Buffers.copy(dst, buffer); + if (!buffer.hasRemaining()) { + eb.free(); + connection.setExtraBytes(null); + } + return result; + } else { + return super.read(dst); + } + } + + @Override + public long read(final ByteBuffer[] dsts, final int offs, final int len) throws IOException { + Pooled eb = connection.getExtraBytes(); + if (eb != null) { + final ByteBuffer buffer = eb.getResource(); + int result = Buffers.copy(dsts, offs, len, buffer); + if (!buffer.hasRemaining()) { + eb.free(); + connection.setExtraBytes(null); + } + return result; + } else { + return super.read(dsts, offs, len); + } + } + + @Override + public void resumeReads() { + if (connection.getExtraBytes() != null) { + wakeupReads(); + } else { + super.resumeReads(); + } + } + + @Override + public void awaitReadable() throws IOException { + if (connection.getExtraBytes() != null) { + return; + } + super.awaitReadable(); + } + + @Override + public void awaitReadable(final long time, final TimeUnit timeUnit) throws IOException { + if (connection.getExtraBytes() != null) { + return; + } + super.awaitReadable(time, timeUnit); + } + +} Index: 3rdParty_sources/undertow/io/undertow/conduits/ReadTimeoutStreamSourceConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/ReadTimeoutStreamSourceConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/ReadTimeoutStreamSourceConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,168 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.server.OpenListener; + +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Options; +import org.xnio.StreamConnection; +import org.xnio.XnioExecutor; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +/** + * Wrapper for read timeout. This should always be the first wrapper applied to the underlying channel. + * + * @author Stuart Douglas + * @see org.xnio.Options#READ_TIMEOUT + */ +public final class ReadTimeoutStreamSourceConduit extends AbstractStreamSourceConduit { + + private XnioExecutor.Key handle; + private final StreamConnection connection; + private volatile long expireTime = -1; + private final OpenListener openListener; + + private static final int FUZZ_FACTOR = 50; //we add 50ms to the timeout to make sure the underlying channel has actually timed out + + private final Runnable timeoutCommand = new Runnable() { + @Override + public void run() { + handle = null; + if (expireTime == -1) { + return; + } + long current = System.currentTimeMillis(); + if (current < expireTime) { + //timeout has been bumped, re-schedule + handle = connection.getIoThread().executeAfter(timeoutCommand, (expireTime - current) + FUZZ_FACTOR, TimeUnit.MILLISECONDS); + return; + } + UndertowLogger.REQUEST_LOGGER.tracef("Timing out channel %s due to inactivity"); + IoUtils.safeClose(connection); + if (connection.getSourceChannel().isReadResumed()) { + ChannelListeners.invokeChannelListener(connection.getSourceChannel(), connection.getSourceChannel().getReadListener()); + } + if (connection.getSinkChannel().isWriteResumed()) { + ChannelListeners.invokeChannelListener(connection.getSinkChannel(), connection.getSinkChannel().getWriteListener()); + } + } + }; + + public ReadTimeoutStreamSourceConduit(final StreamSourceConduit delegate, StreamConnection connection, OpenListener openListener) { + super(delegate); + this.connection = connection; + this.openListener = openListener; + } + + private void handleReadTimeout(final long ret) throws IOException { + if (!connection.isOpen()) { + return; + } + if (ret == 0 && handle != null) { + return; + } + Integer timeout = getTimeout(); + if (timeout == null || timeout <= 0) { + return; + } + long currentTime = System.currentTimeMillis(); + long expireTimeVar = expireTime; + if (expireTimeVar != -1 && currentTime > expireTimeVar) { + IoUtils.safeClose(connection); + throw new ClosedChannelException(); + } + expireTime = currentTime + timeout; + XnioExecutor.Key key = handle; + if (key == null) { + handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS); + } + } + + @Override + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + long ret = super.transferTo(position, count, target); + handleReadTimeout(ret); + return ret; + } + + @Override + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + long ret = super.transferTo(count, throughBuffer, target); + handleReadTimeout(ret); + return ret; + } + + @Override + public long read(final ByteBuffer[] dsts, final int offset, final int length) throws IOException { + long ret = super.read(dsts, offset, length); + handleReadTimeout(ret); + return ret; + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + int ret = super.read(dst); + handleReadTimeout(ret); + return ret; + } + + @Override + public void awaitReadable() throws IOException { + Integer timeout = getTimeout(); + if (timeout != null && timeout > 0) { + super.awaitReadable(timeout + FUZZ_FACTOR, TimeUnit.MILLISECONDS); + } else { + super.awaitReadable(); + } + } + + @Override + public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { + Integer timeout = getTimeout(); + if (timeout != null && timeout > 0) { + long millis = timeUnit.toMillis(time); + super.awaitReadable(Math.min(millis, timeout + FUZZ_FACTOR), TimeUnit.MILLISECONDS); + } else { + super.awaitReadable(time, timeUnit); + } + } + + private Integer getTimeout() throws IOException { + Integer timeout = connection.getSourceChannel().getOption(Options.READ_TIMEOUT); + Integer idleTimeout = openListener.getUndertowOptions().get(UndertowOptions.IDLE_TIMEOUT); + if ((timeout == null || timeout <= 0) && idleTimeout != null) { + timeout = idleTimeout; + } else if (timeout != null && idleTimeout != null && idleTimeout > 0) { + timeout = Math.min(timeout, idleTimeout); + } + return timeout; + } +} Index: 3rdParty_sources/undertow/io/undertow/conduits/WriteTimeoutStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/conduits/WriteTimeoutStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/conduits/WriteTimeoutStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,182 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.conduits; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.server.OpenListener; + +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Options; +import org.xnio.StreamConnection; +import org.xnio.XnioExecutor; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +/** + * Wrapper for write timeout. This should always be the first wrapper applied to the underlying channel. + * + * @author Stuart Douglas + * @see org.xnio.Options#READ_TIMEOUT + */ +public final class WriteTimeoutStreamSinkConduit extends AbstractStreamSinkConduit { + + private XnioExecutor.Key handle; + private final StreamConnection connection; + private volatile long expireTime = -1; + private final OpenListener openListener; + + private static final int FUZZ_FACTOR = 50; //we add 50ms to the timeout to make sure the underlying channel has actually timed out + + private final Runnable timeoutCommand = new Runnable() { + @Override + public void run() { + handle = null; + if (expireTime == -1) { + return; + } + long current = System.currentTimeMillis(); + if (current < expireTime) { + //timeout has been bumped, re-schedule + handle = connection.getIoThread().executeAfter(timeoutCommand, (expireTime - current) + FUZZ_FACTOR, TimeUnit.MILLISECONDS); + return; + } + UndertowLogger.REQUEST_LOGGER.tracef("Timing out channel %s due to inactivity"); + IoUtils.safeClose(connection); + if (connection.getSourceChannel().isReadResumed()) { + ChannelListeners.invokeChannelListener(connection.getSourceChannel(), connection.getSourceChannel().getReadListener()); + } + if (connection.getSinkChannel().isWriteResumed()) { + ChannelListeners.invokeChannelListener(connection.getSinkChannel(), connection.getSinkChannel().getWriteListener()); + } + } + }; + + public WriteTimeoutStreamSinkConduit(final StreamSinkConduit delegate, StreamConnection connection, OpenListener openListener) { + super(delegate); + this.connection = connection; + this.openListener = openListener; + } + + private void handleWriteTimeout(final long ret) throws IOException { + if (!connection.isOpen()) { + return; + } + if (ret == 0 && handle != null) { + return; + } + Integer timeout = getTimeout(); + if (timeout == null || timeout <= 0) { + return; + } + long currentTime = System.currentTimeMillis(); + long expireTimeVar = expireTime; + if (expireTimeVar != -1 && currentTime > expireTimeVar) { + IoUtils.safeClose(connection); + throw new ClosedChannelException(); + } + expireTime = currentTime + timeout; + XnioExecutor.Key key = handle; + if (key == null) { + handle = connection.getIoThread().executeAfter(timeoutCommand, timeout, TimeUnit.MILLISECONDS); + } + } + + @Override + public int write(final ByteBuffer src) throws IOException { + int ret = super.write(src); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + long ret = super.write(srcs, offset, length); + handleWriteTimeout(ret); + return ret; + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + int ret = super.writeFinal(src); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + long ret = super.writeFinal(srcs, offset, length); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + long ret = super.transferFrom(src, position, count); + handleWriteTimeout(ret); + return ret; + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + long ret = super.transferFrom(source, count, throughBuffer); + handleWriteTimeout(ret); + return ret; + } + + @Override + public void awaitWritable() throws IOException { + Integer timeout = getTimeout(); + if (timeout != null && timeout > 0) { + super.awaitWritable(timeout + FUZZ_FACTOR, TimeUnit.MILLISECONDS); + } else { + super.awaitWritable(); + } + } + + @Override + public void awaitWritable(long time, TimeUnit timeUnit) throws IOException { + Integer timeout = getTimeout(); + if (timeout != null && timeout > 0) { + long millis = timeUnit.toMillis(time); + super.awaitWritable(Math.min(millis, timeout + FUZZ_FACTOR), TimeUnit.MILLISECONDS); + } else { + super.awaitWritable(time, timeUnit); + } + } + + private Integer getTimeout() throws IOException { + Integer timeout = connection.getSourceChannel().getOption(Options.WRITE_TIMEOUT); + Integer idleTimeout = openListener.getUndertowOptions().get(UndertowOptions.IDLE_TIMEOUT); + if ((timeout == null || timeout <= 0) && idleTimeout != null) { + timeout = idleTimeout; + } else if (timeout != null && idleTimeout != null && idleTimeout > 0) { + timeout = Math.min(timeout, idleTimeout); + } + return timeout; + } +} Index: 3rdParty_sources/undertow/io/undertow/io/AsyncSenderImpl.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/AsyncSenderImpl.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/AsyncSenderImpl.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,427 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import org.xnio.Buffers; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; + +/** + * @author Stuart Douglas + */ +public class AsyncSenderImpl implements Sender { + + private static final Charset utf8 = Charset.forName("UTF-8"); + + private StreamSinkChannel channel; + private final HttpServerExchange exchange; + private ByteBuffer[] buffer; + private Pooled[] pooledBuffers = null; + private FileChannel fileChannel; + private IoCallback callback; + private boolean inCallback; + + private final ChannelListener writeListener = new ChannelListener() { + @Override + public void handleEvent(final StreamSinkChannel streamSinkChannel) { + try { + long toWrite = Buffers.remaining(buffer); + long written = 0; + while (written < toWrite) { + long res = streamSinkChannel.write(buffer, 0, buffer.length); + written += res; + if (res == 0) { + return; + } + } + streamSinkChannel.suspendWrites(); + invokeOnComplete(); + } catch (IOException e) { + streamSinkChannel.suspendWrites(); + invokeOnException(callback, e); + } + } + }; + + public class TransferTask implements Runnable, ChannelListener { + public boolean run(boolean complete) { + try { + FileChannel source = fileChannel; + long pos = source.position(); + long size = source.size(); + + StreamSinkChannel dest = channel; + if (dest == null) { + if (callback == IoCallback.END_EXCHANGE) { + if (exchange.getResponseContentLength() == -1) { + exchange.setResponseContentLength(size); + } + } + channel = dest = exchange.getResponseChannel(); + if (dest == null) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + } + + while (size - pos > 0) { + long ret = dest.transferFrom(source, pos, size - pos); + pos += ret; + if (ret == 0) { + source.position(pos); + dest.getWriteSetter().set(this); + dest.resumeWrites(); + return false; + } + } + + if (complete) { + invokeOnComplete(); + } + } catch (IOException e) { + invokeOnException(callback, e); + } + + return true; + } + + @Override + public void handleEvent(StreamSinkChannel channel) { + channel.suspendWrites(); + channel.getWriteSetter().set(null); + exchange.dispatch(this); + } + + @Override + public void run() { + run(true); + } + } + + private final TransferTask transferTask = new TransferTask(); + + + public AsyncSenderImpl(final HttpServerExchange exchange) { + this.exchange = exchange; + } + + + @Override + public void send(final ByteBuffer buffer, final IoCallback callback) { + if (callback == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("callback"); + } + if (this.buffer != null || this.fileChannel != null) { + throw UndertowMessages.MESSAGES.dataAlreadyQueued(); + } + StreamSinkChannel channel = this.channel; + if (channel == null) { + if (callback == IoCallback.END_EXCHANGE) { + if (exchange.getResponseContentLength() == -1) { + exchange.setResponseContentLength(buffer.remaining()); + } + } + this.channel = channel = exchange.getResponseChannel(); + if (channel == null) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + } + this.callback = callback; + if (inCallback) { + this.buffer = new ByteBuffer[]{buffer}; + return; + } + try { + do { + if (buffer.remaining() == 0) { + callback.onComplete(exchange, this); + return; + } + int res = channel.write(buffer); + if (res == 0) { + this.buffer = new ByteBuffer[]{buffer}; + this.callback = callback; + channel.getWriteSetter().set(writeListener); + channel.resumeWrites(); + return; + } + } while (buffer.hasRemaining()); + invokeOnComplete(); + + } catch (IOException e) { + + invokeOnException(callback, e); + } + } + + @Override + public void send(final ByteBuffer[] buffer, final IoCallback callback) { + if (callback == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("callback"); + } + if (this.buffer != null) { + throw UndertowMessages.MESSAGES.dataAlreadyQueued(); + } + this.callback = callback; + if (inCallback) { + this.buffer = buffer; + return; + } + + long totalToWrite = Buffers.remaining(buffer); + + StreamSinkChannel channel = this.channel; + if (channel == null) { + if (callback == IoCallback.END_EXCHANGE) { + if (exchange.getResponseContentLength() == -1) { + exchange.setResponseContentLength(totalToWrite); + } + } + this.channel = channel = exchange.getResponseChannel(); + if (channel == null) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + } + + final long total = totalToWrite; + long written = 0; + + try { + do { + long res = channel.write(buffer); + written += res; + if (res == 0) { + this.buffer = buffer; + this.callback = callback; + channel.getWriteSetter().set(writeListener); + channel.resumeWrites(); + return; + } + } while (written < total); + invokeOnComplete(); + + } catch (IOException e) { + invokeOnException(callback, e); + } + } + + + @Override + public void transferFrom(FileChannel source, IoCallback callback) { + if (callback == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("callback"); + } + if (this.fileChannel != null || this.buffer != null) { + throw UndertowMessages.MESSAGES.dataAlreadyQueued(); + } + + this.callback = callback; + this.fileChannel = source; + if (inCallback) { + return; + } + + if (exchange.isInIoThread()) { + exchange.dispatch(transferTask); + return; + } + + transferTask.run(); + } + + @Override + public void send(final ByteBuffer buffer) { + send(buffer, IoCallback.END_EXCHANGE); + } + + @Override + public void send(final ByteBuffer[] buffer) { + send(buffer, IoCallback.END_EXCHANGE); + } + + @Override + public void send(final String data, final IoCallback callback) { + send(data, utf8, callback); + } + + @Override + public void send(final String data, final Charset charset, final IoCallback callback) { + ByteBuffer bytes = ByteBuffer.wrap(data.getBytes(charset)); + if (bytes.remaining() == 0) { + callback.onComplete(exchange, this); + } else { + int i = 0; + ByteBuffer[] bufs = null; + while (bytes.hasRemaining()) { + Pooled pooled = exchange.getConnection().getBufferPool().allocate(); + if (bufs == null) { + int noBufs = (bytes.remaining() + pooled.getResource().remaining() - 1) / pooled.getResource().remaining(); //round up division trick + pooledBuffers = new Pooled[noBufs]; + bufs = new ByteBuffer[noBufs]; + } + pooledBuffers[i] = pooled; + bufs[i] = pooled.getResource(); + Buffers.copy(pooled.getResource(), bytes); + pooled.getResource().flip(); + ++i; + } + send(bufs, callback); + } + } + + @Override + public void send(final String data) { + send(data, IoCallback.END_EXCHANGE); + } + + @Override + public void send(final String data, final Charset charset) { + send(data, charset, IoCallback.END_EXCHANGE); + } + + @Override + public void close(final IoCallback callback) { + try { + StreamSinkChannel channel = this.channel; + if (channel == null) { + if (exchange.getResponseContentLength() == -1) { + exchange.setResponseContentLength(0); + } + this.channel = channel = exchange.getResponseChannel(); + if (channel == null) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + } + channel.shutdownWrites(); + if (!channel.flush()) { + channel.getWriteSetter().set(ChannelListeners.flushingChannelListener( + new ChannelListener() { + @Override + public void handleEvent(final StreamSinkChannel channel) { + if(callback != null) { + callback.onComplete(exchange, AsyncSenderImpl.this); + } + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(final StreamSinkChannel channel, final IOException exception) { + try { + if(callback != null) { + invokeOnException(callback, exception); + } + } finally { + IoUtils.safeClose(channel); + } + } + } + )); + channel.resumeWrites(); + } else { + if (callback != null) { + callback.onComplete(exchange, this); + } + } + } catch (IOException e) { + if (callback != null) { + invokeOnException(callback, e); + } + } + } + + @Override + public void close() { + close(null); + } + + /** + * Invokes the onComplete method. If send is called again in onComplete then + * we loop and write it out. This prevents possible stack overflows due to recursion + */ + private void invokeOnComplete() { + for (; ; ) { + if (pooledBuffers != null) { + for (Pooled buffer : pooledBuffers) { + buffer.free(); + } + pooledBuffers = null; + } + IoCallback callback = this.callback; + this.buffer = null; + this.fileChannel = null; + this.callback = null; + inCallback = true; + try { + callback.onComplete(exchange, this); + } finally { + inCallback = false; + } + + StreamSinkChannel channel = this.channel; + if (this.buffer != null) { + long t = Buffers.remaining(buffer); + final long total = t; + long written = 0; + + try { + do { + long res = channel.write(buffer); + written += res; + if (res == 0) { + channel.getWriteSetter().set(writeListener); + channel.resumeWrites(); + return; + } + } while (written < total); + //we loop and invoke onComplete again + } catch (IOException e) { + invokeOnException(callback, e); + } + } else if (this.fileChannel != null) { + if (!transferTask.run(false)) { + return; + } + } else { + return; + } + + } + } + + + private void invokeOnException(IoCallback callback, IOException e) { + + if (pooledBuffers != null) { + for (Pooled buffer : pooledBuffers) { + buffer.free(); + } + pooledBuffers = null; + } + callback.onException(exchange, this, e); + } +} Index: 3rdParty_sources/undertow/io/undertow/io/BlockingSenderImpl.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/BlockingSenderImpl.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/BlockingSenderImpl.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,276 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.io.EOFException; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import org.xnio.IoUtils; + +/** + * A sender that uses an output stream. + * + * @author Stuart Douglas + */ +public class BlockingSenderImpl implements Sender { + + private static final Charset utf8 = Charset.forName("UTF-8"); + /** + * TODO: we should be used pooled buffers + */ + public static final int BUFFER_SIZE = 128; + + private final HttpServerExchange exchange; + private final OutputStream outputStream; + private boolean inCall; + private ByteBuffer[] next; + private FileChannel pendingFile; + private IoCallback queuedCallback; + + public BlockingSenderImpl(final HttpServerExchange exchange, final OutputStream outputStream) { + this.exchange = exchange; + this.outputStream = outputStream; + } + + @Override + public void send(final ByteBuffer buffer, final IoCallback callback) { + if (inCall) { + queue(new ByteBuffer[]{buffer}, callback); + return; + } + if (writeBuffer(buffer, callback)) { + invokeOnComplete(callback); + } + } + + + @Override + public void send(final ByteBuffer[] buffer, final IoCallback callback) { + if (inCall) { + queue(buffer, callback); + return; + } + if (!writeBuffer(buffer, callback)) { + return; + } + invokeOnComplete(callback); + } + + @Override + public void send(final ByteBuffer buffer) { + send(buffer, IoCallback.END_EXCHANGE); + } + + @Override + public void send(final ByteBuffer[] buffer) { + send(buffer, IoCallback.END_EXCHANGE); + } + + @Override + public void send(final String data, final IoCallback callback) { + if (inCall) { + queue(new ByteBuffer[]{ByteBuffer.wrap(data.getBytes(utf8))}, callback); + return; + } + try { + outputStream.write(data.getBytes(utf8)); + invokeOnComplete(callback); + } catch (IOException e) { + callback.onException(exchange, this, e); + } + } + + @Override + public void send(final String data, final Charset charset, final IoCallback callback) { + if (inCall) { + queue(new ByteBuffer[]{ByteBuffer.wrap(data.getBytes(charset))}, callback); + return; + } + try { + outputStream.write(data.getBytes(charset)); + invokeOnComplete(callback); + } catch (IOException e) { + callback.onException(exchange, this, e); + } + } + + @Override + public void send(final String data) { + send(data, IoCallback.END_EXCHANGE); + } + + @Override + public void send(final String data, final Charset charset) { + send(data, charset, IoCallback.END_EXCHANGE); + } + + @Override + public void transferFrom(FileChannel source, IoCallback callback) { + if (inCall) { + queue(source, callback); + return; + } + performTransfer(source, callback); + invokeOnComplete(callback); + } + + private void performTransfer(FileChannel source, IoCallback callback) { + if (outputStream instanceof BufferWritableOutputStream) { + try { + ((BufferWritableOutputStream) outputStream).transferFrom(source); + } catch (IOException e) { + callback.onException(exchange, this, e); + } + } else { + ByteBuffer buffer = ByteBuffer.allocate(BUFFER_SIZE); + try { + long pos = source.position(); + long size = source.size(); + while (size - pos > 0) { + int ret = source.read(buffer); + if (ret <= 0) { + break; + } + pos += ret; + outputStream.write(buffer.array(), buffer.arrayOffset(), ret); + buffer.clear(); + } + + if (pos != size) { + throw new EOFException("Unexpected EOF reading file"); + } + + } catch (IOException e) { + callback.onException(exchange, this, e); + } + } + } + + @Override + public void close(final IoCallback callback) { + try { + outputStream.close(); + invokeOnComplete(callback); + } catch (IOException e) { + callback.onException(exchange, this, e); + } + } + + @Override + public void close() { + IoUtils.safeClose(outputStream); + } + + private boolean writeBuffer(final ByteBuffer buffer, final IoCallback callback) { + return writeBuffer(new ByteBuffer[]{buffer}, callback); + } + + private boolean writeBuffer(final ByteBuffer[] buffers, final IoCallback callback) { + if (outputStream instanceof BufferWritableOutputStream) { + //fast path, if the stream can take a buffer directly just write to it + try { + ((BufferWritableOutputStream) outputStream).write(buffers); + return true; + } catch (IOException e) { + callback.onException(exchange, this, e); + return false; + } + } + for (ByteBuffer buffer : buffers) { + if (buffer.hasArray()) { + try { + outputStream.write(buffer.array(), buffer.arrayOffset(), buffer.remaining()); + } catch (IOException e) { + callback.onException(exchange, this, e); + return false; + } + } else { + byte[] b = new byte[BUFFER_SIZE]; + while (buffer.hasRemaining()) { + int toRead = Math.min(buffer.remaining(), BUFFER_SIZE); + buffer.get(b, 0, toRead); + try { + outputStream.write(b, 0, toRead); + } catch (IOException e) { + callback.onException(exchange, this, e); + return false; + } + } + } + } + return true; + } + + + private void invokeOnComplete(final IoCallback callback) { + inCall = true; + try { + callback.onComplete(exchange, this); + } finally { + inCall = false; + } + while (next != null || pendingFile != null) { + ByteBuffer[] next = this.next; + IoCallback queuedCallback = this.queuedCallback; + FileChannel file = this.pendingFile; + this.next = null; + this.queuedCallback = null; + this.pendingFile = null; + + if (next != null) { + for (ByteBuffer buffer : next) { + writeBuffer(buffer, queuedCallback); + } + } else if (file != null) { + performTransfer(file, queuedCallback); + } + inCall = true; + try { + queuedCallback.onComplete(exchange, this); + } finally { + inCall = false; + } + } + } + + private void queue(final ByteBuffer[] byteBuffers, final IoCallback ioCallback) { + //if data is sent from withing the callback we queue it, to prevent the stack growing indefinitely + if (next != null) { + throw UndertowMessages.MESSAGES.dataAlreadyQueued(); + } + next = byteBuffers; + queuedCallback = ioCallback; + } + + private void queue(final FileChannel source, final IoCallback ioCallback) { + //if data is sent from withing the callback we queue it, to prevent the stack growing indefinitely + if (pendingFile != null) { + throw UndertowMessages.MESSAGES.dataAlreadyQueued(); + } + pendingFile = source; + queuedCallback = ioCallback; + } + +} Index: 3rdParty_sources/undertow/io/undertow/io/BufferWritableOutputStream.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/BufferWritableOutputStream.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/BufferWritableOutputStream.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * Represents an output stream that can write byte buffers + * directly. + * + * @author Stuart Douglas + */ +public interface BufferWritableOutputStream { + + void write(final ByteBuffer[] buffers) throws IOException; + + void write(final ByteBuffer byteBuffer) throws IOException; + + void transferFrom(FileChannel source) throws IOException; + +} Index: 3rdParty_sources/undertow/io/undertow/io/DefaultIoCallback.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/DefaultIoCallback.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/DefaultIoCallback.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.io.IOException; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpServerExchange; +import org.xnio.IoUtils; + +/** + * A default callback implementation that simply ends the exchange + * + * @author Stuart Douglas + * @see IoCallback#END_EXCHANGE + */ +public class DefaultIoCallback implements IoCallback { + + protected DefaultIoCallback() { + + } + + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + sender.close(new IoCallback() { + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + exchange.endExchange(); + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + exchange.endExchange(); + } + }); + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + try { + exchange.endExchange(); + } finally { + IoUtils.safeClose(exchange.getConnection()); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/io/IoCallback.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/IoCallback.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/IoCallback.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.io.IOException; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +public interface IoCallback { + + void onComplete(final HttpServerExchange exchange, final Sender sender); + + void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception); + + /** + * A default callback that simply ends the exchange. + */ + IoCallback END_EXCHANGE = new DefaultIoCallback(); + +} Index: 3rdParty_sources/undertow/io/undertow/io/Sender.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/Sender.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/Sender.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,127 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +/** + * Sender interface that allows for callback based async IO. + * + * Note that all methods on this class are asynchronous, and may result in dispatch to an IO thread. After calling + * a method on this class you should not perform any more work on the current exchange until the callback is invoked. + * + * NOTE: implementers of this interface should be careful that they do not recursively call onComplete, which can + * lead to stack overflows if send is called many times. + * + * + * @author Stuart Douglas + */ +public interface Sender { + + /** + * Write the given buffer using async IO, and calls the given callback on completion or error. + * + * @param buffer The buffer to send. + * @param callback The callback + */ + void send(final ByteBuffer buffer, final IoCallback callback); + + /** + * Write the given buffers using async IO, and calls the given callback on completion or error. + * + * @param buffer The buffers to send. + * @param callback The callback + */ + void send(final ByteBuffer[] buffer, final IoCallback callback); + + /** + * Write the given buffer using async IO, and ends the exchange when done + * + * @param buffer The buffer to send. + */ + void send(final ByteBuffer buffer); + + /** + * Write the given buffers using async IO, and ends the exchange when done + * + * @param buffer The buffers to send. + */ + void send(final ByteBuffer[] buffer); + + /** + * Write the given String using async IO, and calls the given callback on completion or error. + *

+ * The CharSequence is encoded to UTF8 + * + * @param data The data to send + * @param callback The callback + */ + void send(final String data, final IoCallback callback); + + /** + * Write the given String using async IO, and calls the given callback on completion or error. + * + * @param data The buffer to end. + * @param charset The charset to use + * @param callback The callback + */ + void send(final String data, final Charset charset, final IoCallback callback); + + + /** + * Write the given String using async IO, and ends the exchange when done + *

+ * The CharSequence is encoded to UTF8 + * + * @param data The data to send + */ + void send(final String data); + + /** + * Write the given String using async IO, and ends the exchange when done + * + * @param data The buffer to end. + * @param charset The charset to use + */ + void send(final String data, final Charset charset); + + + /** + * Transfers all content from the specified file + * + * @param channel the file channel to transfer + * @param callback The callback + */ + void transferFrom(final FileChannel channel, final IoCallback callback); + + /** + * Closes this sender asynchronously. The given callback is notified on completion + * + * @param callback The callback that is notified when all data has been flushed and the channel is closed + */ + void close(final IoCallback callback); + + /** + * Closes this sender asynchronously + * + */ + void close(); +} Index: 3rdParty_sources/undertow/io/undertow/io/UndertowInputStream.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/UndertowInputStream.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/UndertowInputStream.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,168 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import org.xnio.Buffers; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.Channels; +import org.xnio.channels.EmptyStreamSourceChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * Input stream that reads from the underlying channel. This stream delays creation + * of the channel till it is actually used. + * + * @author Stuart Douglas + */ +public class UndertowInputStream extends InputStream { + + private final StreamSourceChannel channel; + private final Pool bufferPool; + + /** + * If this stream is ready for a read + */ + private static final int FLAG_CLOSED = 1; + private static final int FLAG_FINISHED = 1 << 1; + + private int state; + private Pooled pooled; + + public UndertowInputStream(final HttpServerExchange exchange) { + if (exchange.isRequestChannelAvailable()) { + this.channel = exchange.getRequestChannel(); + } else { + this.channel = new EmptyStreamSourceChannel(exchange.getIoThread()); + } + this.bufferPool = exchange.getConnection().getBufferPool(); + } + + @Override + public int read() throws IOException { + byte[] b = new byte[1]; + int read = read(b); + if (read == -1) { + return -1; + } + return b[0] & 0xff; + } + + @Override + public int read(final byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public int read(final byte[] b, final int off, final int len) throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + readIntoBuffer(); + if (anyAreSet(state, FLAG_FINISHED)) { + return -1; + } + if (len == 0) { + return 0; + } + ByteBuffer buffer = pooled.getResource(); + int copied = Buffers.copy(ByteBuffer.wrap(b, off, len), buffer); + if (!buffer.hasRemaining()) { + pooled.free(); + pooled = null; + } + return copied; + } + + private void readIntoBuffer() throws IOException { + if (pooled == null && !anyAreSet(state, FLAG_FINISHED)) { + pooled = bufferPool.allocate(); + + int res = Channels.readBlocking(channel, pooled.getResource()); + pooled.getResource().flip(); + if (res == -1) { + state |= FLAG_FINISHED; + pooled.free(); + pooled = null; + } + } + } + + private void readIntoBufferNonBlocking() throws IOException { + if (pooled == null && !anyAreSet(state, FLAG_FINISHED)) { + pooled = bufferPool.allocate(); + int res = channel.read(pooled.getResource()); + if (res == 0) { + pooled.free(); + pooled = null; + return; + } + pooled.getResource().flip(); + if (res == -1) { + state |= FLAG_FINISHED; + pooled.free(); + pooled = null; + } + } + } + + @Override + public int available() throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + readIntoBufferNonBlocking(); + if (anyAreSet(state, FLAG_FINISHED)) { + return -1; + } + if (pooled == null) { + return 0; + } + return pooled.getResource().remaining(); + } + + @Override + public void close() throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) { + return; + } + while (allAreClear(state, FLAG_FINISHED)) { + readIntoBuffer(); + if (pooled != null) { + pooled.free(); + pooled = null; + } + } + if (pooled != null) { + pooled.free(); + pooled = null; + } + channel.shutdownReads(); + state |= FLAG_FINISHED | FLAG_CLOSED; + } +} Index: 3rdParty_sources/undertow/io/undertow/io/UndertowOutputStream.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/io/UndertowOutputStream.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/io/UndertowOutputStream.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,342 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.io; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import org.xnio.Buffers; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.Channels; +import org.xnio.channels.StreamSinkChannel; + +import static org.xnio.Bits.anyAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * Buffering output stream that wraps a channel. + *

+ * This stream delays channel creation, so if a response will fit in the buffer it is not necessary to + * set the content length header. + * + * @author Stuart Douglas + */ +public class UndertowOutputStream extends OutputStream implements BufferWritableOutputStream { + + private final HttpServerExchange exchange; + private ByteBuffer buffer; + private Pooled pooledBuffer; + private StreamSinkChannel channel; + private int state; + private int written; + private final long contentLength; + + private static final int FLAG_CLOSED = 1; + private static final int FLAG_WRITE_STARTED = 1 << 1; + + private static final int MAX_BUFFERS_TO_ALLOCATE = 10; + + /** + * Construct a new instance. No write timeout is configured. + * + * @param exchange The exchange + */ + public UndertowOutputStream(HttpServerExchange exchange) { + this.exchange = exchange; + this.contentLength = exchange.getResponseContentLength(); + } + + /** + * {@inheritDoc} + */ + public void write(final int b) throws IOException { + write(new byte[]{(byte) b}, 0, 1); + } + + /** + * {@inheritDoc} + */ + public void write(final byte[] b) throws IOException { + write(b, 0, b.length); + } + + /** + * {@inheritDoc} + */ + public void write(final byte[] b, final int off, final int len) throws IOException { + if (len < 1) { + return; + } + if (anyAreSet(state, FLAG_CLOSED)) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + //if this is the last of the content + ByteBuffer buffer = buffer(); + if (len == contentLength - written || buffer.remaining() < len) { + if (buffer.remaining() < len) { + + //so what we have will not fit. + //We allocate multiple buffers up to MAX_BUFFERS_TO_ALLOCATE + //and put it in them + //if it still dopes not fit we loop, re-using these buffers + + StreamSinkChannel channel = this.channel; + if (channel == null) { + this.channel = channel = exchange.getResponseChannel(); + } + final Pool bufferPool = exchange.getConnection().getBufferPool(); + ByteBuffer[] buffers = new ByteBuffer[MAX_BUFFERS_TO_ALLOCATE + 1]; + Pooled[] pooledBuffers = new Pooled[MAX_BUFFERS_TO_ALLOCATE]; + try { + buffers[0] = buffer; + int bytesWritten = 0; + int rem = buffer.remaining(); + buffer.put(b, bytesWritten + off, rem); + buffer.flip(); + bytesWritten += rem; + int bufferCount = 1; + for (int i = 0; i < MAX_BUFFERS_TO_ALLOCATE; ++i) { + Pooled pooled = bufferPool.allocate(); + pooledBuffers[bufferCount - 1] = pooled; + buffers[bufferCount++] = pooled.getResource(); + ByteBuffer cb = pooled.getResource(); + int toWrite = len - bytesWritten; + if (toWrite > cb.remaining()) { + rem = cb.remaining(); + cb.put(b, bytesWritten + off, rem); + cb.flip(); + bytesWritten += rem; + } else { + cb.put(b, bytesWritten + off, len - bytesWritten); + bytesWritten = len; + cb.flip(); + break; + } + } + Channels.writeBlocking(channel, buffers, 0, bufferCount); + while (bytesWritten < len) { + //ok, it did not fit, loop and loop and loop until it is done + bufferCount = 0; + for (int i = 0; i < MAX_BUFFERS_TO_ALLOCATE + 1; ++i) { + ByteBuffer cb = buffers[i]; + cb.clear(); + bufferCount++; + int toWrite = len - bytesWritten; + if (toWrite > cb.remaining()) { + rem = cb.remaining(); + cb.put(b, bytesWritten + off, rem); + cb.flip(); + bytesWritten += rem; + } else { + cb.put(b, bytesWritten + off, len - bytesWritten); + bytesWritten = len; + cb.flip(); + break; + } + } + Channels.writeBlocking(channel, buffers, 0, bufferCount); + } + buffer.clear(); + } finally { + for (int i = 0; i < pooledBuffers.length; ++i) { + Pooled p = pooledBuffers[i]; + if (p == null) { + break; + } + p.free(); + } + } + } else { + buffer.put(b, off, len); + if (buffer.remaining() == 0) { + writeBufferBlocking(false); + } + } + } else { + buffer.put(b, off, len); + if (buffer.remaining() == 0) { + writeBufferBlocking(false); + } + } + updateWritten(len); + } + + @Override + public void write(ByteBuffer[] buffers) throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + int len = 0; + for (ByteBuffer buf : buffers) { + len += buf.remaining(); + } + if (len < 1) { + return; + } + + //if we have received the exact amount of content write it out in one go + //this is a common case when writing directly from a buffer cache. + if (this.written == 0 && len == contentLength) { + if (channel == null) { + channel = exchange.getResponseChannel(); + } + Channels.writeBlocking(channel, buffers, 0, buffers.length); + state |= FLAG_WRITE_STARTED; + } else { + ByteBuffer buffer = buffer(); + if (len < buffer.remaining()) { + Buffers.copy(buffer, buffers, 0, buffers.length); + } else { + if (channel == null) { + channel = exchange.getResponseChannel(); + } + if (buffer.position() == 0) { + Channels.writeBlocking(channel, buffers, 0, buffers.length); + } else { + final ByteBuffer[] newBuffers = new ByteBuffer[buffers.length + 1]; + buffer.flip(); + newBuffers[0] = buffer; + System.arraycopy(buffers, 0, newBuffers, 1, buffers.length); + Channels.writeBlocking(channel, newBuffers, 0, newBuffers.length); + buffer.clear(); + } + state |= FLAG_WRITE_STARTED; + } + } + updateWritten(len); + } + + @Override + public void write(ByteBuffer byteBuffer) throws IOException { + write(new ByteBuffer[]{byteBuffer}); + } + + void updateWritten(final long len) throws IOException { + this.written += len; + if (contentLength != -1 && this.written >= contentLength) { + flush(); + close(); + } + } + + /** + * {@inheritDoc} + */ + public void flush() throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + if (buffer != null && buffer.position() != 0) { + writeBufferBlocking(false); + } + if (channel == null) { + channel = exchange.getResponseChannel(); + } + Channels.flushBlocking(channel); + } + + private void writeBufferBlocking(final boolean writeFinal) throws IOException { + if (channel == null) { + channel = exchange.getResponseChannel(); + } + buffer.flip(); + + while (buffer.hasRemaining()) { + if(writeFinal) { + channel.writeFinal(buffer); + } else { + channel.write(buffer); + } + if(buffer.hasRemaining()) { + channel.awaitWritable(); + } + } + buffer.clear(); + state |= FLAG_WRITE_STARTED; + } + @Override + public void transferFrom(FileChannel source) throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + if (buffer != null && buffer.position() != 0) { + writeBufferBlocking(false); + } + if (channel == null) { + channel = exchange.getResponseChannel(); + } + long position = source.position(); + long size = source.size(); + Channels.transferBlocking(channel, source, position, size); + updateWritten(size - position); + } + + /** + * {@inheritDoc} + */ + public void close() throws IOException { + if (anyAreSet(state, FLAG_CLOSED)) return; + try { + state |= FLAG_CLOSED; + if (anyAreClear(state, FLAG_WRITE_STARTED) && channel == null) { + if (buffer == null) { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "0"); + } else { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "" + buffer.position()); + } + } + if (buffer != null) { + writeBufferBlocking(true); + } + if (channel == null) { + channel = exchange.getResponseChannel(); + } + if(channel == null) { + return; + } + StreamSinkChannel channel = this.channel; + channel.shutdownWrites(); + Channels.flushBlocking(channel); + } finally { + if (pooledBuffer != null) { + pooledBuffer.free(); + buffer = null; + } else { + buffer = null; + } + } + } + + private ByteBuffer buffer() { + ByteBuffer buffer = this.buffer; + if (buffer != null) { + return buffer; + } + this.pooledBuffer = exchange.getConnection().getBufferPool().allocate(); + this.buffer = pooledBuffer.getResource(); + return this.buffer; + } + +} Index: 3rdParty_sources/undertow/io/undertow/predicate/AndPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/AndPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/AndPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +class AndPredicate implements Predicate { + + private final Predicate[] predicates; + + public AndPredicate(final Predicate ... predicates) { + this.predicates = predicates; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + for(final Predicate predicate : predicates) { + if(!predicate.resolve(value)) { + return false; + } + } + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/AuthenticationRequiredPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/AuthenticationRequiredPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/AuthenticationRequiredPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,59 @@ +package io.undertow.predicate; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpServerExchange; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * Predicate that returns true if authentication is required. + * + * @author Stuart Douglas + */ +public class AuthenticationRequiredPredicate implements Predicate { + + public static final AuthenticationRequiredPredicate INSTANCE = new AuthenticationRequiredPredicate(); + + @Override + public boolean resolve(HttpServerExchange value) { + SecurityContext sc = value.getSecurityContext(); + if(sc == null) { + return false; + } + return sc.isAuthenticationRequired(); + } + + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "auth-required"; + } + + @Override + public Map> parameters() { + final Map> params = new HashMap<>(); + return params; + } + + @Override + public Set requiredParameters() { + final Set params = new HashSet<>(); + return params; + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public Predicate build(final Map config) { + return INSTANCE; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/ContainsPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/ContainsPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/ContainsPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,94 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.server.HttpServerExchange; + +/** + * Returns true if the request header is present and contains one of the strings to match. + * + * @author Stuart Douglas + */ +class ContainsPredicate implements Predicate { + + private final ExchangeAttribute attribute; + private final String[] values; + + ContainsPredicate(final ExchangeAttribute attribute, final String[] values) { + this.attribute = attribute; + this.values = new String[values.length]; + System.arraycopy(values, 0, this.values, 0, values.length); + } + + @Override + public boolean resolve(final HttpServerExchange value) { + String attr = attribute.readAttribute(value); + if (attr == null) { + return false; + } + for (int i = 0; i < values.length; ++i) { + if (attr.contains(values[i])) { + return true; + } + } + return false; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "contains"; + } + + @Override + public Map> parameters() { + final Map> params = new HashMap<>(); + params.put("value", ExchangeAttribute.class); + params.put("search", String[].class); + return params; + } + + @Override + public Set requiredParameters() { + final Set params = new HashSet<>(); + params.add("value"); + params.add("search"); + return params; + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public Predicate build(final Map config) { + String[] search = (String[]) config.get("search"); + ExchangeAttribute values = (ExchangeAttribute) config.get("value"); + return new ContainsPredicate(values, search); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/EqualsPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/EqualsPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/EqualsPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,93 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.server.HttpServerExchange; + +/** + * Returns true if all the provided arguments are equal to each other + * + * @author Stuart Douglas + */ +class EqualsPredicate implements Predicate { + + private final ExchangeAttribute[] attributes; + + EqualsPredicate(final ExchangeAttribute[] attributes) { + this.attributes = attributes; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + if(attributes.length < 2) { + return true; + } + String first = attributes[0].readAttribute(value); + for(int i = 1; i < attributes.length; ++i) { + String current = attributes[i].readAttribute(value); + if(first == null) { + if(current != null) { + return false; + } + } else if(current == null) { + return false; + } else if(!first.equals(current)) { + return false; + } + } + return true; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "equals"; + } + + @Override + public Map> parameters() { + final Map> params = new HashMap<>(); + params.put("value", ExchangeAttribute[].class); + return params; + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public Predicate build(final Map config) { + ExchangeAttribute[] value = (ExchangeAttribute[]) config.get("value"); + return new EqualsPredicate(value); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/ExistsPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/ExistsPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/ExistsPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.server.HttpServerExchange; + +/** + * Returns true if the given attribute is not null and not an empty string + * + * @author Stuart Douglas + */ +class ExistsPredicate implements Predicate { + + private final ExchangeAttribute attribute; + + ExistsPredicate(final ExchangeAttribute attribute) { + this.attribute = attribute; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + final String att = attribute.readAttribute(value); + if(att == null) { + return false; + } + return !att.isEmpty(); + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "exists"; + } + + @Override + public Map> parameters() { + final Map> params = new HashMap<>(); + params.put("value", ExchangeAttribute.class); + return params; + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public Predicate build(final Map config) { + ExchangeAttribute value = (ExchangeAttribute) config.get("value"); + return new ExistsPredicate(value); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/FalsePredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/FalsePredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/FalsePredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +class FalsePredicate implements Predicate { + + public static final FalsePredicate INSTANCE = new FalsePredicate(); + + public static FalsePredicate instance() { + return INSTANCE; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + return false; + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/MaxContentSizePredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/MaxContentSizePredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/MaxContentSizePredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,79 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; + +/** + * Predicate that returns true if the Content-Size of a request is above a + * given value. + * + * @author Stuart Douglas + */ +class MaxContentSizePredicate implements Predicate { + + private final long maxSize; + + public MaxContentSizePredicate(final long maxSize) { + this.maxSize = maxSize; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + final String length = value.getResponseHeaders().getFirst(Headers.CONTENT_LENGTH); + if (length == null) { + return false; + } + return Long.parseLong(length) > maxSize; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "max-content-size"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("value", Long.class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public Predicate build(final Map config) { + Long max = (Long) config.get("value"); + return new MaxContentSizePredicate(max); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/MethodPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/MethodPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/MethodPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,83 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +class MethodPredicate implements Predicate { + + private final HttpString[] methods; + + MethodPredicate(String[] methods) { + HttpString[] values = new HttpString[methods.length]; + for(int i = 0; i < methods.length; ++i) { + values[i] = HttpString.tryFromString(methods[i]); + } + this.methods = values; + } + + + @Override + public boolean resolve(final HttpServerExchange value) { + for(int i =0; i < methods.length; ++i) { + if(value.getRequestMethod().equals(methods[i])) { + return true; + } + } + return false; + } + + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "method"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("value", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public Predicate build(final Map config) { + String[] methods = (String[]) config.get("value"); + return new MethodPredicate(methods); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/MinContentSizePredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/MinContentSizePredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/MinContentSizePredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,79 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; + +/** + * Predicate that returns true if the Content-Size of a request is below a + * given value. + * + * @author Stuart Douglas + */ +class MinContentSizePredicate implements Predicate { + + private final long minSize; + + public MinContentSizePredicate(final long minSize) { + this.minSize = minSize; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + final String length = value.getResponseHeaders().getFirst(Headers.CONTENT_LENGTH); + if (length == null) { + return false; + } + return Long.parseLong(length) < minSize; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "min-content-size"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("value", Long.class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public Predicate build(final Map config) { + Long max = (Long) config.get("value"); + return new MinContentSizePredicate(max); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/NotPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/NotPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/NotPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +class NotPredicate implements Predicate { + + private final Predicate predicate; + + public NotPredicate(final Predicate predicate) { + this.predicate = predicate; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + return !predicate.resolve(value); + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/OrPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/OrPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/OrPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +class OrPredicate implements Predicate { + + private final Predicate[] predicates; + + public OrPredicate(final Predicate... predicates) { + this.predicates = predicates; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + for (final Predicate predicate : predicates) { + if (predicate.resolve(value)) { + return true; + } + } + return false; + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PathMatchPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PathMatchPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PathMatchPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,82 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.PathMatcher; + +/** + * @author Stuart Douglas + */ +class PathMatchPredicate implements Predicate { + + private final PathMatcher pathMatcher; + + public PathMatchPredicate(final String... paths) { + PathMatcher matcher = new PathMatcher<>(); + for(String path : paths) { + if(!path.startsWith("/")) { + matcher.addExactPath("/" + path, Boolean.TRUE); + } else { + matcher.addExactPath(path, Boolean.TRUE); + } + } + this.pathMatcher = matcher; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + final String relativePath = value.getRelativePath(); + PathMatcher.PathMatch result = pathMatcher.match(relativePath); + return result.getValue() == Boolean.TRUE; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "path"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("path", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("path"); + } + + @Override + public String defaultParameter() { + return "path"; + } + + @Override + public Predicate build(final Map config) { + String[] path = (String[]) config.get("path"); + return new PathMatchPredicate(path); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PathPrefixPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PathPrefixPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PathPrefixPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,88 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.PathMatcher; + +/** + * @author Stuart Douglas + */ +class PathPrefixPredicate implements Predicate { + + private final PathMatcher pathMatcher; + + public PathPrefixPredicate(final String... paths) { + PathMatcher matcher = new PathMatcher<>(); + for(String path : paths) { + if(!path.startsWith("/")) { + matcher.addPrefixPath("/" + path, Boolean.TRUE); + } else { + matcher.addPrefixPath(path, Boolean.TRUE); + } + } + this.pathMatcher = matcher; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + final String relativePath = value.getRelativePath(); + PathMatcher.PathMatch result = pathMatcher.match(relativePath); + + boolean matches = result.getValue() == Boolean.TRUE; + if(matches) { + Map context = value.getAttachment(PREDICATE_CONTEXT); + context.put("remaining", result.getRemaining()); + } + return matches; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "path-prefix"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("path", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("path"); + } + + @Override + public String defaultParameter() { + return "path"; + } + + @Override + public Predicate build(final Map config) { + String[] path = (String[]) config.get("path"); + return new PathPrefixPredicate(path); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PathSuffixPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PathSuffixPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PathSuffixPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,72 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +class PathSuffixPredicate implements Predicate { + + private final String suffix; + + public PathSuffixPredicate(final String suffix) { + this.suffix = suffix; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + return value.getRelativePath().endsWith(suffix); + } + + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "path-suffix"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("path", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("path"); + } + + @Override + public String defaultParameter() { + return "path"; + } + + @Override + public Predicate build(final Map config) { + String[] path = (String[]) config.get("path"); + return Predicates.suffixes(path); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PathTemplatePredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PathTemplatePredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PathTemplatePredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,95 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.PathTemplate; + +/** + * @author Stuart Douglas + */ +public class PathTemplatePredicate implements Predicate { + + private final ExchangeAttribute attribute; + private final PathTemplate value; + + public PathTemplatePredicate(final String template, final ExchangeAttribute attribute) { + this.attribute = attribute; + this.value = PathTemplate.create(template); + } + + @Override + public boolean resolve(final HttpServerExchange exchange) { + final Map params = new HashMap<>(); + boolean result = this.value.matches(attribute.readAttribute(exchange), params); + if (result) { + Map context = exchange.getAttachment(PREDICATE_CONTEXT); + if (context != null) { + context.putAll(params); + } + } + return result; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "path-template"; + } + + @Override + public Map> parameters() { + final Map> params = new HashMap<>(); + params.put("value", String.class); + params.put("match", ExchangeAttribute.class); + return params; + } + + @Override + public Set requiredParameters() { + final Set params = new HashSet<>(); + params.add("value"); + return params; + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public Predicate build(final Map config) { + ExchangeAttribute match = (ExchangeAttribute) config.get("match"); + if (match == null) { + match = ExchangeAttributes.relativePath(); + } + String value = (String) config.get("value"); + return new PathTemplatePredicate(value, match); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/predicate/Predicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/Predicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/Predicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Map; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +/** + * A predicate. + * + * This is mainly uses by handlers as a way to decide if a request should have certain + * processing applied, based on the given conditions. + * + * @author Stuart Douglas + */ +public interface Predicate { + + /** + * Attachment key that can be used to store additional predicate context that allows the predicates to store + * additional information. For example a predicate that matches on a regular expression can place additional + * information about match groups into the predicate context. + * + * Predicates must not rely on this attachment being present, it will only be present if the predicate is being + * used in a situation where this information may be required by later handlers. + * + */ + AttachmentKey> PREDICATE_CONTEXT = AttachmentKey.create(Map.class); + + boolean resolve(final HttpServerExchange value); + +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PredicateBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PredicateBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PredicateBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.Map; +import java.util.Set; + +/** + * An interface that knows how to build a predicate from a textual representation. This is loaded + * using a service loader to make it configurable. + *

+ * This makes it easy to configure conditions based on a string representation + * + * @author Stuart Douglas + */ +public interface PredicateBuilder { + + /** + * The string representation of the predicate name. + * + * @return The predicate name + */ + String name(); + + /** + * Returns a map of parameters and their types. + */ + Map> parameters(); + + /** + * + * @return The required parameters + */ + Set requiredParameters(); + + /** + * @return The default parameter name, or null if it does not have a default parameter + */ + String defaultParameter(); + + /** + * Creates a predicate + * + * @param config The predicate config + * @return The new predicate + */ + Predicate build(final Map config); + +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PredicateParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PredicateParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PredicateParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,584 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.lang.reflect.Array; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +import io.undertow.UndertowMessages; +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributeParser; +import io.undertow.attribute.ExchangeAttributes; + +/** + * Parser that can build a predicate from a string representation. The underlying syntax is quite simple, and example is + * shown below: + *

+ * + * path["/MyPath"] or (method[value="POST"] and not headersPresent[value={Content-Type, "Content-Encoding"}, ignoreTrailer=true] + * + *

+ * The following boolean operators are built in, listed in order or precedence: + * - not + * - and + * - or + *

+ * They work pretty much as you would expect them to. All other tokens are taken + * to be predicate names. If the predicate does not require any parameters then the + * brackets can be omitted, otherwise they are mandatory. + *

+ * If a predicate is only being passed a single parameter then the parameter name can be omitted. + * Strings can be enclosed in optional double or single quotations marks, and quotation marks can be escaped using + * \". + *

+ * Array types are represented via a comma separated list of values enclosed in curly braces. + *

+ * TODO: should we use antlr (or whatever) here? I don't really want an extra dependency just for this... + * + * @author Stuart Douglas + */ +public class PredicateParser { + + + public static final Predicate parse(String string, final ClassLoader classLoader) { + final Map builders = loadBuilders(classLoader); + final ExchangeAttributeParser attributeParser = ExchangeAttributes.parser(classLoader); + return parse(string, builders, attributeParser); + } + + private static Map loadBuilders(final ClassLoader classLoader) { + ServiceLoader loader = ServiceLoader.load(PredicateBuilder.class, classLoader); + final Map ret = new HashMap<>(); + for (PredicateBuilder builder : loader) { + if (ret.containsKey(builder.name())) { + if (ret.get(builder.name()).getClass() != builder.getClass()) { + throw UndertowMessages.MESSAGES.moreThanOnePredicateWithName(builder.name(), builder.getClass(), ret.get(builder.name()).getClass()); + } + } else { + ret.put(builder.name(), builder); + } + } + return ret; + } + + private static IllegalStateException error(final String string, int pos, String reason) { + StringBuilder b = new StringBuilder(); + b.append(string); + b.append('\n'); + for (int i = 0; i < pos; ++i) { + b.append(' '); + } + b.append('^'); + throw UndertowMessages.MESSAGES.errorParsingPredicateString(reason, b.toString()); + } + + static Predicate parse(final String string, final Map builders, final ExchangeAttributeParser attributeParser) { + + //shunting yard algorithm + //gets rid or parentheses and fixes up operator ordering + Deque tokens = tokenize(string); + Deque operatorStack = new ArrayDeque<>(); + + //the output, consisting of predicate nodes and string representations of operators + //it is a bit yuck mixing up the types, but whatever + Deque output = new ArrayDeque<>(); + + while (!tokens.isEmpty()) { + Token token = tokens.poll(); + if (isSpecialChar(token.token)) { + if (token.token.equals("(")) { + operatorStack.push("("); + } else if (token.token.equals(")")) { + for (; ; ) { + String op = operatorStack.pop(); + if (op == null) { + throw error(string, token.position, "Unexpected end of input"); + } else if (op.equals("(")) { + break; + } else { + output.push(op); + } + } + } else { + throw error(string, token.position, "Mismatched parenthesis"); + } + } else { + if (isOperator(token.token)) { + int prec = precedence(token.token); + String top = operatorStack.peek(); + while (top != null) { + if (top.equals("(")) { + break; + } + int exitingPrec = precedence(top); + if (prec <= exitingPrec) { + output.push(operatorStack.pop()); + } else { + break; + } + top = operatorStack.peek(); + } + operatorStack.push(token.token); + } else { + output.push(parsePredicate(string, token, tokens, builders, attributeParser)); + } + } + } + while (!operatorStack.isEmpty()) { + String op = operatorStack.pop(); + if (op.equals(")")) { + throw error(string, string.length(), "Mismatched parenthesis"); + } + output.push(op); + } + //now we have our tokens + Predicate predicate = collapseOutput(output.pop(), output).resolve(); + if (!output.isEmpty()) { + throw error(string, 0, "Invalid expression"); + } + return predicate; + + } + + private static Node collapseOutput(final Object token, final Deque tokens) { + if (token instanceof Node) { + return (Node) token; + } else if (token.equals("and")) { + Node n1 = collapseOutput(tokens.pop(), tokens); + Node n2 = collapseOutput(tokens.pop(), tokens); + return new AndNode(n2, n1); + } else if (token.equals("or")) { + Node n1 = collapseOutput(tokens.pop(), tokens); + Node n2 = collapseOutput(tokens.pop(), tokens); + return new OrNode(n2, n1); + } else if (token.equals("not")) { + Node n1 = collapseOutput(tokens.pop(), tokens); + return new NotNode(n1); + } else { + throw new IllegalStateException("Invalid operator " + token); + } + + } + + private static Object parsePredicate(final String string, final Token token, final Deque tokens, final Map builders, final ExchangeAttributeParser attributeParser) { + if (token.token.equals("true")) { + return new PredicateNode(TruePredicate.instance()); + } else if (token.token.equals("false")) { + return new PredicateNode(FalsePredicate.instance()); + } else { + PredicateBuilder builder = builders.get(token.token); + if (builder == null) { + + throw error(string, token.position, "no predicate named " + token.token + " known predicates: " + builders.keySet()); + } + Token next = tokens.peek(); + if (next.token.equals("[")) { + final Map values = new HashMap<>(); + + tokens.poll(); + next = tokens.poll(); + if (next == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + if (next.token.equals("{")) { + return handleSingleArrayValue(string, builder, tokens, next, attributeParser); + } + while (!next.token.equals("]")) { + Token equals = tokens.poll(); + if (!equals.token.equals("=")) { + if (equals.token.equals("]") && values.isEmpty()) { + //single value case + return handleSingleValue(string, builder, next, attributeParser); + } else if (equals.token.equals(",")) { + tokens.push(equals); + tokens.push(next); + return handleSingleVarArgsValue(string, builder, tokens, next, attributeParser); + } + throw error(string, equals.position, "Unexpected token"); + } + Token value = tokens.poll(); + if (value == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + if (value.token.equals("{")) { + values.put(next.token, readArrayType(string, tokens, next, builder, attributeParser, "}")); + } else { + if (isOperator(value.token) || isSpecialChar(value.token)) { + throw error(string, value.position, "Unexpected token"); + } + + Class type = builder.parameters().get(next.token); + if (type == null) { + throw error(string, next.position, "Unexpected parameter " + next.token); + } + values.put(next.token, coerceToType(string, value, type, attributeParser)); + } + + next = tokens.poll(); + if (next == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + if (!next.token.equals("]")) { + if (!next.token.equals(",")) { + throw error(string, string.length(), "Expecting , or ]"); + } + next = tokens.poll(); + if (next == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + } + } + checkParameters(string, next.position, values, builder); + return new BuilderNode(builder, values); + + } else { + if (isSpecialChar(next.token)) { + throw error(string, next.position, "Unexpected character"); + } + return new BuilderNode(builder); + } + } + } + + private static Node handleSingleArrayValue(final String string, final PredicateBuilder builder, final Deque tokens, final Token token, final ExchangeAttributeParser attributeParser) { + String sv = builder.defaultParameter(); + if (sv == null) { + throw error(string, token.position, "default parameter not supported"); + } + Object array = readArrayType(string, tokens, new Token(sv, token.position), builder, attributeParser, "}"); + Token close = tokens.poll(); + if (!close.token.equals("]")) { + throw error(string, close.position, "expected ]"); + } + return new BuilderNode(builder, Collections.singletonMap(sv, array)); + } + + private static Node handleSingleVarArgsValue(final String string, final PredicateBuilder builder, final Deque tokens, final Token token, final ExchangeAttributeParser attributeParser) { + String sv = builder.defaultParameter(); + if (sv == null) { + throw error(string, token.position, "default parameter not supported"); + } + Object array = readArrayType(string, tokens, new Token(sv, token.position), builder, attributeParser, "]"); + return new BuilderNode(builder, Collections.singletonMap(sv, array)); + } + + private static Object readArrayType(final String string, final Deque tokens, Token paramName, PredicateBuilder builder, final ExchangeAttributeParser attributeParser, String expectedEndToken) { + Class type = builder.parameters().get(paramName.token); + if (type == null) { + throw error(string, paramName.position, "no parameter called " + paramName.token); + } else if (!type.isArray()) { + throw error(string, paramName.position, "parameter is not an array type " + paramName.token); + } + + Class componentType = type.getComponentType(); + final List values = new ArrayList<>(); + Token token = tokens.poll(); + while (token != null) { + Token commaOrEnd = tokens.poll(); + values.add(coerceToType(string, token, componentType, attributeParser)); + if (commaOrEnd.token.equals(expectedEndToken)) { + Object array = Array.newInstance(componentType, values.size()); + for (int i = 0; i < values.size(); ++i) { + Array.set(array, i, values.get(i)); + } + return array; + } else if (!commaOrEnd.token.equals(",")) { + throw error(string, commaOrEnd.position, "expected either , or }"); + } + token = tokens.poll(); + } + throw error(string, string.length(), "unexpected end of input in array"); + } + + + private static Object handleSingleValue(final String string, final PredicateBuilder builder, final Token next, final ExchangeAttributeParser attributeParser) { + String sv = builder.defaultParameter(); + if (sv == null) { + throw error(string, next.position, "default parameter not supported"); + } + Map values = Collections.singletonMap(sv, coerceToType(string, next, builder.parameters().get(sv), attributeParser)); + checkParameters(string, next.position, values, builder); + return new BuilderNode(builder, values); + } + + private static void checkParameters(final String string, int pos, final Map values, final PredicateBuilder builder) { + final Set required = new HashSet<>(builder.requiredParameters()); + for (String key : values.keySet()) { + required.remove(key); + } + if (!required.isEmpty()) { + throw error(string, pos, "Missing required parameters " + required); + } + } + + + private static Object coerceToType(final String string, final Token token, final Class type, final ExchangeAttributeParser attributeParser) { + if (type.isArray()) { + Object array = Array.newInstance(type.getComponentType(), 1); + Array.set(array, 0, coerceToType(string, token, type.getComponentType(), attributeParser)); + return array; + } + + if (type == String.class) { + return token.token; + } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { + return Boolean.valueOf(token.token); + } else if (type.equals(Byte.class) || type.equals(byte.class)) { + return Byte.valueOf(token.token); + } else if (type.equals(Character.class) || type.equals(char.class)) { + if (token.token.length() != 1) { + throw error(string, token.position, "Cannot coerce " + token.token + " to a Character"); + } + return Character.valueOf(token.token.charAt(0)); + } else if (type.equals(Short.class) || type.equals(short.class)) { + return Short.valueOf(token.token); + } else if (type.equals(Integer.class) || type.equals(int.class)) { + return Integer.valueOf(token.token); + } else if (type.equals(Long.class) || type.equals(long.class)) { + return Long.valueOf(token.token); + } else if (type.equals(Float.class) || type.equals(float.class)) { + return Float.valueOf(token.token); + } else if (type.equals(Double.class) || type.equals(double.class)) { + return Double.valueOf(token.token); + } else if (type.equals(ExchangeAttribute.class)) { + return attributeParser.parse(token.token); + } + + return token.token; + } + + private static int precedence(String operator) { + if (operator.equals("not")) { + return 3; + } else if (operator.equals("and")) { + return 2; + } else if (operator.equals("or")) { + return 1; + } + throw new IllegalStateException(); + } + + + private static boolean isOperator(final String op) { + return op.equals("and") || op.equals("or") || op.equals("not"); + } + + private static boolean isSpecialChar(String token) { + if (token.length() != 1) { + return false; + } + char c = token.charAt(0); + switch (c) { + case '(': + case ')': + case ',': + case '=': + case '{': + case '}': + case '[': + case ']': + return true; + default: + return false; + } + } + + static Deque tokenize(final String string) { + char currentStringDelim = 0; + boolean inVariable = false; + + int pos = 0; + StringBuilder current = new StringBuilder(); + Deque ret = new ArrayDeque<>(); + while (pos < string.length()) { + char c = string.charAt(pos); + if (currentStringDelim != 0) { + if (c == currentStringDelim && current.charAt(current.length() - 1) != '\\') { + ret.add(new Token(current.toString(), pos)); + current.setLength(0); + currentStringDelim = 0; + } else { + current.append(c); + } + } else { + switch (c) { + case ' ': + case '\t': { + if (current.length() != 0) { + ret.add(new Token(current.toString(), pos)); + current.setLength(0); + } + break; + } + case '(': + case ')': + case ',': + case '=': + case '[': + case ']': + case '{': + case '}': { + if (inVariable) { + current.append(c); + if (c == '}') { + inVariable = false; + } + } else { + if (current.length() != 0) { + ret.add(new Token(current.toString(), pos)); + current.setLength(0); + } + ret.add(new Token("" + c, pos)); + } + break; + } + case '"': + case '\'': { + if (current.length() != 0) { + throw error(string, pos, "Unexpected token"); + } + currentStringDelim = c; + break; + } + case '%': { + current.append(c); + if (string.charAt(pos + 1) == '{') { + inVariable = true; + } + break; + } + default: + current.append(c); + } + } + ++pos; + } + if (current.length() > 0) { + ret.add(new Token(current.toString(), string.length())); + } + return ret; + } + + + static final class Token { + final String token; + final int position; + + private Token(final String token, final int position) { + this.token = token; + this.position = position; + } + } + + private interface Node { + + Predicate resolve(); + } + + private static class AndNode implements Node { + + private final Node node1, node2; + + private AndNode(final Node node1, final Node node2) { + this.node1 = node1; + this.node2 = node2; + } + + @Override + public Predicate resolve() { + return new AndPredicate(node1.resolve(), node2.resolve()); + } + } + + + private static class OrNode implements Node { + + private final Node node1, node2; + + private OrNode(final Node node1, final Node node2) { + this.node1 = node1; + this.node2 = node2; + } + + @Override + public Predicate resolve() { + return new OrPredicate(node1.resolve(), node2.resolve()); + } + } + + + private static class NotNode implements Node { + + private final Node node; + + private NotNode(final Node node) { + this.node = node; + } + + @Override + public Predicate resolve() { + return new NotPredicate(node.resolve()); + } + } + + private static class BuilderNode implements Node { + + private final PredicateBuilder builder; + private final Map parameters; + + private BuilderNode(final PredicateBuilder builder) { + this.builder = builder; + this.parameters = Collections.emptyMap(); + } + + private BuilderNode(final PredicateBuilder builder, final Map parameters) { + this.builder = builder; + this.parameters = parameters; + } + + @Override + public Predicate resolve() { + return builder.build(parameters); + } + } + + private static class PredicateNode implements Node { + + private final Predicate predicate; + + private PredicateNode(final Predicate predicate) { + this.predicate = predicate; + } + + @Override + public Predicate resolve() { + return predicate; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/predicate/Predicates.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/Predicates.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/Predicates.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,228 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; + +/** + * Utility class used for creating predicates + * + * + * @author Stuart Douglas + */ +public class Predicates { + + /** + * Creates a procedure that returns true if the given ExchangeAttributes are equal. + * @param attributes to be compared in the predictor. + * @return A new EqualsPredicate. + */ + public static Predicate equals(final ExchangeAttribute[] attributes){ + return new EqualsPredicate(attributes); + } + + /** + * Creates a predicate that returns true if an only if the given predicates all + * return true. + */ + public static Predicate and(final Predicate... predicates) { + return new AndPredicate(predicates); + } + + /** + * Creates a predicate that returns true if any of the given predicates + * return true. + */ + public static Predicate or(final Predicate... predicates) { + return new OrPredicate(predicates); + } + + /** + * Creates a predicate that returns true if the given predicate returns + * false. + */ + public static Predicate not(final Predicate predicate) { + return new NotPredicate(predicate); + } + + /** + * Creates a predicate that returns true if the given path matches exactly. + */ + public static Predicate path(final String path) { + return new PathMatchPredicate(path); + } + + /** + * Creates a predicate that returns true if any of the given paths match exactly. + */ + public static Predicate paths(final String... paths) { + final PathMatchPredicate[] predicates = new PathMatchPredicate[paths.length]; + for (int i = 0; i < paths.length; ++i) { + predicates[i] = new PathMatchPredicate(paths[i]); + } + return or(predicates); + } + + /** + * Creates a predicate that returns true if the request path ends with the provided suffix. + */ + public static Predicate suffix(final String path) { + return new PathSuffixPredicate(path); + } + + /** + * Creates a predicate that returns true if the request path ends with any of the provided suffixes. + */ + public static Predicate suffixes(final String... paths) { + if(paths.length == 1) { + return suffix(paths[0]); + } + final PathSuffixPredicate[] predicates = new PathSuffixPredicate[paths.length]; + for (int i = 0; i < paths.length; ++i) { + predicates[i] = new PathSuffixPredicate(paths[i]); + } + return or(predicates); + } + + /** + * Creates a predicate that returns true if the given relative path starts with the provided prefix. + */ + public static Predicate prefix(final String path) { + return new PathPrefixPredicate(path); + } + + /** + * Creates a predicate that returns true if the relative request path matches any of the provided prefixes. + */ + public static Predicate prefixes(final String... paths) { + return new PathPrefixPredicate(paths); + } + + /** + * Predicate that returns true if the Content-Size of a request is above a + * given value. + * + * @author Stuart Douglas + */ + public static Predicate maxContentSize(final long size) { + return new MaxContentSizePredicate(size); + } + + /** + * Predicate that returns true if the Content-Size of a request is below a + * given value. + */ + public static Predicate minContentSize(final long size) { + return new MinContentSizePredicate(size); + } + + /** + * Prediction which always returns true + */ + public static Predicate truePredicate() { + return TruePredicate.instance(); + } + + /** + * Predicate which always returns false. + */ + public static Predicate falsePredicate() { + return FalsePredicate.instance(); + } + + /** + * Return a predicate that will return true if the given attribute is not null and not empty. + * + * @param attribute The attribute to check whether it exists or not. + */ + public static Predicate exists(final ExchangeAttribute attribute) { + return new ExistsPredicate(attribute); + } + + /** + * Returns true if the given attribute is present and contains one of the provided value. + * @param attribute The exchange attribute. + * @param values The values to check for. + */ + public static Predicate contains(final ExchangeAttribute attribute, final String ... values) { + return new ContainsPredicate(attribute, values); + } + + /** + * Creates a predicate that matches the given attribute against a regex. A full match is not required + * @param attribute The exchange attribute to check against. + * @param pattern The pattern to look for. + */ + public static Predicate regex(final ExchangeAttribute attribute, final String pattern) { + return new RegularExpressionPredicate(pattern, attribute); + } + + /** + * Creates a predicate that matches the given attribute against a regex. + * @param requireFullMatch If a full match is required in order to return true. + * @param attribute The attribute to check against. + * @param pattern The pattern to look for. + */ + public static Predicate regex(final ExchangeAttribute attribute, final String pattern, boolean requireFullMatch) { + return new RegularExpressionPredicate(pattern, attribute, requireFullMatch); + } + + /** + * Creates a predicate that matches the given attribute against a regex. + * @param requireFullMatch If a full match is required in order to return true. + * @param attribute The attribute to check against. + * @param pattern The pattern to look for. + */ + public static Predicate regex(final String attribute, final String pattern, final ClassLoader classLoader, final boolean requireFullMatch) { + return new RegularExpressionPredicate(pattern, ExchangeAttributes.parser(classLoader).parse(attribute), requireFullMatch); + } + + /** + * A predicate that returns true if authentication is required + * + * @return A predicate that returns true if authentication is required + */ + public static Predicate authRequired() { + return AuthenticationRequiredPredicate.INSTANCE; + } + + /** + * parses the predicate string, and returns the result, using the TCCL to load predicate definitions + * @param predicate The prediate string + * @return The predicate + */ + public static Predicate parse(final String predicate) { + return PredicateParser.parse(predicate, Thread.currentThread().getContextClassLoader()); + } + + /** + * parses the predicate string, and returns the result + * @param predicate The prediate string + * @param classLoader The class loader to load the predicates from + * @return The predicate + */ + public static Predicate parse(final String predicate, ClassLoader classLoader) { + return PredicateParser.parse(predicate, classLoader); + } + + private Predicates() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/PredicatesHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/PredicatesHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/PredicatesHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,107 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.PredicatedHandler; +import io.undertow.util.AttachmentKey; + +import java.util.TreeMap; + +/** + * Handler that can deal with a large number of predicates. chaining together a large number of {@link io.undertow.predicate.PredicatesHandler.Holder} + * instances will make the stack grow to large, so this class is used that can deal with a large number of predicates. + * + * @author Stuart Douglas + */ +public class PredicatesHandler implements HttpHandler { + + private volatile Holder[] handlers = new Holder[0]; + private volatile HttpHandler next; + + //non-static, so multiple handlers can co-exist + private final AttachmentKey CURRENT_POSITION = AttachmentKey.create(Integer.class); + + public PredicatesHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + final int length = handlers.length; + Integer current = exchange.getAttachment(CURRENT_POSITION); + int pos; + if (current == null) { + pos = 0; + exchange.putAttachment(Predicate.PREDICATE_CONTEXT, new TreeMap()); + } else { + pos = current; + } + for (; pos < length; ++pos) { + final Holder handler = handlers[pos]; + if (handler.predicate.resolve(exchange)) { + exchange.putAttachment(CURRENT_POSITION, pos + 1); + handler.handler.handleRequest(exchange); + return; + } + } + next.handleRequest(exchange); + + } + + /** + * Adds a new predicated handler. + *

+ * + * @param predicate + * @param handlerWrapper + */ + public PredicatesHandler addPredicatedHandler(final Predicate predicate, final HandlerWrapper handlerWrapper) { + Holder[] old = handlers; + Holder[] handlers = new Holder[old.length + 1]; + System.arraycopy(old, 0, handlers, 0, old.length); + handlers[old.length] = new Holder(predicate, handlerWrapper.wrap(this)); + this.handlers = handlers; + return this; + } + + public PredicatesHandler addPredicatedHandler(final PredicatedHandler handler) { + return addPredicatedHandler(handler.getPredicate(), handler.getHandler()); + } + + public void setNext(HttpHandler next) { + this.next = next; + } + + public HttpHandler getNext() { + return next; + } + + private static final class Holder { + final Predicate predicate; + final HttpHandler handler; + + private Holder(Predicate predicate, HttpHandler handler) { + this.predicate = predicate; + this.handler = handler; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/RegularExpressionPredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/RegularExpressionPredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/RegularExpressionPredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,118 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HttpServerExchange; + +/** + * A predicate that does a regex match against an exchange. + *

+ *

+ * By default this match is done against the relative URI, however it is possible to set it to match against other + * exchange attributes. + * + * @author Stuart Douglas + */ +public class RegularExpressionPredicate implements Predicate { + + private final Pattern pattern; + private final ExchangeAttribute matchAttribute; + private final boolean requireFullMatch; + + public RegularExpressionPredicate(final String regex, final ExchangeAttribute matchAttribute, final boolean requireFullMatch) { + this.requireFullMatch = requireFullMatch; + pattern = Pattern.compile(regex); + this.matchAttribute = matchAttribute; + } + + public RegularExpressionPredicate(final String regex, final ExchangeAttribute matchAttribute) { + this(regex, matchAttribute, false); + } + + @Override + public boolean resolve(final HttpServerExchange value) { + Matcher matcher = pattern.matcher(matchAttribute.readAttribute(value)); + final boolean matches; + if (requireFullMatch) { + matches = matcher.matches(); + } else { + matches = matcher.find(); + } + + if (matches) { + Map context = value.getAttachment(PREDICATE_CONTEXT); + if (context != null) { + int count = matcher.groupCount(); + for (int i = 0; i <= count; ++i) { + context.put(Integer.toString(i), matcher.group(i)); + } + } + } + return matches; + } + + public static class Builder implements PredicateBuilder { + + @Override + public String name() { + return "regex"; + } + + @Override + public Map> parameters() { + final Map> params = new HashMap<>(); + params.put("pattern", String.class); + params.put("value", ExchangeAttribute.class); + params.put("full-match", Boolean.class); + return params; + } + + @Override + public Set requiredParameters() { + final Set params = new HashSet<>(); + params.add("pattern"); + return params; + } + + @Override + public String defaultParameter() { + return "pattern"; + } + + @Override + public Predicate build(final Map config) { + ExchangeAttribute value = (ExchangeAttribute) config.get("value"); + if(value == null) { + value = ExchangeAttributes.relativePath(); + } + Boolean fullMatch = (Boolean) config.get("full-match"); + String pattern = (String) config.get("pattern"); + return new RegularExpressionPredicate(pattern, value, fullMatch == null ? false : fullMatch); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/predicate/TruePredicate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/predicate/TruePredicate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/predicate/TruePredicate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.predicate; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +class TruePredicate implements Predicate { + + public static final TruePredicate INSTANCE = new TruePredicate(); + + public static TruePredicate instance() { + return INSTANCE; + } + + @Override + public boolean resolve(final HttpServerExchange value) { + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpClientStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpClientStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpClientStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSinkChannel; + +/** + * @author Stuart Douglas + */ +public class AbstractAjpClientStreamSinkChannel extends AbstractFramedStreamSinkChannel { + protected AbstractAjpClientStreamSinkChannel(AjpClientChannel channel) { + super(channel); + } + + @Override + protected boolean isLastFrame() { + return false; + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpClientStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpClientStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpClientStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,35 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import java.nio.ByteBuffer; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; + +/** + * @author Stuart Douglas + */ +public class AbstractAjpClientStreamSourceChannel extends AbstractFramedStreamSourceChannel { + + + public AbstractAjpClientStreamSourceChannel(AjpClientChannel framedChannel, Pooled data, long frameDataRemaining) { + super(framedChannel, data, frameDataRemaining); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AbstractAjpParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,176 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import io.undertow.util.HttpString; + +/** + * @author Stuart Douglas + */ +abstract class AbstractAjpParser { + + public static final int STRING_LENGTH_MASK = 1 << 31; + + /** + * The length of the string being read + */ + public int stringLength = -1; + + /** + * The current string being read + */ + public StringBuilder currentString; + + /** + * when reading the first byte of an integer this stores the first value. It is set to -1 to signify that + * the first byte has not been read yet. + */ + public int currentIntegerPart = -1; + boolean containsUrlCharacters = false; + public int readHeaders = 0; + + public void reset() { + + stringLength = -1; + currentString = null; + currentIntegerPart = -1; + readHeaders = 0; + } + + public abstract void parse(final ByteBuffer buf) throws IOException; + + protected IntegerHolder parse16BitInteger(ByteBuffer buf) { + if (!buf.hasRemaining()) { + return new IntegerHolder(-1, false); + } + int number = this.currentIntegerPart; + if (number == -1) { + number = (buf.get() & 0xFF); + } + if (buf.hasRemaining()) { + final byte b = buf.get(); + int result = ((0xFF & number) << 8) + (b & 0xFF); + this.currentIntegerPart = -1; + return new IntegerHolder(result, true); + } else { + this.currentIntegerPart = number; + return new IntegerHolder(-1, false); + } + } + + protected StringHolder parseString(ByteBuffer buf, boolean header) { + boolean containsUrlCharacters = this.containsUrlCharacters; + if (!buf.hasRemaining()) { + return new StringHolder(null, false, false); + } + int stringLength = this.stringLength; + if (stringLength == -1) { + int number = buf.get() & 0xFF; + if (buf.hasRemaining()) { + final byte b = buf.get(); + stringLength = ((0xFF & number) << 8) + (b & 0xFF); + } else { + this.stringLength = number | STRING_LENGTH_MASK; + return new StringHolder(null, false, false); + } + } else if ((stringLength & STRING_LENGTH_MASK) != 0) { + int number = stringLength & ~STRING_LENGTH_MASK; + stringLength = ((0xFF & number) << 8) + (buf.get() & 0xFF); + } + if (header && (stringLength & 0xFF00) != 0) { + this.stringLength = -1; + return new StringHolder(headers(stringLength & 0xFF)); + } + if (stringLength == 0xFFFF) { + //OxFFFF means null + this.stringLength = -1; + return new StringHolder(null, true, false); + } + StringBuilder builder = this.currentString; + + if (builder == null) { + builder = new StringBuilder(); + this.currentString = builder; + } + int length = builder.length(); + while (length < stringLength) { + if (!buf.hasRemaining()) { + this.stringLength = stringLength; + this.containsUrlCharacters = containsUrlCharacters; + return new StringHolder(null, false, false); + } + char c = (char) buf.get(); + if(c == '+' || c == '%') { + containsUrlCharacters = true; + } + builder.append(c); + ++length; + } + + if (buf.hasRemaining()) { + buf.get(); //null terminator + this.currentString = null; + this.stringLength = -1; + this.containsUrlCharacters = false; + return new StringHolder(builder.toString(), true, containsUrlCharacters); + } else { + this.stringLength = stringLength; + this.containsUrlCharacters = containsUrlCharacters; + return new StringHolder(null, false, false); + } + } + + public abstract boolean isComplete(); + + protected abstract HttpString headers(int offset); + + protected static class IntegerHolder { + public final int value; + public final boolean readComplete; + + private IntegerHolder(int value, boolean readComplete) { + this.value = value; + this.readComplete = readComplete; + } + } + + protected static class StringHolder { + public final String value; + public final HttpString header; + public final boolean readComplete; + public final boolean containsUrlCharacters; + + private StringHolder(String value, boolean readComplete, boolean containsUrlCharacters) { + this.value = value; + this.readComplete = readComplete; + this.containsUrlCharacters = containsUrlCharacters; + this.header = null; + } + + private StringHolder(HttpString value) { + this.value = null; + this.readComplete = true; + this.header = value; + this.containsUrlCharacters = false; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,278 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_END_RESPONSE; +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_REQUEST_BODY_CHUNK; +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_SEND_BODY_CHUNK; +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_SEND_HEADERS; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.framed.AbstractFramedChannel; +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.util.Attachable; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; + +/** + * AJP client side channel. + * + * @author Stuart Douglas + */ +public class AjpClientChannel extends AbstractFramedChannel { + + private final AbstractAjpParser ajpParser; + + private AjpClientResponseStreamSourceChannel source; + private AjpClientRequestClientStreamSinkChannel sink; + + boolean sinkDone = true; + boolean sourceDone = true; + + private boolean lastFrameSent; + private boolean lastFrameRecieved; + + /** + * Create a new {@link io.undertow.server.protocol.framed.AbstractFramedChannel} + * 8 + * + * @param connectedStreamChannel The {@link org.xnio.channels.ConnectedStreamChannel} over which the WebSocket Frames should get send and received. + * Be aware that it already must be "upgraded". + * @param bufferPool The {@link org.xnio.Pool} which will be used to acquire {@link java.nio.ByteBuffer}'s from. + */ + public AjpClientChannel(StreamConnection connectedStreamChannel, Pool bufferPool) { + super(connectedStreamChannel, bufferPool, AjpClientFramePriority.INSTANCE, null); + ajpParser = new AjpResponseParser(); + } + + @Override + protected AbstractAjpClientStreamSourceChannel createChannel(FrameHeaderData frameHeaderData, Pooled frameData) throws IOException { + if (frameHeaderData instanceof SendHeadersResponse) { + SendHeadersResponse h = (SendHeadersResponse) frameHeaderData; + AjpClientResponseStreamSourceChannel sourceChannel = new AjpClientResponseStreamSourceChannel(this, h.headers, h.statusCode, h.reasonPhrase, frameData, (int) frameHeaderData.getFrameLength()); + this.source = sourceChannel; + return sourceChannel; + } else if (frameHeaderData instanceof RequestBodyChunk) { + RequestBodyChunk r = (RequestBodyChunk) frameHeaderData; + this.sink.chunkRequested(r.getLength()); + frameData.free(); + return null; + } else { + frameData.free(); + throw new RuntimeException("TODO: unkown frame"); + } + + } + + @Override + protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { + ajpParser.parse(data); + if (ajpParser.isComplete()) { + try { + AjpResponseParser parser = (AjpResponseParser) ajpParser; + if (parser.prefix == FRAME_TYPE_SEND_HEADERS) { + return new SendHeadersResponse(parser.statusCode, parser.reasonPhrase, parser.headers); + } else if (parser.prefix == FRAME_TYPE_REQUEST_BODY_CHUNK) { + return new RequestBodyChunk(parser.readBodyChunkSize); + } else if (parser.prefix == FRAME_TYPE_SEND_BODY_CHUNK) { + return new SendBodyChunk(parser.currentIntegerPart + 1); //+1 for the null terminator + } else if (parser.prefix == FRAME_TYPE_END_RESPONSE) { + boolean persistent = parser.currentIntegerPart != 0; + if (!persistent) { + lastFrameRecieved = true; + lastFrameSent = true; + } + return new EndResponse(); + } else { + //TODO: ping pong ETC + UndertowLogger.ROOT_LOGGER.debug("UNKOWN FRAME"); + } + } finally { + ajpParser.reset(); + } + } + return null; + } + + public AjpClientRequestClientStreamSinkChannel sendRequest(final HttpString method, final String path, final HttpString protocol, final HeaderMap headers, final Attachable attachable, ChannelListener finishListener) { + if (!sinkDone || !sourceDone) { + throw UndertowMessages.MESSAGES.ajpRequestAlreadyInProgress(); + } + sinkDone = false; + sourceDone = false; + AjpClientRequestClientStreamSinkChannel ajpClientRequestStreamSinkChannel = new AjpClientRequestClientStreamSinkChannel(this, finishListener, headers, path, method, protocol, attachable); + sink = ajpClientRequestStreamSinkChannel; + source = null; + return ajpClientRequestStreamSinkChannel; + } + + @Override + protected boolean isLastFrameReceived() { + return lastFrameRecieved; + } + + @Override + protected boolean isLastFrameSent() { + return lastFrameSent; + } + + protected void lastDataRead() { + lastFrameRecieved = true; + lastFrameSent = true; + IoUtils.safeClose(this); + } + + @Override + protected void handleBrokenSourceChannel(Throwable e) { + IoUtils.safeClose(source, sink); + UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(e)); + IoUtils.safeClose(this); + } + + @Override + protected void handleBrokenSinkChannel(Throwable e) { + IoUtils.safeClose(source, sink); + UndertowLogger.REQUEST_IO_LOGGER.ioException(new IOException(e)); + IoUtils.safeClose(this); + } + + @Override + protected void closeSubChannels() { + IoUtils.safeClose(source, sink); + } + + void sinkDone() { + sinkDone = true; + if (sourceDone) { + sink = null; + source = null; + } + } + + void sourceDone() { + sourceDone = true; + if (sinkDone) { + sink = null; + source = null; + } else { + sink.startDiscard(); + } + } + + @Override + public boolean isOpen() { + return super.isOpen() && !lastFrameSent && !lastFrameRecieved; + } + + @Override + protected synchronized void recalculateHeldFrames() throws IOException { + super.recalculateHeldFrames(); + } + + class SendHeadersResponse implements FrameHeaderData { + + private final int statusCode; + private final String reasonPhrase; + private final HeaderMap headers; + + SendHeadersResponse(int statusCode, String reasonPhrase, HeaderMap headers) { + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + this.headers = headers; + } + + @Override + public long getFrameLength() { + //zero + return 0; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + return null; + } + } + + + class RequestBodyChunk implements FrameHeaderData { + + private final int length; + + RequestBodyChunk(int length) { + this.length = length; + } + + public int getLength() { + return length; + } + + @Override + public long getFrameLength() { + return 0; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + return null; + } + } + + + class SendBodyChunk implements FrameHeaderData { + + private final int length; + + SendBodyChunk(int length) { + this.length = length; + } + + @Override + public long getFrameLength() { + return length; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + return source; + } + } + + class EndResponse implements FrameHeaderData { + + @Override + public long getFrameLength() { + return 0; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + return source; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientFramePriority.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientFramePriority.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientFramePriority.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,67 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import java.util.Deque; +import java.util.Iterator; +import java.util.List; + +import io.undertow.server.protocol.framed.FramePriority; +import io.undertow.server.protocol.framed.SendFrameHeader; + +/** + * AJP frame priority + * @author Stuart Douglas + */ +class AjpClientFramePriority implements FramePriority{ + + public static AjpClientFramePriority INSTANCE = new AjpClientFramePriority(); + + @Override + public boolean insertFrame(AbstractAjpClientStreamSinkChannel newFrame, List pendingFrames) { + if(newFrame instanceof AjpClientRequestClientStreamSinkChannel) { + SendFrameHeader header = ((AjpClientRequestClientStreamSinkChannel) newFrame).generateSendFrameHeader(); + if(header.getByteBuffer() == null) { + //we clear the header, as we want to generate a new real header when the flow control window is updated + ((AjpClientRequestClientStreamSinkChannel) newFrame).clearHeader(); + return false; + } + } + pendingFrames.add(newFrame); + return true; + } + + @Override + public void frameAdded(AbstractAjpClientStreamSinkChannel addedFrame, List pendingFrames, Deque holdFrames) { + Iterator it = holdFrames.iterator(); + while (it.hasNext()){ + AbstractAjpClientStreamSinkChannel pending = it.next(); + if(pending instanceof AjpClientRequestClientStreamSinkChannel) { + SendFrameHeader header = ((AjpClientRequestClientStreamSinkChannel) pending).generateSendFrameHeader(); + if(header.getByteBuffer() != null) { + pendingFrames.add(pending); + it.remove(); + } else { + //we clear the header, as we want to generate a new real header when the flow control window is updated + ((AjpClientRequestClientStreamSinkChannel) pending).clearHeader(); + } + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientRequestClientStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientRequestClientStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientRequestClientStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,319 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import static io.undertow.protocols.ajp.AjpConstants.ATTR_AUTH_TYPE; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_QUERY_STRING; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_REMOTE_USER; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_ROUTE; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_SECRET; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_SSL_CERT; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_SSL_CIPHER; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_SSL_KEY_SIZE; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_SSL_SESSION; +import static io.undertow.protocols.ajp.AjpConstants.ATTR_STORED_METHOD; +import static io.undertow.protocols.ajp.AjpUtils.notNull; +import static io.undertow.protocols.ajp.AjpUtils.putString; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.ChannelListener; +import org.xnio.Pooled; + +import io.undertow.UndertowMessages; +import io.undertow.client.ProxiedRequestAttachments; +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.Attachable; +import io.undertow.util.FlexBase64; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.ImmediatePooled; + +/** + * AJP stream sink channel that corresponds to a request send from the load balancer to the backend + * + * @author Stuart Douglas + */ +public class AjpClientRequestClientStreamSinkChannel extends AbstractAjpClientStreamSinkChannel { + + private final ChannelListener finishListener; + + private static final int MAX_DATA_SIZE = 8186; + + private final HeaderMap headers; + private final String path; + private final HttpString method; + private final HttpString protocol; + private final Attachable attachable; + + + private boolean firstFrameWritten = false; + private long dataSize; + private int requestedChunkSize = -1; + private SendFrameHeader header; + /** + * When we are the client and the + */ + private boolean discardMode = false; + + AjpClientRequestClientStreamSinkChannel(AjpClientChannel channel, ChannelListener finishListener, HeaderMap headers, String path, HttpString method, HttpString protocol, Attachable attachable) { + super(channel); + this.finishListener = finishListener; + this.headers = headers; + this.path = path; + this.method = method; + this.protocol = protocol; + this.attachable = attachable; + } + + + private SendFrameHeader createFrameHeaderImpl() { + if(discardMode) { + getBuffer().clear(); + getBuffer().flip(); + return new SendFrameHeader(new ImmediatePooled<>(ByteBuffer.wrap(new byte[0]))); + } + Pooled pooledHeaderBuffer = getChannel().getBufferPool().allocate(); + final ByteBuffer buffer = pooledHeaderBuffer.getResource(); + ByteBuffer dataBuffer = getBuffer(); + int dataInBuffer = dataBuffer.remaining(); + if (!firstFrameWritten && requestedChunkSize == 0) { + //we are waiting on a read body chunk + return new SendFrameHeader(dataInBuffer, null); + } + + if (!firstFrameWritten) { + String contentLength = headers.getFirst(Headers.CONTENT_LENGTH); + if (contentLength != null) { + dataSize = Long.parseLong(contentLength); + requestedChunkSize = MAX_DATA_SIZE; + if (dataInBuffer > dataSize) { + throw UndertowMessages.MESSAGES.fixedLengthOverflow(); + } + } else if (isWritesShutdown() && !headers.contains(Headers.TRANSFER_ENCODING)) { + //writes are shut down, go to fixed length + headers.put(Headers.CONTENT_LENGTH, dataInBuffer); + dataSize = dataInBuffer; + requestedChunkSize = MAX_DATA_SIZE; + } else { + headers.put(Headers.TRANSFER_ENCODING, Headers.CHUNKED.toString()); + dataSize = -1; + requestedChunkSize = 0; + } + + firstFrameWritten = true; + final String path; + final String queryString; + int qsIndex = this.path.indexOf('?'); + if (qsIndex == -1) { + path = this.path; + queryString = null; + } else { + path = this.path.substring(0, qsIndex); + queryString = this.path.substring(qsIndex + 1); + } + + buffer.put((byte) 0x12); + buffer.put((byte) 0x34); + buffer.put((byte) 0); //we fill the size in later + buffer.put((byte) 0); + buffer.put((byte) 2); + boolean storeMethod = false; + Integer methodNp = AjpConstants.HTTP_METHODS_MAP.get(method); + if (methodNp == null) { + methodNp = 0xFF; + storeMethod = true; + } + buffer.put((byte) (int) methodNp); + AjpUtils.putHttpString(buffer, protocol); + putString(buffer, path); + putString(buffer, notNull(attachable.getAttachment(ProxiedRequestAttachments.REMOTE_ADDRESS))); + putString(buffer, notNull(attachable.getAttachment(ProxiedRequestAttachments.REMOTE_HOST))); + putString(buffer, notNull(attachable.getAttachment(ProxiedRequestAttachments.SERVER_NAME))); + AjpUtils.putInt(buffer, notNull(attachable.getAttachment(ProxiedRequestAttachments.SERVER_PORT))); + buffer.put((byte) (notNull(attachable.getAttachment(ProxiedRequestAttachments.IS_SSL)) ? 1 : 0)); + + int headers = 0; + //we need to count the headers + final HeaderMap responseHeaders = this.headers; + for (HttpString name : responseHeaders.getHeaderNames()) { + headers += responseHeaders.get(name).size(); + } + + AjpUtils.putInt(buffer, headers); + + + for (final HttpString header : responseHeaders.getHeaderNames()) { + for (String headerValue : responseHeaders.get(header)) { + Integer headerCode = AjpConstants.HEADER_MAP.get(header); + if (headerCode != null) { + AjpUtils.putInt(buffer, headerCode); + } else { + AjpUtils.putHttpString(buffer, header); + } + putString(buffer, headerValue); + } + } + + if (queryString != null) { + buffer.put((byte) ATTR_QUERY_STRING); //query_string + putString(buffer, queryString); + } + String remoteUser = attachable.getAttachment(ProxiedRequestAttachments.REMOTE_USER); + if (remoteUser != null) { + buffer.put((byte) ATTR_REMOTE_USER); + putString(buffer, remoteUser); + } + String authType = attachable.getAttachment(ProxiedRequestAttachments.AUTH_TYPE); + if (authType != null) { + buffer.put((byte) ATTR_AUTH_TYPE); + putString(buffer, authType); + } + String route = attachable.getAttachment(ProxiedRequestAttachments.ROUTE); + if (route != null) { + buffer.put((byte) ATTR_ROUTE); + putString(buffer, route); + } + String sslCert = attachable.getAttachment(ProxiedRequestAttachments.SSL_CERT); + if (sslCert != null) { + buffer.put((byte) ATTR_SSL_CERT); + putString(buffer, sslCert); + } + String sslCypher = attachable.getAttachment(ProxiedRequestAttachments.SSL_CYPHER); + if (sslCypher != null) { + buffer.put((byte) ATTR_SSL_CIPHER); + putString(buffer, sslCypher); + } + byte[] sslSession = attachable.getAttachment(ProxiedRequestAttachments.SSL_SESSION_ID); + if (sslSession != null) { + buffer.put((byte) ATTR_SSL_SESSION); + putString(buffer, FlexBase64.encodeString(sslSession, false)); + } + Integer sslKeySize = attachable.getAttachment(ProxiedRequestAttachments.SSL_KEY_SIZE); + if (sslKeySize != null) { + buffer.put((byte) ATTR_SSL_KEY_SIZE); + putString(buffer, sslKeySize.toString()); + } + String secret = attachable.getAttachment(ProxiedRequestAttachments.SECRET); + if (secret != null) { + buffer.put((byte) ATTR_SECRET); + putString(buffer, secret); + } + + if (storeMethod) { + buffer.put((byte) ATTR_STORED_METHOD); + putString(buffer, method.toString()); + } + buffer.put((byte) 0xFF); + + int dataLength = buffer.position() - 4; + buffer.put(2, (byte) ((dataLength >> 8) & 0xFF)); + buffer.put(3, (byte) (dataLength & 0xFF)); + } + if (dataSize == 0) { + //no data, just write out this frame and we are done + buffer.flip(); + return new SendFrameHeader(pooledHeaderBuffer); + } else if (requestedChunkSize > 0) { + + if (isWritesShutdown() && dataInBuffer == 0) { + buffer.put((byte) 0x12); + buffer.put((byte) 0x34); + buffer.put((byte) 0x00); + buffer.put((byte) 0x02); + buffer.put((byte) 0x00); + buffer.put((byte) 0x00); + buffer.flip(); + return new SendFrameHeader(pooledHeaderBuffer); + } + int remaining = dataInBuffer; + remaining = Math.min(remaining, MAX_DATA_SIZE); + remaining = Math.min(remaining, requestedChunkSize); + int bodySize = remaining + 2; + buffer.put((byte) 0x12); + buffer.put((byte) 0x34); + buffer.put((byte) ((bodySize >> 8) & 0xFF)); + buffer.put((byte) (bodySize & 0xFF)); + buffer.put((byte) ((remaining >> 8) & 0xFF)); + buffer.put((byte) (remaining & 0xFF)); + requestedChunkSize = 0; + if (remaining < dataInBuffer) { + dataBuffer.limit(getBuffer().position() + remaining); + buffer.flip(); + return new SendFrameHeader(dataInBuffer - remaining, pooledHeaderBuffer, dataSize < 0); + } else { + buffer.flip(); + return new SendFrameHeader(0, pooledHeaderBuffer, dataSize < 0); + } + } else { + //chunked. We just write the headers, and leave all the data in the buffer + //they need to send us a read body chunk in order to get any data + buffer.flip(); + if(buffer.remaining() == 0) { + pooledHeaderBuffer.free(); + return new SendFrameHeader(dataInBuffer, null, true); + } + dataBuffer.limit(dataBuffer.position()); + return new SendFrameHeader(dataInBuffer, pooledHeaderBuffer, true); + } + } + + SendFrameHeader generateSendFrameHeader() { + header = createFrameHeaderImpl(); + return header; + } + + void chunkRequested(int size) throws IOException { + requestedChunkSize = size; + getChannel().recalculateHeldFrames(); + } + + public void startDiscard() { + discardMode = true; + try { + getChannel().recalculateHeldFrames(); + } catch (IOException e) { + markBroken(); + } + } + + @Override + protected final SendFrameHeader createFrameHeader() { + SendFrameHeader header = this.header; + this.header = null; + return header; + } + + @Override + protected void handleFlushComplete(boolean finalFrame) { + super.handleFlushComplete(finalFrame); + + if (finalFrame) { + getChannel().sinkDone(); + } + if (finalFrame && finishListener != null) { + finishListener.handleEvent(this); + } + } + + public void clearHeader() { + header = null; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientResponseStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientResponseStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpClientResponseStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,85 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.ChannelListener; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.util.HeaderMap; + +/** + * @author Stuart Douglas + */ +public class AjpClientResponseStreamSourceChannel extends AbstractAjpClientStreamSourceChannel { + + private ChannelListener finishListener; + + private final HeaderMap headers; + private final int statusCode; + private final String reasonPhrase; + + public AjpClientResponseStreamSourceChannel(AjpClientChannel framedChannel, HeaderMap headers, int statusCode, String reasonPhrase, Pooled frameData, int remaining) { + super(framedChannel, frameData, remaining); + this.headers = headers; + this.statusCode = statusCode; + this.reasonPhrase = reasonPhrase; + } + + public HeaderMap getHeaders() { + return headers; + } + + public int getStatusCode() { + return statusCode; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + public void setFinishListener(ChannelListener finishListener) { + this.finishListener = finishListener; + } + + @Override + protected void handleHeaderData(FrameHeaderData headerData) { + if(headerData instanceof AjpClientChannel.EndResponse) { + lastFrame(); + } + } + protected long handleFrameData(Pooled frameData, long frameDataRemaining) { + if(frameDataRemaining > 0 && frameData.getResource().remaining() == frameDataRemaining) { + //there is a null terminator on the end + frameData.getResource().limit(frameData.getResource().limit() - 1); + return frameDataRemaining - 1; + } + return frameDataRemaining; + } + + @Override + protected void complete() throws IOException { + if(finishListener != null) { + getFramedChannel().sourceDone(); + finishListener.handleEvent(this); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpConstants.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpConstants.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpConstants.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,136 @@ +package io.undertow.protocols.ajp; + +import static io.undertow.util.Methods.ACL; +import static io.undertow.util.Methods.BASELINE_CONTROL; +import static io.undertow.util.Methods.CHECKIN; +import static io.undertow.util.Methods.CHECKOUT; +import static io.undertow.util.Methods.COPY; +import static io.undertow.util.Methods.DELETE; +import static io.undertow.util.Methods.GET; +import static io.undertow.util.Methods.HEAD; +import static io.undertow.util.Methods.LABEL; +import static io.undertow.util.Methods.LOCK; +import static io.undertow.util.Methods.MERGE; +import static io.undertow.util.Methods.MKACTIVITY; +import static io.undertow.util.Methods.MKCOL; +import static io.undertow.util.Methods.MKWORKSPACE; +import static io.undertow.util.Methods.MOVE; +import static io.undertow.util.Methods.OPTIONS; +import static io.undertow.util.Methods.POST; +import static io.undertow.util.Methods.PROPFIND; +import static io.undertow.util.Methods.PROPPATCH; +import static io.undertow.util.Methods.PUT; +import static io.undertow.util.Methods.REPORT; +import static io.undertow.util.Methods.SEARCH; +import static io.undertow.util.Methods.TRACE; +import static io.undertow.util.Methods.UNCHECKOUT; +import static io.undertow.util.Methods.UNLOCK; +import static io.undertow.util.Methods.UPDATE; +import static io.undertow.util.Methods.VERSION_CONTROL; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +/** + * @author Stuart Douglas + */ +class AjpConstants { + + + public static final int FRAME_TYPE_SEND_HEADERS = 4; + public static final int FRAME_TYPE_REQUEST_BODY_CHUNK = 6; + public static final int FRAME_TYPE_SEND_BODY_CHUNK = 3; + public static final int FRAME_TYPE_END_RESPONSE = 5; + public static final int FRAME_TYPE_CPONG = 9; + public static final int FRAME_TYPE_CPING = 10; + public static final int FRAME_TYPE_SHUTDOWN = 7; + + + static final Map HEADER_MAP; + static final Map HTTP_METHODS_MAP; + static final HttpString[] HTTP_HEADERS_ARRAY; + + static final int ATTR_CONTEXT = 0x01; + static final int ATTR_SERVLET_PATH = 0x02; + static final int ATTR_REMOTE_USER = 0x03; + static final int ATTR_AUTH_TYPE = 0x04; + static final int ATTR_QUERY_STRING = 0x05; + static final int ATTR_ROUTE = 0x06; + static final int ATTR_SSL_CERT = 0x07; + static final int ATTR_SSL_CIPHER = 0x08; + static final int ATTR_SSL_SESSION = 0x09; + static final int ATTR_REQ_ATTRIBUTE = 0x0A; + static final int ATTR_SSL_KEY_SIZE = 0x0B; + static final int ATTR_SECRET = 0x0C; + static final int ATTR_STORED_METHOD = 0x0D; + static final int ATTR_ARE_DONE = 0xFF; + + + static { + final Map headers = new HashMap<>(); + headers.put(Headers.ACCEPT, 0xA001); + headers.put(Headers.ACCEPT_CHARSET, 0xA002); + headers.put(Headers.ACCEPT_ENCODING, 0xA003); + headers.put(Headers.ACCEPT_LANGUAGE, 0xA004); + headers.put(Headers.AUTHORIZATION, 0xA005); + headers.put(Headers.CONNECTION, 0xA006); + headers.put(Headers.CONTENT_TYPE, 0xA007); + headers.put(Headers.CONTENT_LENGTH, 0xA008); + headers.put(Headers.COOKIE, 0xA009); + headers.put(Headers.COOKIE2, 0xA00A); + headers.put(Headers.HOST, 0xA00B); + headers.put(Headers.PRAGMA, 0xA00C); + headers.put(Headers.REFERER, 0xA00D); + headers.put(Headers.USER_AGENT, 0xA00E); + + HEADER_MAP = Collections.unmodifiableMap(headers); + + final Map methods = new HashMap<>(); + methods.put(OPTIONS, 1); + methods.put(GET, 2); + methods.put(HEAD, 3); + methods.put(POST, 4); + methods.put(PUT, 5); + methods.put(DELETE, 6); + methods.put(TRACE, 7); + methods.put(PROPFIND, 8); + methods.put(PROPPATCH, 9); + methods.put(MKCOL, 10); + methods.put(COPY, 11); + methods.put(MOVE, 12); + methods.put(LOCK, 13); + methods.put(UNLOCK, 14); + methods.put(ACL, 15); + methods.put(REPORT, 16); + methods.put(VERSION_CONTROL, 17); + methods.put(CHECKIN, 18); + methods.put(CHECKOUT, 19); + methods.put(UNCHECKOUT, 20); + methods.put(SEARCH, 21); + methods.put(MKWORKSPACE, 22); + methods.put(UPDATE, 23); + methods.put(LABEL, 24); + methods.put(MERGE, 25); + methods.put(BASELINE_CONTROL, 26); + methods.put(MKACTIVITY, 27); + HTTP_METHODS_MAP = Collections.unmodifiableMap(methods); + + HTTP_HEADERS_ARRAY = new HttpString[]{null, + Headers.CONTENT_TYPE, + Headers.CONTENT_LANGUAGE, + Headers.CONTENT_LENGTH, + Headers.DATE, + Headers.LAST_MODIFIED, + Headers.LOCATION, + Headers.SET_COOKIE, + Headers.SET_COOKIE2, + Headers.SERVLET_ENGINE, + Headers.STATUS, + Headers.WWW_AUTHENTICATE + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpResponseParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpResponseParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpResponseParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,237 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_END_RESPONSE; +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_REQUEST_BODY_CHUNK; +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_SEND_BODY_CHUNK; +import static io.undertow.protocols.ajp.AjpConstants.FRAME_TYPE_SEND_HEADERS; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; + +/** + * Parser used for the client (i.e. load balancer) side of the AJP connection. + * + * @author Stuart Douglas + */ +class AjpResponseParser extends AbstractAjpParser { + + public static final AjpResponseParser INSTANCE = new AjpResponseParser(); + + private static final int AB = ('A' << 8) + 'B'; + + //states + public static final int BEGIN = 0; + public static final int READING_MAGIC_NUMBER = 1; + public static final int READING_DATA_SIZE = 2; + public static final int READING_PREFIX_CODE = 3; + public static final int READING_STATUS_CODE = 4; + public static final int READING_REASON_PHRASE = 5; + public static final int READING_NUM_HEADERS = 6; + public static final int READING_HEADERS = 7; + public static final int READING_PERSISTENT_BOOLEAN = 8; + public static final int READING_BODY_CHUNK_LENGTH = 9; + public static final int DONE = 10; + + //parser states + int state; + byte prefix; + int dataSize; + int numHeaders = 0; + HttpString currentHeader; + + //final states + int statusCode; + String reasonPhrase; + HeaderMap headers = new HeaderMap(); + int readBodyChunkSize; + + public boolean isComplete() { + return state == DONE; + } + + public void reset() { + super.reset(); + state = 0; + prefix = 0; + dataSize = 0; + numHeaders = 0; + currentHeader = null; + + statusCode = 0; + reasonPhrase = null; + headers = new HeaderMap(); + } + + public void parse(final ByteBuffer buf) throws IOException { + if (!buf.hasRemaining()) { + return; + } + switch (this.state) { + case BEGIN: { + IntegerHolder result = parse16BitInteger(buf); + if (!result.readComplete) { + return; + } else { + if (result.value != AB) { + throw new IOException("Wrong magic number"); + } + } + } + case READING_DATA_SIZE: { + IntegerHolder result = parse16BitInteger(buf); + if (!result.readComplete) { + this.state = READING_DATA_SIZE; + return; + } else { + this.dataSize = result.value; + } + } + case READING_PREFIX_CODE: { + if (!buf.hasRemaining()) { + this.state = READING_PREFIX_CODE; + return; + } else { + final byte prefix = buf.get(); + this.prefix = prefix; + if (prefix == FRAME_TYPE_END_RESPONSE) { + this.state = READING_PERSISTENT_BOOLEAN; + break; + } else if (prefix == FRAME_TYPE_SEND_BODY_CHUNK) { + this.state = READING_BODY_CHUNK_LENGTH; + break; + } else if (prefix != FRAME_TYPE_SEND_HEADERS && prefix != FRAME_TYPE_REQUEST_BODY_CHUNK) { + this.state = DONE; + return; + } + } + } + case READING_STATUS_CODE: { + //this state is overloaded for the request size + //when reading state=6 (read_body_chunk requests) + + IntegerHolder result = parse16BitInteger(buf); + if (result.readComplete) { + if (this.prefix == FRAME_TYPE_SEND_HEADERS) { + statusCode = result.value; + } else { + //read body chunk or end result + //a bit hacky + this.state = DONE; + this.readBodyChunkSize = result.value; + return; + } + } else { + this.state = READING_STATUS_CODE; + return; + } + } + case READING_REASON_PHRASE: { + StringHolder result = parseString(buf, false); + if (result.readComplete) { + reasonPhrase = result.value; + //exchange.setRequestURI(result.value); + } else { + this.state = READING_REASON_PHRASE; + return; + } + } + case READING_NUM_HEADERS: { + IntegerHolder result = parse16BitInteger(buf); + if (!result.readComplete) { + this.state = READING_NUM_HEADERS; + return; + } else { + this.numHeaders = result.value; + } + } + case READING_HEADERS: { + int readHeaders = this.readHeaders; + while (readHeaders < this.numHeaders) { + if (this.currentHeader == null) { + StringHolder result = parseString(buf, true); + if (!result.readComplete) { + this.state = READING_HEADERS; + this.readHeaders = readHeaders; + return; + } + if (result.header != null) { + this.currentHeader = result.header; + } else { + this.currentHeader = HttpString.tryFromString(result.value); + } + } + StringHolder result = parseString(buf, false); + if (!result.readComplete) { + this.state = READING_HEADERS; + this.readHeaders = readHeaders; + return; + } + headers.add(this.currentHeader, result.value); + this.currentHeader = null; + ++readHeaders; + } + break; + } + } + + if (state == READING_PERSISTENT_BOOLEAN) { + if (!buf.hasRemaining()) { + return; + } + currentIntegerPart = buf.get(); + this.state = DONE; + return; + } else if (state == READING_BODY_CHUNK_LENGTH) { + IntegerHolder result = parse16BitInteger(buf); + if (result.readComplete) { + this.currentIntegerPart = result.value; + this.state = DONE; + } + return; + } else { + this.state = DONE; + } + } + + @Override + protected HttpString headers(int offset) { + return AjpConstants.HTTP_HEADERS_ARRAY[offset]; + } + + public HeaderMap getHeaders() { + return headers; + } + + public int getStatusCode() { + return statusCode; + } + + public String getReasonPhrase() { + return reasonPhrase; + } + + public int getReadBodyChunkSize() { + return readBodyChunkSize; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/ajp/AjpUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.ajp; + +import java.nio.ByteBuffer; + +import io.undertow.util.HttpString; + +/** + * @author Stuart Douglas + */ +class AjpUtils { + + + static boolean notNull(Boolean attachment) { + return attachment == null ? false : attachment; + } + + static int notNull(Integer attachment) { + return attachment == null ? 0 : attachment; + } + + static String notNull(String attachment) { + return attachment == null ? "" : attachment; + } + + static void putInt(final ByteBuffer buf, int value) { + buf.put((byte) ((value >> 8) & 0xFF)); + buf.put((byte) (value & 0xFF)); + } + + static void putString(final ByteBuffer buf, String value) { + final int length = value.length(); + putInt(buf, length); + for (int i = 0; i < length; ++i) { + buf.put((byte) value.charAt(i)); + } + buf.put((byte) 0); + } + + static void putHttpString(final ByteBuffer buf, HttpString value) { + final int length = value.length(); + putInt(buf, length); + value.appendTo(buf); + buf.put((byte) 0); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/AbstractHttp2StreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/AbstractHttp2StreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/AbstractHttp2StreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSinkChannel; + +/** + * @author Stuart Douglas + */ +public class AbstractHttp2StreamSinkChannel extends AbstractFramedStreamSinkChannel { + + AbstractHttp2StreamSinkChannel(Http2Channel channel) { + super(channel); + } + + @Override + protected boolean isLastFrame() { + return false; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/AbstractHttp2StreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/AbstractHttp2StreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/AbstractHttp2StreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; + +/** + * SPDY stream source channel + * + * @author Stuart Douglas + */ +public class AbstractHttp2StreamSourceChannel extends AbstractFramedStreamSourceChannel { + + AbstractHttp2StreamSourceChannel(Http2Channel framedChannel) { + super(framedChannel); + } + + AbstractHttp2StreamSourceChannel(Http2Channel framedChannel, Pooled data, long frameDataRemaining) { + super(framedChannel, data, frameDataRemaining); + } + + @Override + protected void handleHeaderData(FrameHeaderData headerData) { + //by default we do nothing + } + + @Override + protected Http2Channel getFramedChannel() { + return super.getFramedChannel(); + } + + public Http2Channel getHttp2Channel() { + return getFramedChannel(); + } + + @Override + protected void lastFrame() { + super.lastFrame(); + } + + void rstStream() { + //noop by default + } + + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/ConnectionErrorException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/ConnectionErrorException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/ConnectionErrorException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; + +/** + * Exception that represents a connection error + * + * @author Stuart Douglas + */ +public class ConnectionErrorException extends IOException { + private final int code; + + public ConnectionErrorException(int code) { + this.code = code; + } + + + public ConnectionErrorException(int code, String message) { + super(message); + this.code = code; + } + + public ConnectionErrorException(int code, Throwable cause) { + super(cause); + this.code = code; + } + + public int getCode() { + return code; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/HPackHuffman.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/HPackHuffman.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/HPackHuffman.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,458 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class HPackHuffman { + + private static final HuffmanCode[] HUFFMAN_CODES; + + /** + * array based tree representation of a huffman code. + *

+ * the high two bytes corresponds to the tree node if the bit is set, and the low two bytes for if it is clear + * if the high bit is set it is a terminal node, otherwise it contains the next node position. + */ + private static final int[] DECODING_TABLE; + + private static final int LOW_TERMINAL_BIT = (0b10000000) << 8; + private static final int HIGH_TERMINAL_BIT = (0b10000000) << 24; + private static final int LOW_MASK = 0b0111111111111111; + + + static { + + HuffmanCode[] codes = new HuffmanCode[257]; + + codes[0] = new HuffmanCode(0x1ff8, 13); + codes[1] = new HuffmanCode(0x7fffd8, 23); + codes[2] = new HuffmanCode(0xfffffe2, 28); + codes[3] = new HuffmanCode(0xfffffe3, 28); + codes[4] = new HuffmanCode(0xfffffe4, 28); + codes[5] = new HuffmanCode(0xfffffe5, 28); + codes[6] = new HuffmanCode(0xfffffe6, 28); + codes[7] = new HuffmanCode(0xfffffe7, 28); + codes[8] = new HuffmanCode(0xfffffe8, 28); + codes[9] = new HuffmanCode(0xffffea, 24); + codes[10] = new HuffmanCode(0x3ffffffc, 30); + codes[11] = new HuffmanCode(0xfffffe9, 28); + codes[12] = new HuffmanCode(0xfffffea, 28); + codes[13] = new HuffmanCode(0x3ffffffd, 30); + codes[14] = new HuffmanCode(0xfffffeb, 28); + codes[15] = new HuffmanCode(0xfffffec, 28); + codes[16] = new HuffmanCode(0xfffffed, 28); + codes[17] = new HuffmanCode(0xfffffee, 28); + codes[18] = new HuffmanCode(0xfffffef, 28); + codes[19] = new HuffmanCode(0xffffff0, 28); + codes[20] = new HuffmanCode(0xffffff1, 28); + codes[21] = new HuffmanCode(0xffffff2, 28); + codes[22] = new HuffmanCode(0x3ffffffe, 30); + codes[23] = new HuffmanCode(0xffffff3, 28); + codes[24] = new HuffmanCode(0xffffff4, 28); + codes[25] = new HuffmanCode(0xffffff5, 28); + codes[26] = new HuffmanCode(0xffffff6, 28); + codes[27] = new HuffmanCode(0xffffff7, 28); + codes[28] = new HuffmanCode(0xffffff8, 28); + codes[29] = new HuffmanCode(0xffffff9, 28); + codes[30] = new HuffmanCode(0xffffffa, 28); + codes[31] = new HuffmanCode(0xffffffb, 28); + codes[32] = new HuffmanCode(0x14, 6); + codes[33] = new HuffmanCode(0x3f8, 10); + codes[34] = new HuffmanCode(0x3f9, 10); + codes[35] = new HuffmanCode(0xffa, 12); + codes[36] = new HuffmanCode(0x1ff9, 13); + codes[37] = new HuffmanCode(0x15, 6); + codes[38] = new HuffmanCode(0xf8, 8); + codes[39] = new HuffmanCode(0x7fa, 11); + codes[40] = new HuffmanCode(0x3fa, 10); + codes[41] = new HuffmanCode(0x3fb, 10); + codes[42] = new HuffmanCode(0xf9, 8); + codes[43] = new HuffmanCode(0x7fb, 11); + codes[44] = new HuffmanCode(0xfa, 8); + codes[45] = new HuffmanCode(0x16, 6); + codes[46] = new HuffmanCode(0x17, 6); + codes[47] = new HuffmanCode(0x18, 6); + codes[48] = new HuffmanCode(0x0, 5); + codes[49] = new HuffmanCode(0x1, 5); + codes[50] = new HuffmanCode(0x2, 5); + codes[51] = new HuffmanCode(0x19, 6); + codes[52] = new HuffmanCode(0x1a, 6); + codes[53] = new HuffmanCode(0x1b, 6); + codes[54] = new HuffmanCode(0x1c, 6); + codes[55] = new HuffmanCode(0x1d, 6); + codes[56] = new HuffmanCode(0x1e, 6); + codes[57] = new HuffmanCode(0x1f, 6); + codes[58] = new HuffmanCode(0x5c, 7); + codes[59] = new HuffmanCode(0xfb, 8); + codes[60] = new HuffmanCode(0x7ffc, 15); + codes[61] = new HuffmanCode(0x20, 6); + codes[62] = new HuffmanCode(0xffb, 12); + codes[63] = new HuffmanCode(0x3fc, 10); + codes[64] = new HuffmanCode(0x1ffa, 13); + codes[65] = new HuffmanCode(0x21, 6); + codes[66] = new HuffmanCode(0x5d, 7); + codes[67] = new HuffmanCode(0x5e, 7); + codes[68] = new HuffmanCode(0x5f, 7); + codes[69] = new HuffmanCode(0x60, 7); + codes[70] = new HuffmanCode(0x61, 7); + codes[71] = new HuffmanCode(0x62, 7); + codes[72] = new HuffmanCode(0x63, 7); + codes[73] = new HuffmanCode(0x64, 7); + codes[74] = new HuffmanCode(0x65, 7); + codes[75] = new HuffmanCode(0x66, 7); + codes[76] = new HuffmanCode(0x67, 7); + codes[77] = new HuffmanCode(0x68, 7); + codes[78] = new HuffmanCode(0x69, 7); + codes[79] = new HuffmanCode(0x6a, 7); + codes[80] = new HuffmanCode(0x6b, 7); + codes[81] = new HuffmanCode(0x6c, 7); + codes[82] = new HuffmanCode(0x6d, 7); + codes[83] = new HuffmanCode(0x6e, 7); + codes[84] = new HuffmanCode(0x6f, 7); + codes[85] = new HuffmanCode(0x70, 7); + codes[86] = new HuffmanCode(0x71, 7); + codes[87] = new HuffmanCode(0x72, 7); + codes[88] = new HuffmanCode(0xfc, 8); + codes[89] = new HuffmanCode(0x73, 7); + codes[90] = new HuffmanCode(0xfd, 8); + codes[91] = new HuffmanCode(0x1ffb, 13); + codes[92] = new HuffmanCode(0x7fff0, 19); + codes[93] = new HuffmanCode(0x1ffc, 13); + codes[94] = new HuffmanCode(0x3ffc, 14); + codes[95] = new HuffmanCode(0x22, 6); + codes[96] = new HuffmanCode(0x7ffd, 15); + codes[97] = new HuffmanCode(0x3, 5); + codes[98] = new HuffmanCode(0x23, 6); + codes[99] = new HuffmanCode(0x4, 5); + codes[100] = new HuffmanCode(0x24, 6); + codes[101] = new HuffmanCode(0x5, 5); + codes[102] = new HuffmanCode(0x25, 6); + codes[103] = new HuffmanCode(0x26, 6); + codes[104] = new HuffmanCode(0x27, 6); + codes[105] = new HuffmanCode(0x6, 5); + codes[106] = new HuffmanCode(0x74, 7); + codes[107] = new HuffmanCode(0x75, 7); + codes[108] = new HuffmanCode(0x28, 6); + codes[109] = new HuffmanCode(0x29, 6); + codes[110] = new HuffmanCode(0x2a, 6); + codes[111] = new HuffmanCode(0x7, 5); + codes[112] = new HuffmanCode(0x2b, 6); + codes[113] = new HuffmanCode(0x76, 7); + codes[114] = new HuffmanCode(0x2c, 6); + codes[115] = new HuffmanCode(0x8, 5); + codes[116] = new HuffmanCode(0x9, 5); + codes[117] = new HuffmanCode(0x2d, 6); + codes[118] = new HuffmanCode(0x77, 7); + codes[119] = new HuffmanCode(0x78, 7); + codes[120] = new HuffmanCode(0x79, 7); + codes[121] = new HuffmanCode(0x7a, 7); + codes[122] = new HuffmanCode(0x7b, 7); + codes[123] = new HuffmanCode(0x7ffe, 15); + codes[124] = new HuffmanCode(0x7fc, 11); + codes[125] = new HuffmanCode(0x3ffd, 14); + codes[126] = new HuffmanCode(0x1ffd, 13); + codes[127] = new HuffmanCode(0xffffffc, 28); + codes[128] = new HuffmanCode(0xfffe6, 20); + codes[129] = new HuffmanCode(0x3fffd2, 22); + codes[130] = new HuffmanCode(0xfffe7, 20); + codes[131] = new HuffmanCode(0xfffe8, 20); + codes[132] = new HuffmanCode(0x3fffd3, 22); + codes[133] = new HuffmanCode(0x3fffd4, 22); + codes[134] = new HuffmanCode(0x3fffd5, 22); + codes[135] = new HuffmanCode(0x7fffd9, 23); + codes[136] = new HuffmanCode(0x3fffd6, 22); + codes[137] = new HuffmanCode(0x7fffda, 23); + codes[138] = new HuffmanCode(0x7fffdb, 23); + codes[139] = new HuffmanCode(0x7fffdc, 23); + codes[140] = new HuffmanCode(0x7fffdd, 23); + codes[141] = new HuffmanCode(0x7fffde, 23); + codes[142] = new HuffmanCode(0xffffeb, 24); + codes[143] = new HuffmanCode(0x7fffdf, 23); + codes[144] = new HuffmanCode(0xffffec, 24); + codes[145] = new HuffmanCode(0xffffed, 24); + codes[146] = new HuffmanCode(0x3fffd7, 22); + codes[147] = new HuffmanCode(0x7fffe0, 23); + codes[148] = new HuffmanCode(0xffffee, 24); + codes[149] = new HuffmanCode(0x7fffe1, 23); + codes[150] = new HuffmanCode(0x7fffe2, 23); + codes[151] = new HuffmanCode(0x7fffe3, 23); + codes[152] = new HuffmanCode(0x7fffe4, 23); + codes[153] = new HuffmanCode(0x1fffdc, 21); + codes[154] = new HuffmanCode(0x3fffd8, 22); + codes[155] = new HuffmanCode(0x7fffe5, 23); + codes[156] = new HuffmanCode(0x3fffd9, 22); + codes[157] = new HuffmanCode(0x7fffe6, 23); + codes[158] = new HuffmanCode(0x7fffe7, 23); + codes[159] = new HuffmanCode(0xffffef, 24); + codes[160] = new HuffmanCode(0x3fffda, 22); + codes[161] = new HuffmanCode(0x1fffdd, 21); + codes[162] = new HuffmanCode(0xfffe9, 20); + codes[163] = new HuffmanCode(0x3fffdb, 22); + codes[164] = new HuffmanCode(0x3fffdc, 22); + codes[165] = new HuffmanCode(0x7fffe8, 23); + codes[166] = new HuffmanCode(0x7fffe9, 23); + codes[167] = new HuffmanCode(0x1fffde, 21); + codes[168] = new HuffmanCode(0x7fffea, 23); + codes[169] = new HuffmanCode(0x3fffdd, 22); + codes[170] = new HuffmanCode(0x3fffde, 22); + codes[171] = new HuffmanCode(0xfffff0, 24); + codes[172] = new HuffmanCode(0x1fffdf, 21); + codes[173] = new HuffmanCode(0x3fffdf, 22); + codes[174] = new HuffmanCode(0x7fffeb, 23); + codes[175] = new HuffmanCode(0x7fffec, 23); + codes[176] = new HuffmanCode(0x1fffe0, 21); + codes[177] = new HuffmanCode(0x1fffe1, 21); + codes[178] = new HuffmanCode(0x3fffe0, 22); + codes[179] = new HuffmanCode(0x1fffe2, 21); + codes[180] = new HuffmanCode(0x7fffed, 23); + codes[181] = new HuffmanCode(0x3fffe1, 22); + codes[182] = new HuffmanCode(0x7fffee, 23); + codes[183] = new HuffmanCode(0x7fffef, 23); + codes[184] = new HuffmanCode(0xfffea, 20); + codes[185] = new HuffmanCode(0x3fffe2, 22); + codes[186] = new HuffmanCode(0x3fffe3, 22); + codes[187] = new HuffmanCode(0x3fffe4, 22); + codes[188] = new HuffmanCode(0x7ffff0, 23); + codes[189] = new HuffmanCode(0x3fffe5, 22); + codes[190] = new HuffmanCode(0x3fffe6, 22); + codes[191] = new HuffmanCode(0x7ffff1, 23); + codes[192] = new HuffmanCode(0x3ffffe0, 26); + codes[193] = new HuffmanCode(0x3ffffe1, 26); + codes[194] = new HuffmanCode(0xfffeb, 20); + codes[195] = new HuffmanCode(0x7fff1, 19); + codes[196] = new HuffmanCode(0x3fffe7, 22); + codes[197] = new HuffmanCode(0x7ffff2, 23); + codes[198] = new HuffmanCode(0x3fffe8, 22); + codes[199] = new HuffmanCode(0x1ffffec, 25); + codes[200] = new HuffmanCode(0x3ffffe2, 26); + codes[201] = new HuffmanCode(0x3ffffe3, 26); + codes[202] = new HuffmanCode(0x3ffffe4, 26); + codes[203] = new HuffmanCode(0x7ffffde, 27); + codes[204] = new HuffmanCode(0x7ffffdf, 27); + codes[205] = new HuffmanCode(0x3ffffe5, 26); + codes[206] = new HuffmanCode(0xfffff1, 24); + codes[207] = new HuffmanCode(0x1ffffed, 25); + codes[208] = new HuffmanCode(0x7fff2, 19); + codes[209] = new HuffmanCode(0x1fffe3, 21); + codes[210] = new HuffmanCode(0x3ffffe6, 26); + codes[211] = new HuffmanCode(0x7ffffe0, 27); + codes[212] = new HuffmanCode(0x7ffffe1, 27); + codes[213] = new HuffmanCode(0x3ffffe7, 26); + codes[214] = new HuffmanCode(0x7ffffe2, 27); + codes[215] = new HuffmanCode(0xfffff2, 24); + codes[216] = new HuffmanCode(0x1fffe4, 21); + codes[217] = new HuffmanCode(0x1fffe5, 21); + codes[218] = new HuffmanCode(0x3ffffe8, 26); + codes[219] = new HuffmanCode(0x3ffffe9, 26); + codes[220] = new HuffmanCode(0xffffffd, 28); + codes[221] = new HuffmanCode(0x7ffffe3, 27); + codes[222] = new HuffmanCode(0x7ffffe4, 27); + codes[223] = new HuffmanCode(0x7ffffe5, 27); + codes[224] = new HuffmanCode(0xfffec, 20); + codes[225] = new HuffmanCode(0xfffff3, 24); + codes[226] = new HuffmanCode(0xfffed, 20); + codes[227] = new HuffmanCode(0x1fffe6, 21); + codes[228] = new HuffmanCode(0x3fffe9, 22); + codes[229] = new HuffmanCode(0x1fffe7, 21); + codes[230] = new HuffmanCode(0x1fffe8, 21); + codes[231] = new HuffmanCode(0x7ffff3, 23); + codes[232] = new HuffmanCode(0x3fffea, 22); + codes[233] = new HuffmanCode(0x3fffeb, 22); + codes[234] = new HuffmanCode(0x1ffffee, 25); + codes[235] = new HuffmanCode(0x1ffffef, 25); + codes[236] = new HuffmanCode(0xfffff4, 24); + codes[237] = new HuffmanCode(0xfffff5, 24); + codes[238] = new HuffmanCode(0x3ffffea, 26); + codes[239] = new HuffmanCode(0x7ffff4, 23); + codes[240] = new HuffmanCode(0x3ffffeb, 26); + codes[241] = new HuffmanCode(0x7ffffe6, 27); + codes[242] = new HuffmanCode(0x3ffffec, 26); + codes[243] = new HuffmanCode(0x3ffffed, 26); + codes[244] = new HuffmanCode(0x7ffffe7, 27); + codes[245] = new HuffmanCode(0x7ffffe8, 27); + codes[246] = new HuffmanCode(0x7ffffe9, 27); + codes[247] = new HuffmanCode(0x7ffffea, 27); + codes[248] = new HuffmanCode(0x7ffffeb, 27); + codes[249] = new HuffmanCode(0xffffffe, 28); + codes[250] = new HuffmanCode(0x7ffffec, 27); + codes[251] = new HuffmanCode(0x7ffffed, 27); + codes[252] = new HuffmanCode(0x7ffffee, 27); + codes[253] = new HuffmanCode(0x7ffffef, 27); + codes[254] = new HuffmanCode(0x7fffff0, 27); + codes[255] = new HuffmanCode(0x3ffffee, 26); + codes[256] = new HuffmanCode(0x3fffffff, 30); + HUFFMAN_CODES = codes; + + //lengths determined by experimentation, just set it to something large then see how large it actually ends up + int[] codingTree = new int[256]; + //the current position in the tree + int pos = 0; + int allocated = 1; //the next position to allocate to + //map of the current state at a given position + //only used while building the tree + HuffmanCode[] currentCode = new HuffmanCode[256]; + currentCode[0] = new HuffmanCode(0, 0); + + final Set allCodes = new HashSet<>(); + allCodes.addAll(Arrays.asList(HUFFMAN_CODES)); + + while (!allCodes.isEmpty()) { + int length = currentCode[pos].length; + int code = currentCode[pos].value; + + int newLength = length + 1; + HuffmanCode high = new HuffmanCode(code << 1 | 1, newLength); + HuffmanCode low = new HuffmanCode(code << 1, newLength); + int newVal = 0; + boolean highTerminal = allCodes.remove(high); + if (highTerminal) { + //bah, linear search + int i = 0; + for (i = 0; i < codes.length; ++i) { + if (codes[i].equals(high)) { + break; + } + } + newVal = LOW_TERMINAL_BIT | i; + } else { + int highPos = allocated++; + currentCode[highPos] = high; + newVal = highPos; + } + newVal <<= 16; + boolean lowTerminal = allCodes.remove(low); + if (lowTerminal) { + //bah, linear search + int i = 0; + for (i = 0; i < codes.length; ++i) { + if (codes[i].equals(low)) { + break; + } + } + newVal |= LOW_TERMINAL_BIT | i; + } else { + int lowPos = allocated++; + currentCode[lowPos] = low; + newVal |= lowPos; + } + codingTree[pos] = newVal; + pos++; + } + DECODING_TABLE = codingTree; + } + + /** + * Decodes a huffman encoded string into the target StringBuilder. There must be enough space left in the buffer + * for this method to succeed. + * + * @param data The byte buffer + * @param length The data length + * @param target The target for the decompressed data + */ + public static void decode(ByteBuffer data, int length, StringBuilder target) { + assert data.remaining() >= length; + int treePos = 0; + for (int i = 0; i < length; ++i) { + byte b = data.get(); + int bitPos = 7; + while (bitPos >= 0) { + int val = DECODING_TABLE[treePos]; + if (((1 << bitPos) & b) == 0) { + //bit not set, we want the lower part of the tree + if ((val & LOW_TERMINAL_BIT) == 0) { + treePos = val & LOW_MASK; + } else { + target.append((char) (val & LOW_MASK)); + treePos = 0; + } + } else { + //bit not set, we want the lower part of the tree + if ((val & HIGH_TERMINAL_BIT) == 0) { + treePos = (val >> 16) & LOW_MASK; + } else { + target.append((char) ((val >> 16) & LOW_MASK)); + treePos = 0; + } + } + bitPos--; + } + } + } + + protected static class HuffmanCode { + /** + * The value of the least significan't bits of the code + */ + int value; + /** + * length of the code, in bits + */ + int length; + + public HuffmanCode(int value, int length) { + this.value = value; + this.length = length; + } + + public int getValue() { + return value; + } + + public int getLength() { + return length; + } + + @Override + public boolean equals(Object o) { + + + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + HuffmanCode that = (HuffmanCode) o; + + if (length != that.length) return false; + if (value != that.value) return false; + + return true; + } + + @Override + public int hashCode() { + int result = value; + result = 31 * result + length; + return result; + } + + @Override + public String toString() { + return "HuffmanCode{" + + "value=" + value + + ", length=" + length + + '}'; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Hpack.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Hpack.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Hpack.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,204 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +import io.undertow.util.HttpString; + +/** + * @author Stuart Douglas + */ +public class Hpack { + + public static final int DEFAULT_TABLE_SIZE = 4096; + + /** + * table that contains powers of two, + * used as both bitmask and to quickly calculate 2^n + */ + private static final int[] PREFIX_TABLE; + + + static final HeaderField[] STATIC_TABLE; + public static final int STATIC_TABLE_LENGTH; + + static { + PREFIX_TABLE = new int[32]; + for (int i = 0; i < 32; ++i) { + int n = 0; + for (int j = 0; j < i; ++j) { + n = n << 1; + n |= 1; + } + PREFIX_TABLE[i] = n; + } + + HeaderField[] fields = new HeaderField[62]; + //note that zero is not used + fields[1] = new HeaderField(new HttpString(":authority"), null); + fields[2] = new HeaderField(new HttpString(":method"), "GET"); + fields[3] = new HeaderField(new HttpString(":method"), "POST"); + fields[4] = new HeaderField(new HttpString(":path"), "/"); + fields[5] = new HeaderField(new HttpString(":path"), "/index.html"); + fields[6] = new HeaderField(new HttpString(":scheme"), "http"); + fields[7] = new HeaderField(new HttpString(":scheme"), "https"); + fields[8] = new HeaderField(new HttpString(":status"), "200"); + fields[9] = new HeaderField(new HttpString(":status"), "204"); + fields[10] = new HeaderField(new HttpString(":status"), "206"); + fields[11] = new HeaderField(new HttpString(":status"), "304"); + fields[12] = new HeaderField(new HttpString(":status"), "400"); + fields[13] = new HeaderField(new HttpString(":status"), "404"); + fields[14] = new HeaderField(new HttpString(":status"), "500"); + fields[15] = new HeaderField(new HttpString("accept-charset"), null); + fields[16] = new HeaderField(new HttpString("accept-encoding"), "gzip, deflate"); + fields[17] = new HeaderField(new HttpString("accept-language"), null); + fields[18] = new HeaderField(new HttpString("accept-ranges"), null); + fields[19] = new HeaderField(new HttpString("accept"), null); + fields[20] = new HeaderField(new HttpString("access-control-allow-origin"), null); + fields[21] = new HeaderField(new HttpString("age"), null); + fields[22] = new HeaderField(new HttpString("allow"), null); + fields[23] = new HeaderField(new HttpString("authorization"), null); + fields[24] = new HeaderField(new HttpString("cache-control"), null); + fields[25] = new HeaderField(new HttpString("content-disposition"), null); + fields[26] = new HeaderField(new HttpString("content-encoding"), null); + fields[27] = new HeaderField(new HttpString("content-language"), null); + fields[28] = new HeaderField(new HttpString("content-length"), null); + fields[29] = new HeaderField(new HttpString("content-location"), null); + fields[30] = new HeaderField(new HttpString("content-range"), null); + fields[31] = new HeaderField(new HttpString("content-type"), null); + fields[32] = new HeaderField(new HttpString("cookie"), null); + fields[33] = new HeaderField(new HttpString("date"), null); + fields[34] = new HeaderField(new HttpString("etag"), null); + fields[35] = new HeaderField(new HttpString("expect"), null); + fields[36] = new HeaderField(new HttpString("expires"), null); + fields[37] = new HeaderField(new HttpString("from"), null); + fields[38] = new HeaderField(new HttpString("host"), null); + fields[39] = new HeaderField(new HttpString("if-match"), null); + fields[40] = new HeaderField(new HttpString("if-modified-since"), null); + fields[41] = new HeaderField(new HttpString("if-none-match"), null); + fields[42] = new HeaderField(new HttpString("if-range"), null); + fields[43] = new HeaderField(new HttpString("if-unmodified-since"), null); + fields[44] = new HeaderField(new HttpString("last-modified"), null); + fields[45] = new HeaderField(new HttpString("link"), null); + fields[46] = new HeaderField(new HttpString("location"), null); + fields[47] = new HeaderField(new HttpString("max-forwards"), null); + fields[48] = new HeaderField(new HttpString("proxy-authenticate"), null); + fields[49] = new HeaderField(new HttpString("proxy-authorization"), null); + fields[50] = new HeaderField(new HttpString("range"), null); + fields[51] = new HeaderField(new HttpString("referer"), null); + fields[52] = new HeaderField(new HttpString("refresh"), null); + fields[53] = new HeaderField(new HttpString("retry-after"), null); + fields[54] = new HeaderField(new HttpString("server"), null); + fields[55] = new HeaderField(new HttpString("set-cookie"), null); + fields[56] = new HeaderField(new HttpString("strict-transport-security"), null); + fields[57] = new HeaderField(new HttpString("transfer-encoding"), null); + fields[58] = new HeaderField(new HttpString("user-agent"), null); + fields[59] = new HeaderField(new HttpString("vary"), null); + fields[60] = new HeaderField(new HttpString("via"), null); + fields[61] = new HeaderField(new HttpString("www-authenticate"), null); + STATIC_TABLE = fields; + STATIC_TABLE_LENGTH = STATIC_TABLE.length - 1; + } + + static class HeaderField { + final HttpString name; + final String value; + final int size; + + HeaderField(HttpString name, String value) { + this.name = name; + this.value = value; + if (value != null) { + this.size = 32 + name.length() + value.length(); + } else { + this.size = -1; + } + } + } + + /** + * Decodes an integer in the HPACK prefex format. If the return value is -1 + * it means that there was not enough data in the buffer to complete the decoding + * sequence. + *

+ * If this method returns -1 then the source buffer will not have been modified. + * + * @param source The buffer that contains the integer + * @param n The encoding prefix length + * @return The encoded integer, or -1 if there was not enough data + */ + protected static int decodeInteger(ByteBuffer source, int n) { + if (source.remaining() == 0) { + return -1; + } + + int sp = source.position(); + int mask = PREFIX_TABLE[n]; + + int i = mask & source.get(); + int b; + if (i < PREFIX_TABLE[n]) { + return i; + } else { + int m = 0; + do { + if (source.remaining() == 0) { + //we have run out of data + //reset + source.position(sp); + return -1; + } + b = source.get(); + i = i + (b & 127) * (PREFIX_TABLE[m] + 1); + m += 7; + } while ((b & 128) == 128); + } + return i; + } + + /** + * Encodes an integer in the HPACK prefix format. + *

+ * This method assumes that the buffer has already had the first 8-n bits filled. + * As such it will modify the last byte that is already present in the buffer, and + * potentially add more if required + * + * @param source The buffer that contains the integer + * @param value The integer to encode + * @param n The encoding prefix length + */ + protected static void encodeInteger(ByteBuffer source, int value, int n) { + int twoNminus1 = PREFIX_TABLE[n]; + int pos = source.position() - 1; + if (value < twoNminus1) { + source.put(pos, (byte) (source.get(pos) | value)); + } else { + source.put(pos, (byte) (source.get(pos) | twoNminus1)); + value = value - twoNminus1; + while (value >= 128) { + source.put((byte) (value % 128 + 128)); + value = value / 128; + } + source.put((byte) value); + } + } + + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackDecoder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackDecoder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackDecoder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,344 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +import io.undertow.util.HttpString; + +/** + * A decoder for HPACK. + * + * @author Stuart Douglas + */ +public class HpackDecoder extends Hpack { + + /** + * The object that receives the headers that are emitted from this decoder + */ + private HeaderEmitter headerEmitter; + + /** + * The header table + */ + private HeaderField[] headerTable; + + /** + * The current HEAD position of the header table. We use a ring buffer type + * construct as it would be silly to actually shuffle the items around in the + * array. + */ + private int firstSlotPosition = 0; + + /** + * The current table size by index (aka the number of index positions that are filled up) + */ + private int filledTableSlots = 0; + + /** + * the current calculates memory size, as per the HPACK algorithm + */ + private int currentMemorySize = 0; + + /** + * The maximum allowed memory size + */ + private int maxMemorySize; + + private final StringBuilder stringBuilder = new StringBuilder(); + + public HpackDecoder(int maxMemorySize) { + this.maxMemorySize = maxMemorySize; + //as each entry takes up at least 32 + //we make sure the table is as big as we may need + //todo: SCRAP THIS APPROACH AND ALLOW RESIZES + int tableSize = maxMemorySize / 32; + headerTable = new HeaderField[tableSize]; + } + + public HpackDecoder() { + this(DEFAULT_TABLE_SIZE); + } + + /** + * Decodes the provided frame data. If this method leaves data in the buffer then + * this buffer should be compacted so this data is preserved, unless there is no + * more data in which case this should be considered a protocol error. + * + * @param buffer The buffer + */ + public void decode(ByteBuffer buffer, boolean moreData) throws HpackException { + while (buffer.hasRemaining()) { + int originalPos = buffer.position(); + byte b = buffer.get(); + if ((b & 0b10000000) != 0) { + //if the first bit is set it is an indexed header field + buffer.position(buffer.position() - 1); //unget the byte + int index = decodeInteger(buffer, 7); //prefix is 7 + if (index == -1) { + buffer.position(originalPos); + return; + } + handleIndex(index); + } else if ((b & 0b01000000) != 0) { + //Literal Header Field with Incremental Indexing + HttpString headerName = readHeaderName(buffer, 6); + if (headerName == null) { + buffer.position(originalPos); + return; + } + String headerValue = readHpackString(buffer); + if (headerValue == null) { + buffer.position(originalPos); + return; + } + headerEmitter.emitHeader(headerName, headerValue, false); + addEntryToHeaderTable(new HeaderField(headerName, headerValue)); + } else if ((b & 0b11110000) == 0) { + //Literal Header Field without Indexing + HttpString headerName = readHeaderName(buffer, 4); + if (headerName == null) { + buffer.position(originalPos); + return; + } + String headerValue = readHpackString(buffer); + if (headerValue == null) { + buffer.position(originalPos); + return; + } + headerEmitter.emitHeader(headerName, headerValue, false); + } else if ((b & 0b11110000) == 0b00010000) { + //Literal Header Field never indexed + HttpString headerName = readHeaderName(buffer, 4); + if (headerName == null) { + buffer.position(originalPos); + return; + } + String headerValue = readHpackString(buffer); + if (headerValue == null) { + buffer.position(originalPos); + return; + } + headerEmitter.emitHeader(headerName, headerValue, true); + } else if ((b & 0b11100000) == 0b00100000) { + //context update max table size change + if (!handleMaxMemorySizeChange(buffer, originalPos)) { + return; + } + } else { + throw new RuntimeException("Not yet implemented"); + } + } + } + + private boolean handleMaxMemorySizeChange(ByteBuffer buffer, int originalPos) { + buffer.position(buffer.position() - 1); //unget the byte + int size = decodeInteger(buffer, 5); + if (size == -1) { + buffer.position(originalPos); + return false; + } + maxMemorySize = size; + if (currentMemorySize > maxMemorySize) { + int newTableSlots = filledTableSlots; + int tableLength = headerTable.length; + int newSize = currentMemorySize; + while (currentMemorySize > maxMemorySize) { + int clearIndex = firstSlotPosition; + firstSlotPosition++; + if (firstSlotPosition == tableLength) { + firstSlotPosition = 0; + } + HeaderField oldData = headerTable[clearIndex]; + headerTable[clearIndex] = null; + newSize -= oldData.size; + newTableSlots--; + } + this.filledTableSlots = newTableSlots; + currentMemorySize = newSize; + } + return true; + } + + private HttpString readHeaderName(ByteBuffer buffer, int prefixLength) throws HpackException { + buffer.position(buffer.position() - 1); //unget the byte + int index = decodeInteger(buffer, prefixLength); + if (index == -1) { + return null; + } else if (index != 0) { + return handleIndexedHeaderName(index); + } else { + String string = readHpackString(buffer); + if (string == null) { + return null; + } + return new HttpString(string); + } + } + + private String readHpackString(ByteBuffer buffer) throws HpackException { + if (!buffer.hasRemaining()) { + return null; + } + byte data = buffer.get(buffer.position()); + + int length = decodeInteger(buffer, 7); + if (buffer.remaining() < length) { + return null; + } + boolean huffman = (data & 0b10000000) != 0; + if (huffman) { + return readHuffmanString(length, buffer); + } + for (int i = 0; i < length; ++i) { + stringBuilder.append((char) buffer.get()); + } + String ret = stringBuilder.toString(); + stringBuilder.setLength(0); + return ret; + } + + private String readHuffmanString(int length, ByteBuffer buffer) { + HPackHuffman.decode(buffer, length, stringBuilder); + String ret = stringBuilder.toString(); + stringBuilder.setLength(0); + return ret; + } + + private HttpString handleIndexedHeaderName(int index) throws HpackException { + if (index <= STATIC_TABLE_LENGTH) { + return STATIC_TABLE[index].name; + } else { + if (index >= STATIC_TABLE_LENGTH + filledTableSlots) { + throw new HpackException(); + } + int adjustedIndex = getRealIndex(index - STATIC_TABLE_LENGTH); + HeaderField res = headerTable[adjustedIndex]; + if (res == null) { + throw new HpackException(); + } + return res.name; + } + } + + /** + * Handle an indexed header representation + * + * @param index The index + * @throws HpackException + */ + private void handleIndex(int index) throws HpackException { + if (index <= STATIC_TABLE_LENGTH) { + addStaticTableEntry(index); + } else { + int adjustedIndex = getRealIndex(index - STATIC_TABLE_LENGTH); + HeaderField headerField = headerTable[adjustedIndex]; + headerEmitter.emitHeader(headerField.name, headerField.value, false); + } + } + + /** + * because we use a ring buffer type construct, and don't actually shuffle + * items in the array, we need to figure out he real index to use. + *

+ * package private for unit tests + * + * @param index The index from the hpack + * @return the real index into the array + */ + int getRealIndex(int index) { + //the index is one based, but our table is zero based, hence -1 + //also because of our ring buffer setup the indexes are reversed + //index = 1 is at position firstSlotPosition + filledSlots + return (firstSlotPosition + (filledTableSlots - index)) % headerTable.length; + } + + private void addStaticTableEntry(int index) throws HpackException { + //adds an entry from the static table. + //this must be an entry with a value as far as I can determine + HeaderField entry = STATIC_TABLE[index]; + if (entry.value == null) { + throw new HpackException(); + } + headerEmitter.emitHeader(entry.name, entry.value, false); + } + + private void addEntryToHeaderTable(HeaderField entry) { + if (entry.size > maxMemorySize) { + //it is to big to fit, so we just completely clear the table. + filledTableSlots = 0; + return; + } + int newTableSlots = filledTableSlots + 1; + int tableLength = headerTable.length; + int index = (firstSlotPosition + filledTableSlots) % tableLength; + headerTable[index] = entry; + int newSize = currentMemorySize + entry.size; + while (newSize > maxMemorySize) { + int clearIndex = firstSlotPosition; + firstSlotPosition++; + if (firstSlotPosition == tableLength) { + firstSlotPosition = 0; + } + HeaderField oldData = headerTable[clearIndex]; + headerTable[clearIndex] = null; + newSize -= oldData.size; + newTableSlots--; + } + this.filledTableSlots = newTableSlots; + currentMemorySize = newSize; + } + + + public interface HeaderEmitter { + + void emitHeader(HttpString name, String value, boolean neverIndex); + } + + + public HeaderEmitter getHeaderEmitter() { + return headerEmitter; + } + + public void setHeaderEmitter(HeaderEmitter headerEmitter) { + this.headerEmitter = headerEmitter; + } + + //package private fields for unit tests + + int getFirstSlotPosition() { + return firstSlotPosition; + } + + HeaderField[] getHeaderTable() { + return headerTable; + } + + int getFilledTableSlots() { + return filledTableSlots; + } + + int getCurrentMemorySize() { + return currentMemorySize; + } + + int getMaxMemorySize() { + return maxMemorySize; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackEncoder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackEncoder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackEncoder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,221 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +/** + * @author Stuart Douglas + */ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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. + */ + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.HttpString; + +/** + * Encoder for HPACK frames. + * + * @author Stuart Douglas + */ +public class HpackEncoder extends Hpack { + + /** + * current bit pos for Huffman + */ + private int currentBitPos; + + private long headersIterator = -1; + + private HeaderMap currentHeaders; + + private static final Map ENCODING_STATIC_TABLE; + + static { + Map map = new HashMap<>(); + for (int i = 1; i < STATIC_TABLE.length; ++i) { + HeaderField m = STATIC_TABLE[i]; + map.put(m.name, new StaticTableEntry(m.value, i)); + } + ENCODING_STATIC_TABLE = Collections.unmodifiableMap(map); + } + + /** + * the table size, not used at the moment cause this implementation does not use indexing yet + */ + private int tableSize; + + /** + * If a buffer does not have space to put some bytes we decrease its position by one, and store the bits here. + * When a new + */ + private int extraData; + + + public HpackEncoder(int tableSize) { + this.tableSize = tableSize; + } + + /** + * Encodes the headers into a buffer. + *

+ * Note that as it looks like the reference set will be dropped the first instruction that is encoded + * in every case in an instruction to clear the reference set. + *

+ * TODO: this is super crappy at the moment, needs to be fixed up, in particular it does no actual compression at the moment + * + * @param headers + * @param target + */ + public State encode(HeaderMap headers, ByteBuffer target) { + if (target.remaining() < 3) { + return State.UNDERFLOW; + } + long it = headersIterator; + if (headersIterator == -1) { + //new headers map + currentBitPos = 0; + it = headers.fastIterate(); + currentHeaders = headers; + //first push a reference set clear context update + //as the reference set is going away this allows us to be compliant with HPACK 08 without doing a heap of extra useless work + } else { + if (headers != currentHeaders) { + throw new IllegalStateException(); + } + if (currentBitPos > 0) { + //put the extra bits into the new buffer + target.put((byte) extraData); + } + } + while (it != -1) { + HeaderValues values = headers.fiCurrent(it); + //initial super crappy implementation: just write everything out as literal header field never indexed + //makes things much simpler + for (int i = 0; i < values.size(); ++i) { + + int required = 11 + values.getHeaderName().length(); //we use 11 to make sure we have enough room for the variable length itegers + + StaticTableEntry staticTable = ENCODING_STATIC_TABLE.get(values.getHeaderName()); + + String val = values.get(i); + required += (1 + val.length()); + + if (target.remaining() < required) { + this.headersIterator = it; + this.currentBitPos = 0; //we don't use huffman yet + return State.UNDERFLOW; + } + if(staticTable == null) { + target.put((byte) 0); + target.put((byte) 0); //to use encodeInteger we need to place the first byte in the buffer. + encodeInteger(target, values.getHeaderName().length(), 7); + values.getHeaderName().appendTo(target); + } else { + target.put((byte) 0); + encodeInteger(target, staticTable.pos, 4); + } + target.put((byte) 0); //to use encodeInteger we need to place the first byte in the buffer. + encodeInteger(target, val.length(), 7); + for (int j = 0; j < val.length(); ++j) { + target.put((byte) val.charAt(j)); + } + + } + it = headers.fiNext(it); + } + headersIterator = -1; + return State.COMPLETE; + } + + /** + * Push the n least significant bits of value into the buffer + * + * @param buffer The Buffer to push into + * @param value The bits to push into the buffer + * @param n The number of bits to push + * @param currentBitPos Value between 0 and 7 specifying the current location of the pit pointer + */ + static int pushBits(ByteBuffer buffer, int value, int n, int currentBitPos) { + + int bitsLeft = n; + if (currentBitPos != 0) { + int rem = 8 - currentBitPos; + //deal with the first partial byte, after that it is full bytes + int forThisByte = n > rem ? rem : n; + //now we left shift the value to leave only the bits we want + int toPush = value >> (n - forThisByte); + //how far we need to shift right + int shift = 8 - (currentBitPos + forThisByte); + int pos = buffer.position() - 1; + buffer.put(pos, (byte) (buffer.get(pos) | (toPush << shift))); + bitsLeft -= forThisByte; + if (bitsLeft == 0) { + int newPos = currentBitPos + n; + return newPos == 8 ? 0 : newPos; + } + //ok, we have dealt with the first partial byte in the buffer + } + while (true) { + int forThisByte = bitsLeft > 8 ? 8 : bitsLeft; + int toPush = value >> (bitsLeft - forThisByte); + int shift = 8 - forThisByte; + buffer.put((byte) (toPush << shift)); + bitsLeft -= forThisByte; + if (bitsLeft == 0) { + return forThisByte; + } + } + } + + public enum State { + COMPLETE, + UNDERFLOW, + + } + + static final class StaticTableEntry { + final String value; + final int pos; + + private StaticTableEntry(final String value, final int pos) { + this.value = value; + this.pos = pos; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/HpackException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,28 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +/** + * Exception that is thrown when the HPACK compress context is broken. + *

+ * In this case the connection must be closed. + */ +public class HpackException extends Exception { + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2Channel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2Channel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2Channel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,674 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import javax.net.ssl.SSLSession; +import org.xnio.Bits; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.ssl.SslConnection; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.server.protocol.framed.AbstractFramedChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.util.Attachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.AttachmentList; +import io.undertow.util.HeaderMap; + +/** + * SPDY channel. + * + * @author Stuart Douglas + */ +public class Http2Channel extends AbstractFramedChannel implements Attachable { + + public static final String CLEARTEXT_UPGRADE_STRING = "h2c-14"; + public static final String SSL_UPGRADE_STRING = "h2-14"; + + static final int FRAME_TYPE_DATA = 0x00; + static final int FRAME_TYPE_HEADERS = 0x01; + static final int FRAME_TYPE_PRIORITY = 0x02; + static final int FRAME_TYPE_RST_STREAM = 0x03; + static final int FRAME_TYPE_SETTINGS = 0x04; + static final int FRAME_TYPE_PUSH_PROMISE = 0x05; + static final int FRAME_TYPE_PING = 0x06; + static final int FRAME_TYPE_GOAWAY = 0x07; + static final int FRAME_TYPE_WINDOW_UPDATE = 0x08; + static final int FRAME_TYPE_CONTINUATION = 0x09; //hopefully this goes away + + + static final int ERROR_NO_ERROR = 0x00; + static final int ERROR_PROTOCOL_ERROR = 0x01; + static final int ERROR_INTERNAL_ERROR = 0x02; + static final int ERROR_FLOW_CONTROL_ERROR = 0x03; + static final int ERROR_SETTINGS_TIMEOUT = 0x04; + static final int ERROR_STREAM_CLOSED = 0x05; + static final int ERROR_FRAME_SIZE_ERROR = 0x06; + static final int ERROR_REFUSED_STREAM = 0x07; + static final int ERROR_CANCEL = 0x08; + static final int ERROR_COMPRESSION_ERROR = 0x09; + static final int ERROR_CONNECT_ERROR = 0x0a; + static final int ERROR_ENHANCE_YOUR_CALM = 0x0b; + static final int ERROR_INADEQUATE_SECURITY = 0x0c; + + static final int DATA_FLAG_END_STREAM = 0x1; + static final int DATA_FLAG_END_SEGMENT = 0x2; + static final int DATA_FLAG_PADDED = 0x8; + + static final int PING_FRAME_LENGTH = 8; + static final int PING_FLAG_ACK = 0x1; + + static final int HEADERS_FLAG_END_STREAM = 0x1; + static final int HEADERS_FLAG_END_SEGMENT = 0x2; + static final int HEADERS_FLAG_END_HEADERS = 0x4; + static final int HEADERS_FLAG_PADDED = 0x8; + static final int HEADERS_FLAG_PRIORITY = 0x20; + + static final int SETTINGS_FLAG_ACK = 0x1; + + + static final int CONTINUATION_FLAG_END_HEADERS = 0x4; + + static final int DEFAULT_INITIAL_WINDOW_SIZE = 65535; + + public static final byte[] PREFACE_BYTES = { + 0x50, 0x52, 0x49, 0x20, 0x2a, 0x20, 0x48, 0x54, + 0x54, 0x50, 0x2f, 0x32, 0x2e, 0x30, 0x0d, 0x0a, + 0x0d, 0x0a, 0x53, 0x4d, 0x0d, 0x0a, 0x0d, 0x0a}; + + private Http2FrameHeaderParser frameParser; + private final Map incomingStreams = new ConcurrentHashMap<>(); + private final Map outgoingStreams = new ConcurrentHashMap<>(); + + + //local + private int encoderHeaderTableSize; + private boolean enablePush = true; + private volatile int initialSendWindowSize = DEFAULT_INITIAL_WINDOW_SIZE; + private volatile int initialReceiveWindowSize = DEFAULT_INITIAL_WINDOW_SIZE; + private int maxConcurrentStreams = -1; + private int maxFrameSize = 16777215; + private int maxHeaderListSize = -1; + + /** + * How much data we have told the remote endpoint we are prepared to accept. + */ + private volatile int receiveWindowSize = initialReceiveWindowSize; + + /** + * How much data we can send to the remote endpoint, at the connection level. + */ + private volatile int sendWindowSize = initialSendWindowSize; + + private boolean thisGoneAway = false; + private boolean peerGoneAway = false; + + private int streamIdCounter; + private int lastGoodStreamId; + + private final HpackDecoder decoder; + private final HpackEncoder encoder; + + private int prefaceCount; + private boolean initialSettingsReceived; //settings frame must be the first frame we relieve + + private final Map, Object> attachments = Collections.synchronizedMap(new HashMap, Object>()); + + + public Http2Channel(StreamConnection connectedStreamChannel, Pool bufferPool, Pooled data, boolean clientSide, boolean fromUpgrade, OptionMap settings) { + this(connectedStreamChannel, bufferPool, data, clientSide, fromUpgrade, null, settings); + } + + public Http2Channel(StreamConnection connectedStreamChannel, Pool bufferPool, Pooled data, boolean clientSide, boolean fromUpgrade, ByteBuffer initialOtherSideSettings, OptionMap settings) { + super(connectedStreamChannel, bufferPool, Http2FramePriority.INSTANCE, data); + streamIdCounter = clientSide ? (fromUpgrade ? 3 : 1) : 2; + if(initialOtherSideSettings != null) { + Http2SettingsParser parser = new Http2SettingsParser(initialOtherSideSettings.remaining()); + try { + parser.parse(initialOtherSideSettings, new Http2FrameHeaderParser(this)); + updateSettings(parser.getSettings()); + } catch (IOException e) { + IoUtils.safeClose(connectedStreamChannel); + //should never happen + throw new RuntimeException(e); + } + } + sendPreface(); + sendSettings(); + encoderHeaderTableSize = settings.get(UndertowOptions.HTTP2_SETTINGS_HEADER_TABLE_SIZE, Hpack.DEFAULT_TABLE_SIZE); + enablePush = settings.get(UndertowOptions.HTTP2_SETTINGS_ENABLE_PUSH, true); + this.decoder = new HpackDecoder(Hpack.DEFAULT_TABLE_SIZE); + this.encoder = new HpackEncoder(encoderHeaderTableSize); + } + + private void sendSettings() { + List settings = new ArrayList<>(); + settings.add(new Http2Setting(Http2Setting.SETTINGS_HEADER_TABLE_SIZE, encoderHeaderTableSize)); + settings.add(new Http2Setting(Http2Setting.SETTINGS_ENABLE_PUSH, enablePush ? 1 : 0)); + Http2SettingsStreamSinkChannel stream = new Http2SettingsStreamSinkChannel(this, settings); + flushChannel(stream); + } + + private void sendSettingsAck() { + Http2SettingsStreamSinkChannel stream = new Http2SettingsStreamSinkChannel(this); + flushChannel(stream); + } + + private void flushChannel(StreamSinkChannel stream) { + try { + stream.shutdownWrites(); + if (!stream.flush()) { + stream.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, null)); + stream.resumeWrites(); + } + } catch (IOException e) { + //the channel excption handling will close the channel + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + } + } + + private void sendPreface() { + Http2PrefaceStreamSinkChannel preface = new Http2PrefaceStreamSinkChannel(this); + flushChannel(preface); + } + + + @Override + protected AbstractHttp2StreamSourceChannel createChannel(FrameHeaderData frameHeaderData, Pooled frameData) throws IOException { + Http2FrameHeaderParser frameParser = (Http2FrameHeaderParser) frameHeaderData; + AbstractHttp2StreamSourceChannel channel; + if (frameParser.type == FRAME_TYPE_DATA) { + //DATA frames must be already associated with a connection. If it gets here then something is wrong + if (frameParser.streamId == 0) { + //spec explicitly calls this out as a connection error + sendGoAway(ERROR_PROTOCOL_ERROR); + } else { + //the spec says we may send the RST_STREAM in this situation, it seems safest to do so + sendRstStream(frameParser.streamId, ERROR_STREAM_CLOSED); + } + UndertowLogger.REQUEST_LOGGER.tracef("Dropping Frame of length %s for stream %s", frameParser.getFrameLength(), frameParser.streamId); + return null; + } + //note that not all frame types are covered here, as some are only relevant to already active streams + //if which case they are handled by the existing channel support + switch (frameParser.type) { + case FRAME_TYPE_HEADERS: { + Http2HeadersParser parser = (Http2HeadersParser) frameParser.parser; + channel = new Http2StreamSourceChannel(this, frameData, frameHeaderData.getFrameLength(), parser.getHeaderMap(), frameParser.streamId); + lastGoodStreamId = Math.max(lastGoodStreamId, frameParser.streamId); + incomingStreams.put(frameParser.streamId, channel); + if (Bits.anyAreSet(frameParser.flags, HEADERS_FLAG_END_STREAM)) { + channel.lastFrame(); + } + break; + } + case FRAME_TYPE_RST_STREAM: { + Http2RstStreamParser parser = (Http2RstStreamParser) frameParser.parser; + if (frameParser.streamId == 0) { + throw new ConnectionErrorException(Http2Channel.ERROR_PROTOCOL_ERROR, UndertowMessages.MESSAGES.streamIdMustNotBeZeroForFrameType(FRAME_TYPE_RST_STREAM)); + } + channel = new Http2RstStreamStreamSourceChannel(this, frameData, parser.getErrorCode(), frameParser.streamId); + handleRstStream(frameParser.streamId); + break; + } + case FRAME_TYPE_SETTINGS: { + if (!Bits.anyAreSet(frameParser.flags, SETTINGS_FLAG_ACK)) { + updateSettings(((Http2SettingsParser) frameParser.parser).getSettings()); + } else { + sendSettingsAck(); + } + channel = new Http2SettingsStreamSourceChannel(this, frameData, frameParser.getFrameLength(), ((Http2SettingsParser) frameParser.parser).getSettings()); + break; + } + case FRAME_TYPE_PING: { + Http2PingParser pingParser = (Http2PingParser) frameParser.parser; + frameData.free(); + channel = new Http2PingStreamSourceChannel(this, pingParser.getData(), Bits.anyAreSet(frameParser.flags, PING_FLAG_ACK)); + break; + } + case FRAME_TYPE_GOAWAY: { + Http2GoAwayParser spdyGoAwayParser = (Http2GoAwayParser) frameParser.parser; + channel = new Http2GoAwayStreamSourceChannel(this, frameData, frameParser.getFrameLength(), spdyGoAwayParser.getStatusCode(), spdyGoAwayParser.getLastGoodStreamId()); + peerGoneAway = true; + break; + } + case FRAME_TYPE_WINDOW_UPDATE: { + Http2WindowUpdateParser parser = (Http2WindowUpdateParser) frameParser.parser; + handleWindowUpdate(frameParser.streamId, parser.getDeltaWindowSize()); + frameData.free(); + //we don't return window update notifications, they are handled internally + return null; + } + default: { + UndertowLogger.REQUEST_LOGGER.tracef("Dropping frame of length %s and type %s for stream %s as we do not understand this type of frame", frameParser.getFrameLength(), frameParser.type, frameParser.streamId); + return null; + } + } + return channel; + } + + @Override + protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { + if (prefaceCount < PREFACE_BYTES.length) { + while (data.hasRemaining() && prefaceCount < PREFACE_BYTES.length) { + if (data.get() != PREFACE_BYTES[prefaceCount]) { + IoUtils.safeClose(getUnderlyingConnection()); + throw UndertowMessages.MESSAGES.incorrectHttp2Preface(); + } + prefaceCount++; + } + } + Http2FrameHeaderParser frameParser = this.frameParser; + if (frameParser == null) { + this.frameParser = frameParser = new Http2FrameHeaderParser(this); + } + if (!frameParser.handle(data)) { + return null; + } + if (!initialSettingsReceived) { + if (frameParser.type != FRAME_TYPE_SETTINGS) { + UndertowLogger.REQUEST_IO_LOGGER.remoteEndpointFailedToSendInitialSettings(); + markReadsBroken(new IOException()); + } else { + initialSettingsReceived = true; + } + } + this.frameParser = null; + return frameParser; + + } + + protected void lastDataRead() { + if (!peerGoneAway && !thisGoneAway) { + //the peer has performed an unclean close + //we assume something happened to the underlying connection + //we attempt to send our own GOAWAY, however it will probably fail, + //which will trigger a forces close of our write side + sendGoAway(ERROR_CONNECT_ERROR); + peerGoneAway = true; + } + } + + @Override + public boolean isOpen() { + return super.isOpen() && !peerGoneAway && !thisGoneAway; + } + + @Override + protected boolean isLastFrameReceived() { + return peerGoneAway; + } + + @Override + protected boolean isLastFrameSent() { + return peerGoneAway || thisGoneAway; + } + + @Override + protected void handleBrokenSourceChannel(Throwable e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Closing HTTP2 channel to %s due to broken read side", getPeerAddress()); + if (e instanceof ConnectionErrorException) { + sendGoAway(((ConnectionErrorException) e).getCode(), new Http2ControlMessageExceptionHandler()); + } else { + sendGoAway(e instanceof ClosedChannelException ? Http2Channel.ERROR_CONNECT_ERROR : Http2Channel.ERROR_PROTOCOL_ERROR, new Http2ControlMessageExceptionHandler()); + } + } + + @Override + protected void handleBrokenSinkChannel(Throwable e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Closing HTTP2 channel to %s due to broken write side", getPeerAddress()); + //the write side is broken, so we can't even send GO_AWAY + //just tear down the TCP connection + IoUtils.safeClose(this); + } + + @Override + protected void closeSubChannels() { + + for (Map.Entry e : incomingStreams.entrySet()) { + AbstractHttp2StreamSourceChannel receiver = e.getValue(); + if (receiver.isReadResumed()) { + ChannelListeners.invokeChannelListener(receiver.getIoThread(), receiver, ((ChannelListener.SimpleSetter) receiver.getReadSetter()).get()); + } + IoUtils.safeClose(receiver); + } + incomingStreams.clear(); + + for (Map.Entry e : outgoingStreams.entrySet()) { + Http2StreamSinkChannel receiver = e.getValue(); + if (receiver.isWritesShutdown()) { + ChannelListeners.invokeChannelListener(receiver.getIoThread(), receiver, ((ChannelListener.SimpleSetter) receiver.getWriteSetter()).get()); + } + IoUtils.safeClose(receiver); + } + outgoingStreams.clear(); + } + + /** + * Setting have been received from the client + * + * @param settings + */ + synchronized void updateSettings(List settings) { + for (Http2Setting setting : settings) { + if (setting.getId() == Http2Setting.SETTINGS_INITIAL_WINDOW_SIZE) { + int old = initialSendWindowSize; + initialSendWindowSize = setting.getValue(); + int difference = old - initialSendWindowSize; + sendWindowSize += difference; + } + //ignore the rest for now + } + } + + public int getHttp2Version() { + return 3; + } + + public int getInitialSendWindowSize() { + return initialSendWindowSize; + } + + public int getInitialReceiveWindowSize() { + return initialReceiveWindowSize; + } + + public synchronized void handleWindowUpdate(int streamId, int deltaWindowSize) throws IOException { + if (streamId == 0) { + boolean exhausted = sendWindowSize == 0; + sendWindowSize += deltaWindowSize; + if (exhausted) { + notifyFlowControlAllowed(); + } + } else { + Http2StreamSinkChannel stream = outgoingStreams.get(streamId); + if (stream == null) { + //TODO: error handling + } else { + stream.updateFlowControlWindow(deltaWindowSize); + } + } + } + + synchronized void notifyFlowControlAllowed() throws IOException { + super.recalculateHeldFrames(); + } + + public void sendPing(byte[] data) { + sendPing(data, new Http2ControlMessageExceptionHandler()); + } + + public void sendPing(byte[] data, final ChannelExceptionHandler exceptionHandler) { + sendPing(data, exceptionHandler, false); + } + + void sendPing(byte[] data, final ChannelExceptionHandler exceptionHandler, boolean ack) { + Http2PingStreamSinkChannel ping = new Http2PingStreamSinkChannel(this, data, ack); + try { + ping.shutdownWrites(); + if (!ping.flush()) { + ping.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, exceptionHandler)); + ping.resumeWrites(); + } + } catch (IOException e) { + exceptionHandler.handleException(ping, e); + } + } + + public void sendGoAway(int status) { + sendGoAway(status, new Http2ControlMessageExceptionHandler()); + } + + public void sendGoAway(int status, final ChannelExceptionHandler exceptionHandler) { + if (thisGoneAway) { + return; + } + thisGoneAway = true; + Http2GoAwayStreamSinkChannel goAway = new Http2GoAwayStreamSinkChannel(this, status, lastGoodStreamId); + try { + goAway.shutdownWrites(); + if (!goAway.flush()) { + goAway.getWriteSetter().set(ChannelListeners.flushingChannelListener(new ChannelListener() { + @Override + public void handleEvent(Channel channel) { + IoUtils.safeClose(Http2Channel.this); + } + }, exceptionHandler)); + goAway.resumeWrites(); + } else { + IoUtils.safeClose(this); + } + } catch (IOException e) { + exceptionHandler.handleException(goAway, e); + } + } + + public void sendUpdateWindowSize(int streamId, int delta) { + Http2WindowUpdateStreamSinkChannel windowUpdateStreamSinkChannel = new Http2WindowUpdateStreamSinkChannel(this, streamId, delta); + try { + windowUpdateStreamSinkChannel.shutdownWrites(); + if (!windowUpdateStreamSinkChannel.flush()) { + windowUpdateStreamSinkChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new Http2ControlMessageExceptionHandler())); + windowUpdateStreamSinkChannel.resumeWrites(); + } + } catch (IOException e) { + handleBrokenSinkChannel(e); + } + + } + + public SSLSession getSslSession() { + StreamConnection con = getUnderlyingConnection(); + if (con instanceof SslConnection) { + return ((SslConnection) con).getSslSession(); + } + return null; + } + + public synchronized void updateReceiveFlowControlWindow(int read) { + if (read <= 0) { + return; + } + receiveWindowSize -= read; + //TODO: make this configurable, we should be able to set the policy that is used to determine when to update the window size + int initialWindowSize = this.initialReceiveWindowSize; + if (receiveWindowSize < (initialWindowSize / 2)) { + int delta = initialWindowSize - receiveWindowSize; + receiveWindowSize += delta; + sendUpdateWindowSize(0, delta); + } + } + + public synchronized Http2HeadersStreamSinkChannel createStream(HeaderMap requestHeaders) throws IOException { + if (!isOpen()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + int streamId = streamIdCounter; + streamIdCounter += 2; + Http2HeadersStreamSinkChannel spdySynStreamStreamSinkChannel = new Http2HeadersStreamSinkChannel(this, streamId, requestHeaders); + outgoingStreams.put(streamId, spdySynStreamStreamSinkChannel); + return spdySynStreamStreamSinkChannel; + + } + + /** + * Try and decrement the send window by the given amount of bytes. + * + * @param bytesToGrab The amount of bytes the sender is trying to send + * @return The actual amount of bytes the sender can send + */ + synchronized int grabFlowControlBytes(int bytesToGrab) { + int min = Math.min(bytesToGrab, sendWindowSize); + sendWindowSize -= min; + return min; + } + + void registerStreamSink(Http2HeadersStreamSinkChannel synResponse) { + outgoingStreams.put(synResponse.getStreamId(), synResponse); + } + + void removeStreamSink(int streamId) { + outgoingStreams.remove(streamId); + } + + Map getIncomingStreams() { + return incomingStreams; + } + + public boolean isClient() { + return streamIdCounter % 2 == 1; + } + + + HpackEncoder getEncoder() { + return encoder; + } + + HpackDecoder getDecoder() { + return decoder; + } + + @Override + public T getAttachment(AttachmentKey key) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + return (T) attachments.get(key); + } + + @Override + public List getAttachmentList(AttachmentKey> key) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + Object o = attachments.get(key); + if (o == null) { + return Collections.emptyList(); + } + return (List) o; + } + + @Override + public T putAttachment(AttachmentKey key, T value) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + return key.cast(attachments.put(key, key.cast(value))); + } + + @Override + public T removeAttachment(AttachmentKey key) { + return key.cast(attachments.remove(key)); + } + + @Override + public void addToAttachmentList(AttachmentKey> key, T value) { + + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + final Map, Object> attachments = this.attachments; + synchronized (attachments) { + final List list = key.cast(attachments.get(key)); + if (list == null) { + final AttachmentList newList = new AttachmentList((Class) Object.class); + attachments.put(key, newList); + newList.add(value); + } else { + list.add(value); + } + } + } + + public void sendRstStream(int streamId, int statusCode) { + handleRstStream(streamId); + try { + Http2RstStreamSinkChannel channel = new Http2RstStreamSinkChannel(this, streamId, statusCode); + channel.shutdownWrites(); + if (!channel.flush()) { + channel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(AbstractHttp2StreamSinkChannel channel, IOException exception) { + markWritesBroken(exception); + } + })); + channel.resumeWrites(); + } + } catch (IOException e) { + markWritesBroken(e); + } + } + + private void handleRstStream(int streamId) { + AbstractHttp2StreamSourceChannel incoming = incomingStreams.remove(streamId); + if (incoming != null) { + incoming.rstStream(); + } + Http2StreamSinkChannel outgoing = outgoingStreams.remove(streamId); + if (outgoing != null) { + outgoing.rstStream(); + } + } + + /** + * Creates a response stream to respond to the initial HTTP upgrade + * @return + */ + public Http2HeadersStreamSinkChannel createInitialUpgradeResponseStream() { + if(lastGoodStreamId != 0) { + throw new IllegalStateException(); + } + lastGoodStreamId = 1; + Http2HeadersStreamSinkChannel stream = new Http2HeadersStreamSinkChannel(this, 1); + outgoingStreams.put(1, stream); + return stream; + + } + + + private class Http2ControlMessageExceptionHandler implements ChannelExceptionHandler { + @Override + public void handleException(AbstractHttp2StreamSinkChannel channel, IOException exception) { + IoUtils.safeClose(channel); + handleBrokenSinkChannel(exception); + } + } + + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2DataFrameParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2DataFrameParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2DataFrameParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,52 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import org.xnio.Bits; + +/** + * Parses the data frame. If the passing flag has not been set then there is nothing to parse. + * + * @author Stuart Douglas + */ +class Http2DataFrameParser extends Http2PushBackParser { + + private int padding = 0; + + public Http2DataFrameParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser headerParser) { + if (Bits.anyAreClear(headerParser.flags, Http2Channel.DATA_FLAG_PADDED)) { + finish(); + return; + } + if (resource.remaining() > 0) { + padding = resource.get() & 0xFF; + finish(); + } + } + + int getPadding() { + return padding; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2DataStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2DataStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2DataStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,180 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.HeaderMap; +import io.undertow.util.ImmediatePooled; + +/** + * Headers channel + * + * @author Stuart Douglas + */ +public class Http2DataStreamSinkChannel extends Http2StreamSinkChannel { + + private final HeaderMap headers; + + private boolean first = true; + private final HpackEncoder encoder; + private ChannelListener completionListener; + + private final int frameType; + + Http2DataStreamSinkChannel(Http2Channel channel, int streamId, int frameType) { + this(channel, streamId, new HeaderMap(), frameType); + } + + Http2DataStreamSinkChannel(Http2Channel channel, int streamId, HeaderMap headers, int frameType) { + super(channel, streamId); + this.encoder = channel.getEncoder(); + this.headers = headers; + this.frameType = frameType; + } + + @Override + protected SendFrameHeader createFrameHeaderImpl() { + final int fcWindow = grabFlowControlBytes(getBuffer().remaining()); + if (fcWindow == 0 && getBuffer().hasRemaining()) { + //flow control window is exhausted + return new SendFrameHeader(getBuffer().remaining(), null); + } + final boolean finalFrame = isWritesShutdown() && fcWindow >= getBuffer().remaining(); + Pooled firstHeaderBuffer = getChannel().getBufferPool().allocate(); + Pooled[] allHeaderBuffers = null; + ByteBuffer firstBuffer = firstHeaderBuffer.getResource(); + boolean firstFrame = false; + if (first) { + firstFrame = true; + first = false; + //back fill the length + firstBuffer.put((byte) 0); + firstBuffer.put((byte) 0); + firstBuffer.put((byte) 0); + + firstBuffer.put((byte) frameType); //type + firstBuffer.put((byte) ((isWritesShutdown() && !getBuffer().hasRemaining() ? Http2Channel.HEADERS_FLAG_END_STREAM : 0) | Http2Channel.HEADERS_FLAG_END_HEADERS)); //flags + + Http2ProtocolUtils.putInt(firstBuffer, getStreamId()); + + HpackEncoder.State result = encoder.encode(headers, firstBuffer); + Pooled current = firstHeaderBuffer; + int length = firstBuffer.position() - 9; + while (result != HpackEncoder.State.COMPLETE) { + //todo: add some kind of limit here + allHeaderBuffers = allocateAll(allHeaderBuffers, current); + current = allHeaderBuffers[allHeaderBuffers.length - 1]; + result = encoder.encode(headers, current.getResource()); + length += current.getResource().position(); + } + firstBuffer.put(0, (byte) ((length >> 16) & 0xFF)); + firstBuffer.put(1, (byte) ((length >> 8) & 0xFF)); + firstBuffer.put(2, (byte) (length & 0xFF)); + } + + Pooled currentPooled = allHeaderBuffers == null ? firstHeaderBuffer : allHeaderBuffers[allHeaderBuffers.length - 1]; + ByteBuffer currentBuffer = currentPooled.getResource(); + int remainingInBuffer = 0; + if (getBuffer().remaining() > 0) { + if (fcWindow > 0) { + //make sure we have room in the header buffer + if (currentBuffer.remaining() < 8) { + allHeaderBuffers = allocateAll(allHeaderBuffers, currentPooled); + currentPooled = allHeaderBuffers == null ? firstHeaderBuffer : allHeaderBuffers[allHeaderBuffers.length - 1]; + currentBuffer = currentPooled.getResource(); + } + remainingInBuffer = getBuffer().remaining() - fcWindow; + getBuffer().limit(getBuffer().position() + fcWindow); + + currentBuffer.put((byte) ((fcWindow >> 16) & 0xFF)); + currentBuffer.put((byte) ((fcWindow >> 8) & 0xFF)); + currentBuffer.put((byte) (fcWindow & 0xFF)); + currentBuffer.put((byte) Http2Channel.FRAME_TYPE_DATA); //type + currentBuffer.put((byte) (finalFrame ? Http2Channel.HEADERS_FLAG_END_STREAM : 0)); //flags + Http2ProtocolUtils.putInt(currentBuffer, getStreamId()); + + } else { + remainingInBuffer = getBuffer().remaining(); + } + } else if (finalFrame && !firstFrame) { + currentBuffer.put((byte) 0); + currentBuffer.put((byte) 0); + currentBuffer.put((byte) 0); + currentBuffer.put((byte) Http2Channel.FRAME_TYPE_DATA); //type + currentBuffer.put((byte) (Http2Channel.HEADERS_FLAG_END_STREAM & 0xFF)); //flags + Http2ProtocolUtils.putInt(currentBuffer, getStreamId()); + } + if (allHeaderBuffers == null) { + //only one buffer required + currentBuffer.flip(); + return new SendFrameHeader(remainingInBuffer, currentPooled); + } else { + //headers were too big to fit in one buffer + //for now we will just copy them into a big buffer + int length = 0; + for (int i = 0; i < allHeaderBuffers.length; ++i) { + length += allHeaderBuffers[i].getResource().position(); + allHeaderBuffers[i].getResource().flip(); + } + try { + ByteBuffer newBuf = ByteBuffer.allocate(length); + + for (int i = 0; i < allHeaderBuffers.length; ++i) { + newBuf.put(allHeaderBuffers[i].getResource()); + } + newBuf.flip(); + return new SendFrameHeader(remainingInBuffer, new ImmediatePooled(newBuf)); + } finally { + //the allocate can oome + for (int i = 0; i < allHeaderBuffers.length; ++i) { + allHeaderBuffers[i].free(); + } + } + } + + } + + + public HeaderMap getHeaders() { + return headers; + } + + @Override + protected void handleFlushComplete(boolean finalFrame) { + super.handleFlushComplete(finalFrame); + if (finalFrame) { + if (completionListener != null) { + ChannelListeners.invokeChannelListener(this, completionListener); + } + } + } + + public ChannelListener getCompletionListener() { + return completionListener; + } + + public void setCompletionListener(ChannelListener completionListener) { + this.completionListener = completionListener; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2FrameHeaderParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2FrameHeaderParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2FrameHeaderParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,160 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_CONTINUATION; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_DATA; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_GOAWAY; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_HEADERS; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_PUSH_PROMISE; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_RST_STREAM; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_SETTINGS; +import static io.undertow.protocols.http2.Http2Channel.FRAME_TYPE_WINDOW_UPDATE; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.Bits; + +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; + +/** + * @author Stuart Douglas + */ +class Http2FrameHeaderParser implements FrameHeaderData { + + final byte[] header = new byte[9]; + int read = 0; + + int length; + int type; + int flags; + int streamId; + + Http2PushBackParser parser = null; + + private static final int SECOND_RESERVED_MASK = ~(1 << 7); + private Http2Channel http2Channel; + + public Http2FrameHeaderParser(Http2Channel http2Channel) { + this.http2Channel = http2Channel; + } + + public boolean handle(final ByteBuffer byteBuffer) throws IOException { + if (parser == null) { + if (!parseFrameHeader(byteBuffer)) { + return false; + } + switch (type) { + case FRAME_TYPE_DATA: { + parser = new Http2DataFrameParser(length); + break; + } + case FRAME_TYPE_HEADERS: { + parser = new Http2HeadersParser( length, http2Channel.getDecoder()); + break; + } + case FRAME_TYPE_RST_STREAM: { + parser = new Http2RstStreamParser(length); + break; + } + case FRAME_TYPE_CONTINUATION: { + //parser = new Http2HeadersParser(http2Channel.getBufferPool(), http2Channel, length, inflater); + throw new RuntimeException("NYI"); //TODO: continuations + } + case FRAME_TYPE_PUSH_PROMISE: { + throw new RuntimeException("NYI"); //TODO: push promise + // break; + } + case FRAME_TYPE_GOAWAY: { + parser = new Http2GoAwayParser(length); + break; + } + case Http2Channel.FRAME_TYPE_PING: { + if (length != 8) { + throw new ConnectionErrorException(Http2Channel.ERROR_FRAME_SIZE_ERROR, UndertowMessages.MESSAGES.invalidPingSize()); + } + if (streamId != 0) { + throw new ConnectionErrorException(Http2Channel.ERROR_PROTOCOL_ERROR, UndertowMessages.MESSAGES.streamIdMustBeZeroForFrameType(Http2Channel.FRAME_TYPE_PING)); + } + parser = new Http2PingParser(length); + break; + } + case FRAME_TYPE_SETTINGS: { + parser = new Http2SettingsParser(length); + break; + } + case FRAME_TYPE_WINDOW_UPDATE: { + parser = new Http2WindowUpdateParser(length); + break; + } + default: { + return true; + } + } + } + parser.parse(byteBuffer, this); + return parser.isFinished(); + } + + private boolean parseFrameHeader(ByteBuffer byteBuffer) { + while (read < 9 && byteBuffer.hasRemaining()) { + header[read++] = byteBuffer.get(); + } + if (read != 9) { + return false; + } + length = (header[0] & 0xFF) << 16; + length += (header[1] & 0xff) << 8; + length += header[2] & 0xff; + type = header[3] & 0xff; + flags = header[4] & 0xff; + streamId = (header[5] & SECOND_RESERVED_MASK & 0xFF) << 24; + streamId += (header[6] & 0xFF) << 16; + streamId += (header[7] & 0xFF) << 8; + streamId += (header[8] & 0xFF); + return true; + } + + @Override + public long getFrameLength() { + //we only consider data frames to have length, all other frames are fully consumed by header parsing + if (type != FRAME_TYPE_DATA) { + return 0; + } + return length; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + if (type == FRAME_TYPE_DATA || + type == FRAME_TYPE_HEADERS || + type == Http2Channel.FRAME_TYPE_CONTINUATION || + type == Http2Channel.FRAME_TYPE_PRIORITY) { + + if (Bits.anyAreSet(flags, Http2Channel.DATA_FLAG_END_STREAM)) { + return http2Channel.getIncomingStreams().remove(streamId); + } else { + return http2Channel.getIncomingStreams().get(streamId); + } + } + return null; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2FramePriority.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2FramePriority.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2FramePriority.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,71 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.util.Deque; +import java.util.Iterator; +import java.util.List; + +import io.undertow.server.protocol.framed.FramePriority; +import io.undertow.server.protocol.framed.SendFrameHeader; + +/** + * TODO: real priority + * + * @author Stuart Douglas + */ +class Http2FramePriority implements FramePriority { + + public static Http2FramePriority INSTANCE = new Http2FramePriority(); + + @Override + public boolean insertFrame(AbstractHttp2StreamSinkChannel newFrame, List pendingFrames) { + //first deal with flow control + if (newFrame instanceof Http2StreamSinkChannel) { + SendFrameHeader header = ((Http2StreamSinkChannel) newFrame).generateSendFrameHeader(); + //if no header is generated then flow control means we can't send anything + if (header.getByteBuffer() == null) { + //we clear the header, as we want to generate a new real header when the flow control window is updated + ((Http2StreamSinkChannel) newFrame).clearHeader(); + return false; + } + } + + pendingFrames.add(newFrame); + return true; + } + + @Override + public void frameAdded(AbstractHttp2StreamSinkChannel addedFrame, List pendingFrames, Deque holdFrames) { + Iterator it = holdFrames.iterator(); + while (it.hasNext()) { + AbstractHttp2StreamSinkChannel pending = it.next(); + if (pending instanceof Http2StreamSinkChannel) { + SendFrameHeader header = ((Http2StreamSinkChannel) pending).generateSendFrameHeader(); + if (header.getByteBuffer() != null) { + pendingFrames.add(pending); + it.remove(); + } else { + //we clear the header, as we want to generate a new real header when the flow control window is updated + ((Http2StreamSinkChannel) pending).clearHeader(); + } + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +/** + * Parser for HTTP2 GO_AWAY frames + * + * @author Stuart Douglas + */ +public class Http2GoAwayParser extends Http2PushBackParser { + + private int statusCode; + private int lastGoodStreamId; + + public Http2GoAwayParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser headerParser) { + if (resource.remaining() < 8) { + return; + } + lastGoodStreamId = Http2ProtocolUtils.readInt(resource); + statusCode = Http2ProtocolUtils.readInt(resource); + } + + public int getStatusCode() { + return statusCode; + } + + public int getLastGoodStreamId() { + return lastGoodStreamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +/** + * The go away + *

+ * TODO: at the moment we don't allow the additional debug data + * + * @author Stuart Douglas + */ +class Http2GoAwayStreamSinkChannel extends Http2NoDataStreamSinkChannel { + + public static final int HEADER_FIRST_LINE = (8 << 8) | (Http2Channel.FRAME_TYPE_GOAWAY); + + private final int status; + private final int lastGoodStreamId; + + protected Http2GoAwayStreamSinkChannel(Http2Channel channel, int status, int lastGoodStreamId) { + super(channel); + this.status = status; + this.lastGoodStreamId = lastGoodStreamId; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(17); + + Http2ProtocolUtils.putInt(buf, HEADER_FIRST_LINE); + buf.put((byte)0); + Http2ProtocolUtils.putInt(buf, 0); //stream id + Http2ProtocolUtils.putInt(buf, lastGoodStreamId); + Http2ProtocolUtils.putInt(buf, status); + buf.flip(); + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + + @Override + protected boolean isLastFrame() { + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2GoAwayStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import org.xnio.Pooled; + +/** + * A HTTP2 go away frame + * + * @author Stuart Douglas + */ +public class Http2GoAwayStreamSourceChannel extends AbstractHttp2StreamSourceChannel { + + private final int status; + private final int lastGoodStreamId; + + Http2GoAwayStreamSourceChannel(Http2Channel framedChannel, Pooled data, long frameDataRemaining, int status, int lastGoodStreamId) { + super(framedChannel, data, frameDataRemaining); + this.status = status; + this.lastGoodStreamId = lastGoodStreamId; + lastFrame(); + } + + public int getStatus() { + return status; + } + + public int getLastGoodStreamId() { + return lastGoodStreamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeaderBlockParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeaderBlockParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeaderBlockParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,84 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.Bits; + +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; + +/** + * Parser for HTTP2 headers + * + * @author Stuart Douglas + */ +abstract class Http2HeaderBlockParser extends Http2PushBackParser implements HpackDecoder.HeaderEmitter { + + private final HeaderMap headerMap = new HeaderMap(); + private boolean beforeHeadersHandled = false; + + private final HpackDecoder decoder; + private int frameRemaining = -1; + + public Http2HeaderBlockParser(int frameLength, HpackDecoder decoder) { + super(frameLength); + this.decoder = decoder; + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser header) throws IOException { + boolean continuationFramesComing = Bits.anyAreClear(header.flags, Http2Channel.HEADERS_FLAG_END_HEADERS); + if (frameRemaining == -1) { + frameRemaining = header.length; + } + final boolean moreDataThisFrame = resource.remaining() < frameRemaining; + final int pos = resource.position(); + try { + if (!beforeHeadersHandled) { + if (!handleBeforeHeader(resource, header)) { + return; + } + } + beforeHeadersHandled = true; + decoder.setHeaderEmitter(this); + try { + decoder.decode(resource, moreDataThisFrame & continuationFramesComing); + } catch (HpackException e) { + throw new ConnectionErrorException(Http2Channel.ERROR_COMPRESSION_ERROR, e); + } + } finally { + int used = resource.position() - pos; + frameRemaining -= used; + } + } + + protected abstract boolean handleBeforeHeader(ByteBuffer resource, Http2FrameHeaderParser header); + + + HeaderMap getHeaderMap() { + return headerMap; + } + + @Override + public void emitHeader(HttpString name, String value, boolean neverIndex) { + headerMap.add(name, value); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeadersParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeadersParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeadersParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import org.xnio.Bits; + +/** + * Parser for HTTP2 Headers frames + * + * @author Stuart Douglas + */ +class Http2HeadersParser extends Http2HeaderBlockParser { + + private static final int DEPENDENCY_MASK = ~(1 << 7); + private int paddingLength = 0; + private int dependentStreamId = 0; + private int weight; + + public Http2HeadersParser(int frameLength, HpackDecoder hpackDecoder) { + super(frameLength, hpackDecoder); + } + + @Override + protected boolean handleBeforeHeader(ByteBuffer resource, Http2FrameHeaderParser headerParser) { + boolean hasPadding = Bits.anyAreSet(headerParser.flags, Http2Channel.HEADERS_FLAG_PADDED); + boolean hasPriority = Bits.anyAreSet(headerParser.flags, Http2Channel.HEADERS_FLAG_PRIORITY); + int reqLength = (hasPadding ? 1 : 0) + (hasPriority ? 5 : 0); + if (reqLength == 0) { + return true; + } + if (resource.remaining() < reqLength) { + return false; + } + if (hasPadding) { + paddingLength = (resource.get() & 0xFF); + } + if (hasPriority) { + if (resource.remaining() < 4) { + return false; + } + dependentStreamId = (resource.get() & DEPENDENCY_MASK & 0xFF) << 24; + dependentStreamId += (resource.get() & 0xFF) << 16; + dependentStreamId += (resource.get() & 0xFF) << 8; + dependentStreamId += (resource.get() & 0xFF); + weight = resource.get() & 0xFF; + } + return true; + } + + int getPaddingLength() { + return paddingLength; + } + + int getDependentStreamId() { + return dependentStreamId; + } + + int getWeight() { + return weight; + } +} \ No newline at end of file Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeadersStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeadersStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2HeadersStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import io.undertow.util.HeaderMap; + +/** + * Headers channel + * + * @author Stuart Douglas + */ +public class Http2HeadersStreamSinkChannel extends Http2DataStreamSinkChannel { + + + public Http2HeadersStreamSinkChannel(Http2Channel channel, int streamId) { + super(channel, streamId, Http2Channel.FRAME_TYPE_HEADERS); + } + + Http2HeadersStreamSinkChannel(Http2Channel channel, int streamId, HeaderMap headers) { + super(channel, streamId, headers, Http2Channel.FRAME_TYPE_HEADERS); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2NoDataStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2NoDataStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2NoDataStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,83 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import org.xnio.channels.StreamSourceChannel; + +import io.undertow.UndertowMessages; + +/** + * Stream sink channel that serves as the basis for channels that do not have the ability + * to write data. + *

+ * In particular these are: + * - PING + * - GO_AWAY + * + * @author Stuart Douglas + */ +abstract class Http2NoDataStreamSinkChannel extends AbstractHttp2StreamSinkChannel { + + protected Http2NoDataStreamSinkChannel(Http2Channel channel) { + super(channel); + } + + @Override + public long transferFrom(FileChannel src, long position, long count) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long transferFrom(StreamSourceChannel source, long count, ByteBuffer throughBuffer) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long write(ByteBuffer[] srcs) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import static io.undertow.protocols.http2.Http2Channel.PING_FRAME_LENGTH; + +import java.nio.ByteBuffer; + +/** + * Parser for HTTP2 ping frames. + * + * @author Stuart Douglas + */ +class Http2PingParser extends Http2PushBackParser { + + final byte[] data = new byte[PING_FRAME_LENGTH]; + + public Http2PingParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser parser) { + if (resource.remaining() < PING_FRAME_LENGTH) { + return; + } + resource.get(data); + } + + byte[] getData() { + return data; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import static io.undertow.protocols.http2.Http2Channel.PING_FRAME_LENGTH; + +import java.nio.ByteBuffer; + +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +/** + * @author Stuart Douglas + */ +class Http2PingStreamSinkChannel extends Http2NoDataStreamSinkChannel { + + public static final int HEADER_NO_ACK = (PING_FRAME_LENGTH << 8) | (Http2Channel.FRAME_TYPE_PING); + public static final int HEADER_ACK = (PING_FRAME_LENGTH << 16) | (Http2Channel.FRAME_TYPE_PING << 8) | Http2Channel.PING_FLAG_ACK; + private final byte[] data; + private final boolean ack; + + protected Http2PingStreamSinkChannel(Http2Channel channel, byte[] data, boolean ack) { + super(channel); + if (data.length != PING_FRAME_LENGTH) { + throw new IllegalArgumentException(UndertowMessages.MESSAGES.httpPingDataMustBeLength8()); + } + this.data = data; + this.ack = ack; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(16); + int firstInt = ack ? HEADER_ACK : HEADER_NO_ACK; + Http2ProtocolUtils.putInt(buf, firstInt); + buf.put((byte) 0); + Http2ProtocolUtils.putInt(buf, 0); //stream id, must be zero + for (int i = 0; i < PING_FRAME_LENGTH; ++i) { + buf.put(data[i]); + } + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PingStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +/** + * A HTTP2 Ping frame + * + * @author Stuart Douglas + */ +public class Http2PingStreamSourceChannel extends AbstractHttp2StreamSourceChannel { + + private final byte[] data; + private final boolean ack; + + Http2PingStreamSourceChannel(Http2Channel framedChannel, byte[] pingData, boolean ack) { + super(framedChannel); + this.data = pingData; + this.ack = ack; + lastFrame(); + } + + public byte[] getData() { + return data; + } + + public boolean isAck() { + return ack; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PrefaceStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PrefaceStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PrefaceStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +/** + * channel implementation that sends the initial HTTP2 preface + * + * @author Stuart Douglas + */ +class Http2PrefaceStreamSinkChannel extends Http2StreamSinkChannel { + Http2PrefaceStreamSinkChannel(Http2Channel channel) { + super(channel, 0); + } + + @Override + protected SendFrameHeader createFrameHeaderImpl() { + return new SendFrameHeader(new ImmediatePooled<>(ByteBuffer.wrap(Http2Channel.PREFACE_BYTES))); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2ProtocolUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2ProtocolUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2ProtocolUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +class Http2ProtocolUtils { + + public static void putInt(final ByteBuffer buffer, int value) { + buffer.put((byte) (value >> 24)); + buffer.put((byte) (value >> 16)); + buffer.put((byte) (value >> 8)); + buffer.put((byte) value); + } + + public static void putInt(final ByteBuffer buffer, int value, int position) { + buffer.put(position, (byte) (value >> 24)); + buffer.put(position + 1, (byte) (value >> 16)); + buffer.put(position + 2, (byte) (value >> 8)); + buffer.put(position + 3, (byte) value); + } + + public static int readInt(ByteBuffer buffer) { + int id = (buffer.get() & 0xFF) << 24; + id += (buffer.get() & 0xFF) << 16; + id += (buffer.get() & 0xFF) << 8; + id += (buffer.get() & 0xFF); + return id; + } + + private Http2ProtocolUtils() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PushBackParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PushBackParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2PushBackParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Parser that supports push back when not all data can be read. + * + * @author Stuart Douglas + */ +public abstract class Http2PushBackParser { + + private byte[] pushedBackData; + private boolean finished; + private int remainingData; + + public Http2PushBackParser(int frameLength) { + this.remainingData = frameLength; + } + + public void parse(ByteBuffer data, Http2FrameHeaderParser headerParser) throws IOException { + int used = 0; + ByteBuffer dataToParse = data; + int oldLimit = dataToParse.limit(); + try { + if (pushedBackData != null) { + dataToParse = ByteBuffer.wrap(new byte[pushedBackData.length + data.remaining()]); + dataToParse.put(pushedBackData); + dataToParse.put(data); + dataToParse.flip(); + oldLimit = dataToParse.limit(); + } + if (dataToParse.remaining() > remainingData) { + dataToParse.limit(dataToParse.position() + remainingData); + } + int rem = dataToParse.remaining(); + handleData(dataToParse, headerParser); + used = rem - dataToParse.remaining(); + + } finally { + //it is possible that we finished the parsing without using up all the data + //and the rest is to be consumed by the stream itself + if (finished) { + dataToParse.limit(oldLimit); + return; + } + int leftOver = dataToParse.remaining(); + if (leftOver > 0) { + pushedBackData = new byte[leftOver]; + dataToParse.get(pushedBackData); + } else { + pushedBackData = null; + } + dataToParse.limit(oldLimit); + remainingData -= used; + if (remainingData == 0) { + finished = true; + } + } + } + + protected abstract void handleData(ByteBuffer resource, Http2FrameHeaderParser headerParser) throws IOException; + + public boolean isFinished() { + return finished; + } + + protected void finish() { + finished = true; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +/** + * Parser for SPDY ping frames. + * + * @author Stuart Douglas + */ +class Http2RstStreamParser extends Http2PushBackParser { + + private int errorCode; + + public Http2RstStreamParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser headerParser) { + if (resource.remaining() < 4) { + return; + } + errorCode = Http2ProtocolUtils.readInt(resource); + + } + + public int getErrorCode() { + return errorCode; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,52 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +/** + * @author Stuart Douglas + */ +class Http2RstStreamSinkChannel extends Http2NoDataStreamSinkChannel { + + public static final int HEADER_FIRST_LINE = (4 << 8) | (Http2Channel.FRAME_TYPE_RST_STREAM); + private final int streamId; + private final int errorCode; + + protected Http2RstStreamSinkChannel(Http2Channel channel, int streamId, int errorCode) { + super(channel); + this.errorCode = errorCode; + this.streamId = streamId; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(13); + Http2ProtocolUtils.putInt(buf, HEADER_FIRST_LINE); + buf.put((byte)0); + Http2ProtocolUtils.putInt(buf, streamId); + Http2ProtocolUtils.putInt(buf, errorCode); + buf.flip(); + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2RstStreamStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import org.xnio.Pooled; + +/** + * A HTTP2 RST Stream channel + * + * @author Stuart Douglas + */ +public class Http2RstStreamStreamSourceChannel extends AbstractHttp2StreamSourceChannel { + + private final int errorCode; + private final int streamId; + + Http2RstStreamStreamSourceChannel(Http2Channel framedChannel, Pooled data, int errorCode, int streamId) { + super(framedChannel, data, 0); + this.errorCode = errorCode; + this.streamId = streamId; + lastFrame(); + } + + public int getErrorCode() { + return errorCode; + } + + public int getStreamId() { + return streamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2Setting.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2Setting.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2Setting.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +/** + * A Http2 Setting + * + * @author Stuart Douglas + */ +public class Http2Setting { + + public static final int SETTINGS_HEADER_TABLE_SIZE = 0x1; + public static final int SETTINGS_ENABLE_PUSH = 0x2; + public static final int SETTINGS_MAX_CONCURRENT_STREAMS = 0x3; + public static final int SETTINGS_INITIAL_WINDOW_SIZE = 0x4; + public static final int SETTINGS_MAX_FRAME_SIZE = 0x5; + public static final int SETTINGS_MAX_HEADER_LIST_SIZE = 0x6; + + private final int id; + private final int value; + + Http2Setting(int id, int value) { + this.id = id; + this.value = value; + } + + public int getId() { + return id; + } + + public int getValue() { + return value; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,58 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * @author Stuart Douglas + */ +class Http2SettingsParser extends Http2PushBackParser { + + private int count = 0; + + private final List settings = new ArrayList<>(); + + public Http2SettingsParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser parser) { + while (count < parser.length) { + if (resource.remaining() < 6) { + return; + } + int id = (resource.get() & 0xFF) << 8; + id += (resource.get() & 0xFF); + int value = (resource.get() & 0xFF) << 24; + value += (resource.get() & 0xFF) << 16; + value += (resource.get() & 0xFF) << 8; + value += (resource.get() & 0xFF); + settings.add(new Http2Setting(id, value)); + count += 6; + } + } + + public List getSettings() { + return settings; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,84 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import java.util.List; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.SendFrameHeader; + +/** + * //TODO: ack + * + * @author Stuart Douglas + */ +public class Http2SettingsStreamSinkChannel extends Http2StreamSinkChannel { + + private final List settings; + + Http2SettingsStreamSinkChannel(Http2Channel channel, List settings) { + super(channel, 0); + this.settings = settings; + } + + /** + * //an ack frame + * + * @param channel + */ + Http2SettingsStreamSinkChannel(Http2Channel channel) { + super(channel, 0); + this.settings = null; + } + + @Override + protected SendFrameHeader createFrameHeaderImpl() { + Pooled pooled = getChannel().getBufferPool().allocate(); + ByteBuffer currentBuffer = pooled.getResource(); + if (settings != null) { + int size = settings.size() * 6; + currentBuffer.put((byte) ((size >> 16) & 0xFF)); + currentBuffer.put((byte) ((size >> 8) & 0xFF)); + currentBuffer.put((byte) (size & 0xFF)); + currentBuffer.put((byte) Http2Channel.FRAME_TYPE_SETTINGS); //type + currentBuffer.put((byte) 0); //flags + Http2ProtocolUtils.putInt(currentBuffer, getStreamId()); + for (Http2Setting setting : settings) { + currentBuffer.put((byte) ((setting.getId() >> 8) & 0xFF)); + currentBuffer.put((byte) (setting.getId() & 0xFF)); + + currentBuffer.put((byte) ((setting.getValue() >> 24) & 0xFF)); + currentBuffer.put((byte) ((setting.getValue() >> 16) & 0xFF)); + currentBuffer.put((byte) ((setting.getValue() >> 8) & 0xFF)); + currentBuffer.put((byte) (setting.getValue() & 0xFF)); + } + } else { + + currentBuffer.put((byte) 0); + currentBuffer.put((byte) 0); + currentBuffer.put((byte) 0); + currentBuffer.put((byte) Http2Channel.FRAME_TYPE_SETTINGS); //type + currentBuffer.put((byte) Http2Channel.SETTINGS_FLAG_ACK); //flags + Http2ProtocolUtils.putInt(currentBuffer, getStreamId()); + } + currentBuffer.flip(); + return new SendFrameHeader(pooled); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2SettingsStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import org.xnio.Pooled; + +/** + * A HTTP2 Settings frame + * + * @author Stuart Douglas + */ +public class Http2SettingsStreamSourceChannel extends AbstractHttp2StreamSourceChannel { + + private final List settings; + + + Http2SettingsStreamSourceChannel(Http2Channel framedChannel, Pooled data, long frameDataRemaining, List settings) { + super(framedChannel, data, frameDataRemaining); + this.settings = settings; + lastFrame(); + } + + public List getSettings() { + return Collections.unmodifiableList(settings); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2StreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2StreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2StreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,159 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.IoUtils; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.SendFrameHeader; + +/** + * @author Stuart Douglas + */ +public abstract class Http2StreamSinkChannel extends AbstractHttp2StreamSinkChannel { + + private final int streamId; + private volatile boolean reset = false; + + //flow control related items. Accessed under lock + private int flowControlWindow; + private int initialWindowSize; //we track the initial window size, and then re-query it to get any delta + + private SendFrameHeader header; + + Http2StreamSinkChannel(Http2Channel channel, int streamId) { + super(channel); + this.streamId = streamId; + this.flowControlWindow = channel.getInitialSendWindowSize(); + this.initialWindowSize = this.flowControlWindow; + } + + public int getStreamId() { + return streamId; + } + + SendFrameHeader generateSendFrameHeader() { + header = createFrameHeaderImpl(); + return header; + } + + protected abstract SendFrameHeader createFrameHeaderImpl(); + + void clearHeader() { + this.header = null; + } + + @Override + protected void channelForciblyClosed() throws IOException { + getChannel().removeStreamSink(getStreamId()); + if (reset) { + return; + } + reset = true; + if (streamId % 2 == (getChannel().isClient() ? 1 : 0)) { + //we initiated the stream + //we only actually reset if we have sent something to the other endpoint + if (isFirstDataWritten()) { + getChannel().sendRstStream(streamId, Http2Channel.ERROR_CANCEL); + } + } else { + getChannel().sendRstStream(streamId, Http2Channel.ERROR_STREAM_CLOSED); + } + } + + @Override + protected final SendFrameHeader createFrameHeader() { + SendFrameHeader header = this.header; + this.header = null; + return header; + } + + @Override + protected void handleFlushComplete(boolean channelClosed) { + if (channelClosed) { + getChannel().removeStreamSink(getStreamId()); + } + } + + /** + * This method should be called before sending. It will return the amount of + * data that can be sent, taking into account the stream and connection flow + * control windows, and the toSend parameter. + *

+ * It will decrement the flow control windows by the amount that can be sent, + * so this method should only be called as a frame is being queued. + * + * @return The number of bytes that can be sent + */ + protected synchronized int grabFlowControlBytes(int toSend) { + if (toSend == 0) { + return 0; + } + int newWindowSize = this.getChannel().getInitialSendWindowSize(); + int settingsDelta = newWindowSize - this.initialWindowSize; + //first adjust for any settings frame updates + this.initialWindowSize = newWindowSize; + this.flowControlWindow += settingsDelta; + + int min = Math.min(toSend, this.flowControlWindow); + int actualBytes = this.getChannel().grabFlowControlBytes(min); + this.flowControlWindow -= actualBytes; + return actualBytes; + } + + synchronized void updateFlowControlWindow(final int delta) throws IOException { + boolean exhausted = flowControlWindow == 0; + flowControlWindow += delta; + if (exhausted) { + getChannel().notifyFlowControlAllowed(); + if (isWriteResumed()) { + resumeWritesInternal(true); + } + } + } + + + protected Pooled[] allocateAll(Pooled[] allHeaderBuffers, Pooled currentBuffer) { + Pooled[] ret; + if (allHeaderBuffers == null) { + ret = new Pooled[2]; + ret[0] = currentBuffer; + ret[1] = getChannel().getBufferPool().allocate(); + } else { + ret = new Pooled[allHeaderBuffers.length + 1]; + System.arraycopy(allHeaderBuffers, 0, ret, 0, allHeaderBuffers.length); + ret[ret.length - 1] = getChannel().getBufferPool().allocate(); + } + return ret; + } + + /** + * Method that is invoked when the stream is reset. + */ + void rstStream() { + if (reset) { + return; + } + reset = true; + IoUtils.safeClose(this); + getChannel().removeStreamSink(getStreamId()); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2StreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2StreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2StreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,215 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import org.xnio.Bits; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; + +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; + +/** + * @author Stuart Douglas + */ +public class Http2StreamSourceChannel extends AbstractHttp2StreamSourceChannel { + + /** + * Flag that is set if the headers frame has the end stream flag set, but not end headers + * which means the last continuation frame is the end of the stream. + */ + private boolean headersEndStream = false; + private boolean rst = false; + private final HeaderMap headers; + private final int streamId; + private HeaderMap newHeaders = null; + private Http2HeadersStreamSinkChannel response; + private int flowControlWindow; + private ChannelListener completionListener; + + Http2StreamSourceChannel(Http2Channel framedChannel, Pooled data, long frameDataRemaining, HeaderMap headers, int streamId) { + super(framedChannel, data, frameDataRemaining); + this.headers = headers; + this.streamId = streamId; + this.flowControlWindow = framedChannel.getInitialReceiveWindowSize(); + } + + @Override + protected void handleHeaderData(FrameHeaderData headerData) { + handleFinalFrame((Http2FrameHeaderParser) headerData); + } + + void handleFinalFrame(Http2FrameHeaderParser headerData) { + Http2FrameHeaderParser data = headerData; + if (data.type == Http2Channel.FRAME_TYPE_DATA) { + if (Bits.anyAreSet(data.flags, Http2Channel.DATA_FLAG_END_STREAM)) { + this.lastFrame(); + } + } else if (data.type == Http2Channel.FRAME_TYPE_HEADERS) { + if (Bits.allAreSet(data.flags, Http2Channel.HEADERS_FLAG_END_STREAM)) { + if (Bits.allAreSet(data.flags, Http2Channel.HEADERS_FLAG_END_HEADERS)) { + this.lastFrame(); + } else { + //continuation frames are coming, then we end the stream + headersEndStream = true; + } + } + } else if (headersEndStream && data.type == Http2Channel.FRAME_TYPE_CONTINUATION) { + if (Bits.anyAreSet(data.flags, Http2Channel.CONTINUATION_FLAG_END_HEADERS)) { + this.lastFrame(); + } + } + } + + public Http2HeadersStreamSinkChannel getResponseChannel() { + if (response != null) { + return response; + } + response = new Http2HeadersStreamSinkChannel(getHttp2Channel(), streamId); + getHttp2Channel().registerStreamSink(response); + return response; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + handleNewHeaders(); + int read = super.read(dst); + updateFlowControlWindow(read); + return read; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + handleNewHeaders(); + long read = super.read(dsts, offset, length); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + handleNewHeaders(); + long read = super.read(dsts); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel streamSinkChannel) throws IOException { + handleNewHeaders(); + long read = super.transferTo(count, throughBuffer, streamSinkChannel); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + handleNewHeaders(); + long read = super.transferTo(position, count, target); + updateFlowControlWindow((int) read); + return read; + } + + /** + * Merge any new headers from HEADERS blocks into the exchange. + */ + private synchronized void handleNewHeaders() { + if (newHeaders != null) { + for (HeaderValues header : newHeaders) { + headers.addAll(header.getHeaderName(), header); + } + newHeaders = null; + } + } + + synchronized void addNewHeaders(HeaderMap headers) { + if (newHeaders != null) { + newHeaders = headers; + } else { + for (HeaderValues header : headers) { + newHeaders.addAll(header.getHeaderName(), header); + } + } + } + + private void updateFlowControlWindow(final int read) { + if (read <= 0) { + return; + } + flowControlWindow -= read; + //TODO: RST stream if flow control limits are exceeded? + //TODO: make this configurable, we should be able to set the policy that is used to determine when to update the window size + Http2Channel spdyChannel = getHttp2Channel(); + spdyChannel.updateReceiveFlowControlWindow(read); + int initialWindowSize = spdyChannel.getInitialReceiveWindowSize(); + if (flowControlWindow < (initialWindowSize / 2)) { + int delta = initialWindowSize - flowControlWindow; + flowControlWindow += delta; + spdyChannel.sendUpdateWindowSize(streamId, delta); + } + } + + @Override + protected void complete() throws IOException { + super.complete(); + if (completionListener != null) { + ChannelListeners.invokeChannelListener(this, completionListener); + } + } + + public HeaderMap getHeaders() { + return headers; + } + + public ChannelListener getCompletionListener() { + return completionListener; + } + + public void setCompletionListener(ChannelListener completionListener) { + this.completionListener = completionListener; + } + + @Override + void rstStream() { + if (rst) { + return; + } + rst = true; + markStreamBroken(); + getHttp2Channel().sendRstStream(streamId, Http2Channel.ERROR_CANCEL); + } + + @Override + protected void channelForciblyClosed() { + if (completionListener != null) { + completionListener.handleEvent(this); + } + rstStream(); + } + + public int getStreamId() { + return streamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2WindowUpdateParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2WindowUpdateParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2WindowUpdateParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +/** + * Parser for HTTP2 window update frames + * + * @author Stuart Douglas + */ +class Http2WindowUpdateParser extends Http2PushBackParser { + + private int deltaWindowSize; + + public Http2WindowUpdateParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource, Http2FrameHeaderParser frameHeaderParser) { + if (resource.remaining() < 4) { + return; + } + deltaWindowSize = Http2ProtocolUtils.readInt(resource); + + } + + public int getDeltaWindowSize() { + return deltaWindowSize; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2WindowUpdateStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2WindowUpdateStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/Http2WindowUpdateStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,55 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.nio.ByteBuffer; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +/** + * A window update frame. + * + * @author Stuart Douglas + */ +class Http2WindowUpdateStreamSinkChannel extends Http2NoDataStreamSinkChannel { + + //length (4) and frame type. There are never any flags + public static final int HEADER_FIRST_LINE = (4 << 8) | (Http2Channel.FRAME_TYPE_WINDOW_UPDATE); + private final int streamId; + private final int deltaWindowSize; + + protected Http2WindowUpdateStreamSinkChannel(Http2Channel channel, int streamId, int deltaWindowSize) { + super(channel); + this.streamId = streamId; + this.deltaWindowSize = deltaWindowSize; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(13); + Http2ProtocolUtils.putInt(buf, HEADER_FIRST_LINE); + buf.put((byte)0); + Http2ProtocolUtils.putInt(buf, streamId); + Http2ProtocolUtils.putInt(buf, deltaWindowSize); + buf.flip(); + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/http2/StreamErrorException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/http2/StreamErrorException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/http2/StreamErrorException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,37 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.http2; + +import java.io.IOException; + +/** + * @author Stuart Douglas + */ +public class StreamErrorException extends IOException { + + private final int errorId; + + public StreamErrorException(int errorId) { + this.errorId = errorId; + } + + public int getErrorId() { + return errorId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,650 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.framed.AbstractFramedChannel; +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.util.Attachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.AttachmentList; +import io.undertow.util.HeaderMap; + +import org.xnio.Bits; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.ssl.SslConnection; + +import javax.net.ssl.SSLSession; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +/** + * SPDY channel. + * + * @author Stuart Douglas + */ +public class SpdyChannel extends AbstractFramedChannel implements Attachable { + + static final int DEFAULT_INITIAL_WINDOW_SIZE = 64 * 1024 * 0124; + + static final int SYN_STREAM = 1; + static final int SYN_REPLY = 2; + static final int RST_STREAM = 3; + static final int SETTINGS = 4; + static final int PING = 6; + static final int GOAWAY = 7; + static final int HEADERS = 8; + static final int WINDOW_UPDATE = 9; + + public static final int CLOSE_OK = 0; + public static final int CLOSE_PROTOCOL_ERROR = 1; + public static final int CLOSE_INTERNAL_ERROR = 2; + + static final int FLAG_FIN = 1; + static final int FLAG_UNIDIRECTIONAL = 2; + static final int CONTROL_FRAME = 1 << 31; + + public static final int RST_STATUS_PROTOCOL_ERROR = 1; + public static final int RST_STATUS_INVALID_STREAM = 2; + public static final int RST_STATUS_REFUSED_STREAM = 3; + public static final int RST_STATUS_UNSUPPORTED_VERSION = 4; + public static final int RST_STATUS_CANCEL = 5; + public static final int RST_STATUS_INTERNAL_ERROR = 6; + public static final int RST_STATUS_FLOW_CONTROL_ERROR = 7; + public static final int RST_STATUS_STREAM_IN_USE = 8; + public static final int RST_STATUS_STREAM_ALREADY_CLOSED = 9; + public static final int RST_STATUS_FRAME_TOO_LARGE = 11; + + private final Inflater inflater = new Inflater(false); + private final Deflater deflater = new Deflater(6); + + private SpdyFrameParser frameParser; + private final Map incomingStreams = new ConcurrentHashMap<>(); + private final Map outgoingStreams = new ConcurrentHashMap<>(); + + private volatile int initialWindowSize = DEFAULT_INITIAL_WINDOW_SIZE; + + + /** + * How much data we have told the remote endpoint we are prepared to accept. + */ + private volatile int receiveWindowSize = initialWindowSize; + + /** + * How much data we can send to the remote endpoint, at the connection level. + */ + private volatile int sendWindowSize = initialWindowSize; + + private final Pool heapBufferPool; + + private boolean thisGoneAway = false; + private boolean peerGoneAway = false; + + private int streamIdCounter; + private int lastGoodStreamId; + + private final Map, Object> attachments = Collections.synchronizedMap(new HashMap, Object>()); + + public SpdyChannel(StreamConnection connectedStreamChannel, Pool bufferPool, Pooled data, Pool heapBufferPool, boolean clientSide) { + super(connectedStreamChannel, bufferPool, SpdyFramePriority.INSTANCE, data); + this.heapBufferPool = heapBufferPool; + this.deflater.setDictionary(SpdyProtocolUtils.SPDY_DICT); + streamIdCounter = clientSide ? 1 : 2; + } + + @Override + protected SpdyStreamSourceChannel createChannel(FrameHeaderData frameHeaderData, Pooled frameData) throws IOException { + SpdyFrameParser frameParser = (SpdyFrameParser) frameHeaderData; + SpdyStreamSourceChannel channel; + if(!frameParser.control) { + //we only handle control frames here. If a data frame falls through it means that it has been cancelled + //we just free the data and return null, effectivly dropping the frame + UndertowLogger.REQUEST_LOGGER.tracef("Dropping Frame of length %s for stream %s", frameParser.getFrameLength(), frameParser.dataFrameStreamId); + return null; + } + //note that not all frame types are covered here, as some are only relevant to already active streams + //if which case they are handled by the existing channel support + switch (frameParser.type) { + case SYN_STREAM: { + SpdySynStreamParser parser = (SpdySynStreamParser) frameParser.parser; + channel = new SpdySynStreamStreamSourceChannel(this, frameData, frameHeaderData.getFrameLength(), deflater, parser.getHeaderMap(), parser.streamId); + lastGoodStreamId = parser.streamId; + if (!Bits.anyAreSet(frameParser.flags, FLAG_FIN)) { + incomingStreams.put(parser.streamId, channel); + } + break; + } + case SYN_REPLY: { + SpdySynReplyParser parser = (SpdySynReplyParser) frameParser.parser; + channel = new SpdySynReplyStreamSourceChannel(this, frameData, frameHeaderData.getFrameLength(), parser.getHeaderMap(), parser.streamId); + lastGoodStreamId = parser.streamId; + if (!Bits.anyAreSet(frameParser.flags, FLAG_FIN)) { + incomingStreams.put(parser.streamId, channel); + } + break; + } + case RST_STREAM: { + SpdyRstStreamParser parser = (SpdyRstStreamParser) frameParser.parser; + channel = new SpdyRstStreamStreamSourceChannel(this, frameData, frameParser.getFrameLength(), parser.getStreamId()); + handleRstStream(parser.getStreamId()); + break; + } + case SETTINGS: { + updateSettings(((SpdySettingsParser) frameParser.parser).getSettings()); + channel = new SpdySettingsStreamSourceChannel(this, frameData, frameParser.getFrameLength(), ((SpdySettingsParser) frameParser.parser).getSettings()); + break; + } + case PING: { + channel = new SpdyPingStreamSourceChannel(this, frameData, frameParser.getFrameLength(), ((SpdyPingParser) frameParser.parser).getId()); + break; + } + case GOAWAY: { + SpdyGoAwayParser spdyGoAwayParser = (SpdyGoAwayParser) frameParser.parser; + channel = new SpdyGoAwayStreamSourceChannel(this, frameData, frameParser.getFrameLength(), spdyGoAwayParser.getStatusCode(), spdyGoAwayParser.getLastGoodStreamId()); + peerGoneAway = true; + break; + } + case WINDOW_UPDATE: { + SpdyWindowUpdateParser parser = (SpdyWindowUpdateParser) frameParser.parser; + handleWindowUpdate(parser.getStreamId(), parser.getDeltaWindowSize()); + frameData.free(); + //we don't return window update notifications, they are handled internally + return null; + } + default: { + throw UndertowMessages.MESSAGES.unexpectedFrameType(frameParser.type); + } + } + if (Bits.anyAreSet(frameParser.flags, FLAG_FIN)) { + channel.lastFrame(); + } + return channel; + } + + @Override + protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { + SpdyFrameParser frameParser = this.frameParser; + if (frameParser == null) { + this.frameParser = frameParser = new SpdyFrameParser(); + } + if (!frameParser.handle(data)) { + return null; + } + this.frameParser = null; + return frameParser; + + } + + protected void lastDataRead() { + if(!peerGoneAway && !thisGoneAway) { + //the peer has performed an unclean close + //we assume something happened to the underlying connection + //we attempt to send our own GOAWAY, however it will probably fail, + //which will trigger a forces close of our write side + sendGoAway(CLOSE_PROTOCOL_ERROR); + peerGoneAway = true; + } + } + + @Override + public boolean isOpen() { + return super.isOpen() && !peerGoneAway && !thisGoneAway; + } + + @Override + protected boolean isLastFrameReceived() { + return peerGoneAway; + } + + @Override + protected boolean isLastFrameSent() { + return peerGoneAway || thisGoneAway; + } + + @Override + protected void handleBrokenSourceChannel(Throwable e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Closing SPDY channel to %s due to broken read side", getPeerAddress()); + sendGoAway(CLOSE_PROTOCOL_ERROR, new SpdyControlMessageExceptionHandler()); + } + + @Override + protected void handleBrokenSinkChannel(Throwable e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Closing SPDY channel to %s due to broken write side", getPeerAddress()); + IoUtils.safeClose(this); + } + + @Override + protected void closeSubChannels() { + for (Map.Entry e : incomingStreams.entrySet()) { + SpdyStreamSourceChannel receiver = e.getValue(); + if (receiver.isReadResumed()) { + ChannelListeners.invokeChannelListener(receiver.getIoThread(), receiver, ((ChannelListener.SimpleSetter) receiver.getReadSetter()).get()); + } + IoUtils.safeClose(receiver); + } + incomingStreams.clear(); + + for (Map.Entry e : outgoingStreams.entrySet()) { + SpdyStreamStreamSinkChannel receiver = e.getValue(); + if (receiver.isWritesShutdown()) { + ChannelListeners.invokeChannelListener(receiver.getIoThread(), receiver, ((ChannelListener.SimpleSetter) receiver.getWriteSetter()).get()); + } + IoUtils.safeClose(receiver); + } + outgoingStreams.clear(); + } + + /** + * Setting have been received from the client + * + * @param settings + */ + synchronized void updateSettings(List settings) { + for (SpdySetting setting : settings) { + if (setting.getId() == SpdySetting.SETTINGS_INITIAL_WINDOW_SIZE) { + int old = initialWindowSize; + initialWindowSize = setting.getValue(); + int difference = old - initialWindowSize; + receiveWindowSize += difference; + sendWindowSize += difference; + } + //ignore the rest for now + } + } + + public int getSpdyVersion() { + return 3; + } + + Pool getHeapBufferPool() { + return heapBufferPool; + } + + int getInitialWindowSize() { + return initialWindowSize; + } + + public synchronized void handleWindowUpdate(int streamId, int deltaWindowSize) throws IOException { + if (streamId == 0) { + boolean exhausted = sendWindowSize == 0; + sendWindowSize += deltaWindowSize; + if(exhausted) { + notifyFlowControlAllowed(); + } + } else { + SpdyStreamStreamSinkChannel stream = outgoingStreams.get(streamId); + if (stream == null) { + //TODO: error handling + } else { + stream.updateFlowControlWindow(deltaWindowSize); + } + } + } + + synchronized void notifyFlowControlAllowed() throws IOException { + super.recalculateHeldFrames(); + } + + public void sendPing(int id) { + sendPing(id, new SpdyControlMessageExceptionHandler()); + } + + public void sendPing(int id, final ChannelExceptionHandler exceptionHandler) { + SpdyPingStreamSinkChannel ping = new SpdyPingStreamSinkChannel(this, id); + try { + ping.shutdownWrites(); + if (!ping.flush()) { + ping.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, exceptionHandler)); + ping.resumeWrites(); + } + } catch (IOException e) { + exceptionHandler.handleException(ping, e); + } + } + + + public void sendGoAway(int status) { + sendGoAway(status, new SpdyControlMessageExceptionHandler()); + } + + public void sendGoAway(int status, final ChannelExceptionHandler exceptionHandler) { + if(thisGoneAway) { + return; + } + thisGoneAway = true; + SpdyGoAwayStreamSinkChannel goAway = new SpdyGoAwayStreamSinkChannel(this, status, lastGoodStreamId); + try { + goAway.shutdownWrites(); + if (!goAway.flush()) { + goAway.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, exceptionHandler)); + goAway.resumeWrites(); + } + } catch (IOException e) { + exceptionHandler.handleException(goAway, e); + } + } + + public void sendUpdateWindowSize(int streamId, int delta) { + SpdyWindowUpdateStreamSinkChannel windowUpdateStreamSinkChannel = new SpdyWindowUpdateStreamSinkChannel(this, streamId, delta); + try { + windowUpdateStreamSinkChannel.shutdownWrites(); + if (!windowUpdateStreamSinkChannel.flush()) { + windowUpdateStreamSinkChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new SpdyControlMessageExceptionHandler())); + windowUpdateStreamSinkChannel.resumeWrites(); + } + } catch (IOException e) { + handleBrokenSinkChannel(e); + } + + } + + public SSLSession getSslSession() { + StreamConnection con = getUnderlyingConnection(); + if (con instanceof SslConnection) { + return ((SslConnection) con).getSslSession(); + } + return null; + } + + public synchronized void updateReceiveFlowControlWindow(int read) { + if(read <= 0) { + return; + } + receiveWindowSize -= read; + //TODO: make this configurable, we should be able to set the policy that is used to determine when to update the window size + int initialWindowSize = this.initialWindowSize; + if (receiveWindowSize < (initialWindowSize / 2)) { + int delta = initialWindowSize - receiveWindowSize; + receiveWindowSize += delta; + sendUpdateWindowSize(0, delta); + } + } + + public synchronized SpdySynStreamStreamSinkChannel createStream(HeaderMap requestHeaders) throws IOException { + if(!isOpen()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + int streamId = streamIdCounter; + streamIdCounter += 2; + SpdySynStreamStreamSinkChannel spdySynStreamStreamSinkChannel = new SpdySynStreamStreamSinkChannel(this, requestHeaders, streamId, deflater); + outgoingStreams.put(streamId, spdySynStreamStreamSinkChannel); + return spdySynStreamStreamSinkChannel; + + } + + /** + * Try and decrement the send window by the given amount of bytes. + * + * @param bytesToGrab The amount of bytes the sender is trying to send + * @return The actual amount of bytes the sender can send + */ + synchronized int grabFlowControlBytes(int bytesToGrab) { + int min = Math.min(bytesToGrab, sendWindowSize); + sendWindowSize -= min; + return min; + } + + void registerStreamSink(SpdySynReplyStreamSinkChannel synResponse) { + outgoingStreams.put(synResponse.getStreamId(), synResponse); + } + + void removeStreamSink(int streamId) { + outgoingStreams.remove(streamId); + } + + public boolean isClient() { + return streamIdCounter % 2 == 1; + } + + @Override + public T getAttachment(AttachmentKey key) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + return (T) attachments.get(key); + } + + @Override + public List getAttachmentList(AttachmentKey> key) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + Object o = attachments.get(key); + if(o == null) { + return Collections.emptyList(); + } + return (List)o; + } + + @Override + public T putAttachment(AttachmentKey key, T value) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + return key.cast(attachments.put(key, key.cast(value))); + } + + @Override + public T removeAttachment(AttachmentKey key) { + return key.cast(attachments.remove(key)); + } + + @Override + public void addToAttachmentList(AttachmentKey> key, T value) { + + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + final Map, Object> attachments = this.attachments; + synchronized (attachments) { + final List list = key.cast(attachments.get(key)); + if (list == null) { + final AttachmentList newList = new AttachmentList((Class) Object.class); + attachments.put(key, newList); + newList.add(value); + } else { + list.add(value); + } + } + } + + public void sendRstStream(int streamId, int statusCode) { + handleRstStream(streamId); + try { + SpdyRstStreamSinkChannel channel = new SpdyRstStreamSinkChannel(this, streamId, statusCode); + channel.shutdownWrites(); + if (!channel.flush()) { + channel.getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(SpdyStreamSinkChannel channel, IOException exception) { + markWritesBroken(exception); + } + })); + channel.resumeWrites(); + } + } catch (IOException e) { + markWritesBroken(e); + } + } + + private void handleRstStream(int streamId) { + SpdyStreamSourceChannel incoming = incomingStreams.remove(streamId); + if(incoming != null) { + incoming.rstStream(); + } + SpdyStreamStreamSinkChannel outgoing = outgoingStreams.remove(streamId); + if(outgoing != null) { + outgoing.rstStream(); + } + } + + + class SpdyFrameParser implements FrameHeaderData { + + final byte[] header = new byte[8]; + int read = 0; + boolean control; + + //control fields + int version; + int type; + + //data fields + int dataFrameStreamId; + + int flags; + int length; + + SpdyPushBackParser parser = null; + + private static final int CONTROL_MASK = 1 << 7; + + public boolean handle(final ByteBuffer byteBuffer) throws IOException { + if (parser == null) { + if (!parseFrameHeader(byteBuffer)) { + return false; + } + if (!control) { + return true; + } + switch (type) { + case SYN_STREAM: { + parser = new SpdySynStreamParser(getBufferPool(), SpdyChannel.this, length, inflater); + break; + } + case RST_STREAM: { + parser = new SpdyRstStreamParser(getBufferPool(), length); + break; + } + case HEADERS: { + parser = new SpdyHeadersParser(getBufferPool(), SpdyChannel.this, length, inflater); + break; + } + case SYN_REPLY: { + parser = new SpdySynReplyParser(getBufferPool(), SpdyChannel.this, length, inflater); + break; + } + case GOAWAY: { + parser = new SpdyGoAwayParser(getBufferPool(), length); + peerGoneAway = true; + break; + } + case PING: { + parser = new SpdyPingParser(getBufferPool(), length); + break; + } + case SETTINGS: { + parser = new SpdySettingsParser(length); + break; + } + case WINDOW_UPDATE: { + parser = new SpdyWindowUpdateParser(length); + break; + } + default: { + return true; + } + } + } + parser.parse(byteBuffer); + return parser.isFinished(); + } + + private boolean parseFrameHeader(ByteBuffer byteBuffer) { + while (read < 8 && byteBuffer.hasRemaining()) { + header[read++] = byteBuffer.get(); + } + if (read != 8) { + return false; + } + control = (header[0] & CONTROL_MASK) != 0; + if (control) { + version = (header[0] & ~CONTROL_MASK & 0xFF) << 8; + version += header[1] & 0xff; + type = (header[2] & 0xff) << 8; + type += header[3] & 0xff; + } else { + dataFrameStreamId = (header[0] & ~CONTROL_MASK & 0xFF) << 24; + dataFrameStreamId += (header[1] & 0xff) << 16; + dataFrameStreamId += (header[2] & 0xff) << 8; + dataFrameStreamId += header[3] & 0xff; + } + flags = header[4] & 0xff; + length = (header[5] & 0xff) << 16; + length = (header[6] & 0xff) << 8; + length += header[7] & 0xff; + return true; + } + + @Override + public long getFrameLength() { + //control frames have no data + //we fully parse them as part of the receive process so they are considered to have a length of zero + if (control) { + return 0; + } + return length; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + if (type == SYN_STREAM) { + return null; + } + int id; + if (control) { + id = parser.getStreamId(); + if (id == -1) { + return null; + } + } else { + id = dataFrameStreamId; + } + //TODO: error + if (Bits.anyAreSet(flags, FLAG_FIN)) { + return incomingStreams.remove(id); + } else { + return incomingStreams.get(id); + } + } + } + + private class SpdyControlMessageExceptionHandler implements ChannelExceptionHandler { + @Override + public void handleException(SpdyStreamSinkChannel channel, IOException exception) { + IoUtils.safeClose(channel); + handleBrokenSinkChannel(exception); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyControlFrameStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyControlFrameStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyControlFrameStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,76 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.UndertowMessages; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * @author Stuart Douglas + */ + abstract class SpdyControlFrameStreamSinkChannel extends SpdyStreamSinkChannel { + + protected SpdyControlFrameStreamSinkChannel(SpdyChannel channel) { + super(channel); + } + + @Override + public long transferFrom(FileChannel src, long position, long count) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long transferFrom(StreamSourceChannel source, long count, ByteBuffer throughBuffer) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long write(ByteBuffer[] srcs) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public int write(ByteBuffer src) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + throw UndertowMessages.MESSAGES.controlFrameCannotHaveBodyContent(); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyFramePriority.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyFramePriority.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyFramePriority.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,71 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.FramePriority; +import io.undertow.server.protocol.framed.SendFrameHeader; + +import java.util.Deque; +import java.util.Iterator; +import java.util.List; + +/** + * TODO: real priority + * + * @author Stuart Douglas + */ +class SpdyFramePriority implements FramePriority{ + + public static SpdyFramePriority INSTANCE = new SpdyFramePriority(); + + @Override + public boolean insertFrame(SpdyStreamSinkChannel newFrame, List pendingFrames) { + //first deal with flow control + if(newFrame instanceof SpdyStreamStreamSinkChannel) { + SendFrameHeader header = ((SpdyStreamStreamSinkChannel) newFrame).generateSendFrameHeader(); + //if no header is generated then flow control means we can't send anything + if(header.getByteBuffer() == null) { + //we clear the header, as we want to generate a new real header when the flow control window is updated + ((SpdyStreamStreamSinkChannel) newFrame).clearHeader(); + return false; + } + } + + pendingFrames.add(newFrame); + return true; + } + + @Override + public void frameAdded(SpdyStreamSinkChannel addedFrame, List pendingFrames, Deque holdFrames) { + Iterator it = holdFrames.iterator(); + while (it.hasNext()){ + SpdyStreamSinkChannel pending = it.next(); + if(pending instanceof SpdyStreamStreamSinkChannel) { + SendFrameHeader header = ((SpdyStreamStreamSinkChannel) pending).generateSendFrameHeader(); + if(header.getByteBuffer() != null) { + pendingFrames.add(pending); + it.remove(); + } else { + //we clear the header, as we want to generate a new real header when the flow control window is updated + ((SpdyStreamStreamSinkChannel) pending).clearHeader(); + } + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,56 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pool; + +import java.nio.ByteBuffer; + +/** + * Parser for SPDY ping frames. + * + * @author Stuart Douglas + */ +public class SpdyGoAwayParser extends SpdyPushBackParser { + + private int statusCode; + private int lastGoodStreamId; + + public SpdyGoAwayParser(Pool bufferPool, int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource) { + if (resource.remaining() < 8) { + return; + } + lastGoodStreamId = SpdyProtocolUtils.readInt(resource); + statusCode = SpdyProtocolUtils.readInt(resource); + + } + + public int getStatusCode() { + return statusCode; + } + + public int getLastGoodStreamId() { + return lastGoodStreamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,57 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +class SpdyGoAwayStreamSinkChannel extends SpdyControlFrameStreamSinkChannel { + + private final int status; + private final int lastGoodStreamId; + + protected SpdyGoAwayStreamSinkChannel(SpdyChannel channel, int status, int lastGoodStreamId) { + super(channel); + this.status = status; + this.lastGoodStreamId = lastGoodStreamId; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(16); + + int firstInt = SpdyChannel.CONTROL_FRAME | (getChannel().getSpdyVersion() << 16) | 7; + SpdyProtocolUtils.putInt(buf, firstInt); + SpdyProtocolUtils.putInt(buf, 8); + SpdyProtocolUtils.putInt(buf, lastGoodStreamId); + SpdyProtocolUtils.putInt(buf, status); + buf.flip(); + return new SendFrameHeader( new ImmediatePooled<>(buf)); + } + + @Override + protected boolean isLastFrame() { + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyGoAwayStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * A SPDY Ping frame + * + * @author Stuart Douglas + */ +public class SpdyGoAwayStreamSourceChannel extends SpdyStreamSourceChannel { + + private final int status; + private final int lastGoodStreamId; + + SpdyGoAwayStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining, int status, int lastGoodStreamId) { + super(framedChannel, data, frameDataRemaining); + this.status = status; + this.lastGoodStreamId = lastGoodStreamId; + lastFrame(); + } + + public int getStatus() { + return status; + } + + public int getLastGoodStreamId() { + return lastGoodStreamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyHeaderBlockParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyHeaderBlockParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyHeaderBlockParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,248 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import org.xnio.Pool; +import org.xnio.Pooled; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.zip.DataFormatException; +import java.util.zip.Inflater; + +/** + * Parser for SPDY compressed header blocks + * + * @author Stuart Douglas + */ +abstract class SpdyHeaderBlockParser extends SpdyPushBackParser { + + private final SpdyChannel channel; + + private int numHeaders = -1; + private int readHeaders = 0; + private final HeaderMap headerMap = new HeaderMap(); + + private final Inflater inflater; + + //state used for parsing headers + private HttpString currentHeader; + private ByteArrayOutputStream partialValue; + private int remainingData; + private boolean beforeHeadersHandled = false; + private byte[] dataOverflow; + + + public SpdyHeaderBlockParser(Pool bufferPool, SpdyChannel channel, int frameLength, Inflater inflater) { + super(frameLength); + this.channel = channel; + this.inflater = inflater; + } + + @Override + protected void handleData(ByteBuffer resource) throws IOException { + if(!beforeHeadersHandled) { + if (!handleBeforeHeader(resource)) { + return; + } + } + beforeHeadersHandled = true; + Pooled outPooled = channel.getHeapBufferPool().allocate(); + Pooled inPooled = channel.getHeapBufferPool().allocate(); + + boolean extraOutput = false; + try { + ByteBuffer outputBuffer = outPooled.getResource(); + ByteBuffer inPooledResource = inPooled.getResource(); + if(dataOverflow != null) { + outputBuffer.put(dataOverflow); + dataOverflow = null; + extraOutput = true; + } + byte[] inputBuffer = inPooledResource.array(); + while (resource.hasRemaining()) { + int rem = resource.remaining(); + if (rem > inputBuffer.length) { + resource.get(inputBuffer, inPooledResource.arrayOffset(), inPooledResource.limit()); + } else { + resource.get(inputBuffer, inPooledResource.arrayOffset(), resource.remaining()); + } + int inputLength = Math.min(rem, inPooledResource.limit()); + inflater.setInput(inputBuffer, inPooledResource.arrayOffset(), inputLength); + while (!inflater.needsInput()) { + int copied = 0; + try { + copied = inflater.inflate(outputBuffer.array(), outputBuffer.arrayOffset() + outputBuffer.position(), outputBuffer.remaining()); + } catch (DataFormatException e) { + throw new StreamErrorException(StreamErrorException.PROTOCOL_ERROR); + } + if (copied == 0 && inflater.needsDictionary()) { + inflater.setDictionary(SpdyProtocolUtils.SPDY_DICT); + } else if(copied > 0) { + outputBuffer.position(outputBuffer.position() + copied); + handleDecompressedData(outputBuffer); + if(outputBuffer.hasRemaining()) { + outputBuffer.compact(); + extraOutput = true; + } else { + extraOutput = false; + outputBuffer.clear(); + } + } + } + } + } finally { + if(extraOutput) { + outPooled.getResource().flip(); + dataOverflow = new byte[outPooled.getResource().remaining()]; + outPooled.getResource().get(dataOverflow); + } + inPooled.free(); + outPooled.free(); + } + } + + protected abstract boolean handleBeforeHeader(ByteBuffer resource); + + + private void handleDecompressedData(ByteBuffer data) throws IOException { + data.flip(); + + if (numHeaders == -1) { + + if(data.remaining() < 4) { + return; + } + numHeaders = (data.get() & 0xFF) << 24; + numHeaders += (data.get() & 0xFF) << 16; + numHeaders += (data.get() & 0xFF) << 8; + numHeaders += (data.get() & 0xFF); + } + while (readHeaders < numHeaders) { + if (currentHeader == null && partialValue == null) { + if (data.remaining() < 4) { + return; + } + int nameLength = (data.get() & 0xFF) << 24; + nameLength += (data.get() & 0xFF) << 16; + nameLength += (data.get() & 0xFF) << 8; + nameLength += (data.get() & 0xFF); + if (nameLength == 0) { + throw new StreamErrorException(StreamErrorException.PROTOCOL_ERROR); + } + + if (data.remaining() >= nameLength) { + currentHeader = new HttpString(data.array(), data.arrayOffset() + data.position(), nameLength); + data.position(data.position() + nameLength); + } else { + remainingData = nameLength - data.remaining(); + partialValue = new ByteArrayOutputStream(); + partialValue.write(data.array(), data.arrayOffset() + data.position(), data.remaining()); + data.position(data.limit()); + return; + } + } else if (currentHeader == null && partialValue != null) { + if (data.remaining() >= remainingData) { + partialValue.write(data.array(), data.arrayOffset() + data.position(), remainingData); + currentHeader = new HttpString(partialValue.toByteArray()); + data.position(data.position() + remainingData); + this.remainingData = -1; + this.partialValue = null; + } else { + remainingData = remainingData - data.remaining(); + partialValue.write(data.array(), data.arrayOffset() + data.position(), data.remaining()); + data.position(data.limit()); + return; + } + } + if (partialValue == null) { + if (data.remaining() < 4) { + return; + } + int valueLength = (data.get() & 0xFF) << 24; + valueLength += (data.get() & 0xFF) << 16; + valueLength += (data.get() & 0xFF) << 8; + valueLength += (data.get() & 0xFF); + //headers can have multiple values, separated by a single null character + + if (data.remaining() >= valueLength) { + int start = data.arrayOffset() + data.position(); + int end = start + valueLength; + byte[] array = data.array(); + for (int i = start; i < end; ++i) { + if (array[i] == 0) { + headerMap.add(currentHeader, new String(array, start, i - start, "UTF-8")); + start = i + 1; + } + } + headerMap.add(currentHeader, new String(array, start, end - start, "UTF-8")); + currentHeader = null; + data.position(data.position() + valueLength); + } else { + remainingData = valueLength - data.remaining(); + int start = data.arrayOffset() + data.position(); + int end = start + data.remaining(); + byte[] array = data.array(); + for (int i = start; i < end; ++i) { + if (array[i] == 0) { + String headerValue = new String(array, start, i - start - 1, "UTF-8"); + headerMap.add(currentHeader, headerValue); + start = i + 1; + } + } + partialValue = new ByteArrayOutputStream(); + partialValue.write(array, start, end - start); + data.position(data.limit()); + return; + } + } else { + if (data.remaining() >= remainingData) { + partialValue.write(data.array(), data.arrayOffset() + data.position(), remainingData); + byte[] completeData = partialValue.toByteArray(); + int start = 0; + int end = completeData.length; + for (int i = start; i < end; ++i) { + if (completeData[i] == 0) { + headerMap.add(currentHeader, new String(completeData, start, i - start - 1, "UTF-8")); + start = i + 1; + } + } + headerMap.add(currentHeader, new String(completeData, start, end - start, "UTF-8")); + data.position(data.position() + remainingData); + currentHeader = null; + this.remainingData = -1; + this.partialValue = null; + } else { + remainingData = remainingData - data.remaining(); + partialValue.write(data.array(), data.arrayOffset() + data.position(), data.remaining()); + data.position(data.limit()); + return; + } + } + this.readHeaders++; + } + } + + HeaderMap getHeaderMap() { + return headerMap; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyHeadersParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyHeadersParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyHeadersParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pool; + +import java.nio.ByteBuffer; +import java.util.zip.Inflater; + +/** + * Parser for SPDY headers frames. + * + * @author Stuart Douglas + */ +class SpdyHeadersParser extends SpdyHeaderBlockParser { + + public SpdyHeadersParser(Pool bufferPool, SpdyChannel channel, int frameLength, Inflater inflater) { + super(bufferPool, channel,frameLength, inflater); + } + + @Override + protected boolean handleBeforeHeader(ByteBuffer resource) { + if (resource.remaining() < 4) { + return false; + } + streamId = SpdyProtocolUtils.readInt(resource); + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pool; + +import java.nio.ByteBuffer; + +/** + * Parser for SPDY ping frames. + * + * @author Stuart Douglas + */ +class SpdyPingParser extends SpdyPushBackParser { + + private int id; + + public SpdyPingParser(Pool bufferPool, int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource) { + if (resource.remaining() < 4) { + return; + } + id = SpdyProtocolUtils.readInt(resource); + } + + public int getId() { + return id; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +class SpdyPingStreamSinkChannel extends SpdyControlFrameStreamSinkChannel { + + private final int id; + + protected SpdyPingStreamSinkChannel(SpdyChannel channel, int id) { + super(channel); + this.id = id; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(12); + + int firstInt = SpdyChannel.CONTROL_FRAME | (getChannel().getSpdyVersion() << 16) | SpdyChannel.PING; + SpdyProtocolUtils.putInt(buf, firstInt); + SpdyProtocolUtils.putInt(buf, 4); //we back fill the length + SpdyProtocolUtils.putInt(buf, id); + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPingStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * A SPDY Ping frame + * + * @author Stuart Douglas + */ +public class SpdyPingStreamSourceChannel extends SpdyStreamSourceChannel { + + private final int id; + + SpdyPingStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining, int id) { + super(framedChannel, data, frameDataRemaining); + this.id = id; + lastFrame(); + } + + public int getId() { + return id; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyProtocolUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyProtocolUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyProtocolUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,234 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +class SpdyProtocolUtils { + + static final byte[] SPDY_DICT = { + 0x00, 0x00, 0x00, 0x07, 0x6f, 0x70, 0x74, 0x69, // - - - - o p t i + 0x6f, 0x6e, 0x73, 0x00, 0x00, 0x00, 0x04, 0x68, // o n s - - - - h + 0x65, 0x61, 0x64, 0x00, 0x00, 0x00, 0x04, 0x70, // e a d - - - - p + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x03, 0x70, // o s t - - - - p + 0x75, 0x74, 0x00, 0x00, 0x00, 0x06, 0x64, 0x65, // u t - - - - d e + 0x6c, 0x65, 0x74, 0x65, 0x00, 0x00, 0x00, 0x05, // l e t e - - - - + 0x74, 0x72, 0x61, 0x63, 0x65, 0x00, 0x00, 0x00, // t r a c e - - - + 0x06, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x00, // - a c c e p t - + 0x00, 0x00, 0x0e, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // t - c h a r s e + 0x74, 0x00, 0x00, 0x00, 0x0f, 0x61, 0x63, 0x63, // t - - - - a c c + 0x65, 0x70, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e p t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x0f, // d i n g - - - - + 0x61, 0x63, 0x63, 0x65, 0x70, 0x74, 0x2d, 0x6c, // a c c e p t - l + 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, 0x00, // a n g u a g e - + 0x00, 0x00, 0x0d, 0x61, 0x63, 0x63, 0x65, 0x70, // - - - a c c e p + 0x74, 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, // t - r a n g e s + 0x00, 0x00, 0x00, 0x03, 0x61, 0x67, 0x65, 0x00, // - - - - a g e - + 0x00, 0x00, 0x05, 0x61, 0x6c, 0x6c, 0x6f, 0x77, // - - - a l l o w + 0x00, 0x00, 0x00, 0x0d, 0x61, 0x75, 0x74, 0x68, // - - - - a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, // o r i z a t i o + 0x6e, 0x00, 0x00, 0x00, 0x0d, 0x63, 0x61, 0x63, // n - - - - c a c + 0x68, 0x65, 0x2d, 0x63, 0x6f, 0x6e, 0x74, 0x72, // h e - c o n t r + 0x6f, 0x6c, 0x00, 0x00, 0x00, 0x0a, 0x63, 0x6f, // o l - - - - c o + 0x6e, 0x6e, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, // n n e c t i o n + 0x00, 0x00, 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x62, 0x61, 0x73, 0x65, // e n t - b a s e + 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x65, 0x6e, 0x63, 0x6f, // e n t - e n c o + 0x64, 0x69, 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, // d i n g - - - - + 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, // c o n t e n t - + 0x6c, 0x61, 0x6e, 0x67, 0x75, 0x61, 0x67, 0x65, // l a n g u a g e + 0x00, 0x00, 0x00, 0x0e, 0x63, 0x6f, 0x6e, 0x74, // - - - - c o n t + 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x65, 0x6e, 0x67, // e n t - l e n g + 0x74, 0x68, 0x00, 0x00, 0x00, 0x10, 0x63, 0x6f, // t h - - - - c o + 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x2d, 0x6c, 0x6f, // n t e n t - l o + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, // c a t i o n - - + 0x00, 0x0b, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x6d, 0x64, 0x35, 0x00, 0x00, 0x00, // t - m d 5 - - - + 0x0d, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, // - c o n t e n t + 0x2d, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, // - r a n g e - - + 0x00, 0x0c, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, // - - c o n t e n + 0x74, 0x2d, 0x74, 0x79, 0x70, 0x65, 0x00, 0x00, // t - t y p e - - + 0x00, 0x04, 0x64, 0x61, 0x74, 0x65, 0x00, 0x00, // - - d a t e - - + 0x00, 0x04, 0x65, 0x74, 0x61, 0x67, 0x00, 0x00, // - - e t a g - - + 0x00, 0x06, 0x65, 0x78, 0x70, 0x65, 0x63, 0x74, // - - e x p e c t + 0x00, 0x00, 0x00, 0x07, 0x65, 0x78, 0x70, 0x69, // - - - - e x p i + 0x72, 0x65, 0x73, 0x00, 0x00, 0x00, 0x04, 0x66, // r e s - - - - f + 0x72, 0x6f, 0x6d, 0x00, 0x00, 0x00, 0x04, 0x68, // r o m - - - - h + 0x6f, 0x73, 0x74, 0x00, 0x00, 0x00, 0x08, 0x69, // o s t - - - - i + 0x66, 0x2d, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, // f - m a t c h - + 0x00, 0x00, 0x11, 0x69, 0x66, 0x2d, 0x6d, 0x6f, // - - - i f - m o + 0x64, 0x69, 0x66, 0x69, 0x65, 0x64, 0x2d, 0x73, // d i f i e d - s + 0x69, 0x6e, 0x63, 0x65, 0x00, 0x00, 0x00, 0x0d, // i n c e - - - - + 0x69, 0x66, 0x2d, 0x6e, 0x6f, 0x6e, 0x65, 0x2d, // i f - n o n e - + 0x6d, 0x61, 0x74, 0x63, 0x68, 0x00, 0x00, 0x00, // m a t c h - - - + 0x08, 0x69, 0x66, 0x2d, 0x72, 0x61, 0x6e, 0x67, // - i f - r a n g + 0x65, 0x00, 0x00, 0x00, 0x13, 0x69, 0x66, 0x2d, // e - - - - i f - + 0x75, 0x6e, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, // u n m o d i f i + 0x65, 0x64, 0x2d, 0x73, 0x69, 0x6e, 0x63, 0x65, // e d - s i n c e + 0x00, 0x00, 0x00, 0x0d, 0x6c, 0x61, 0x73, 0x74, // - - - - l a s t + 0x2d, 0x6d, 0x6f, 0x64, 0x69, 0x66, 0x69, 0x65, // - m o d i f i e + 0x64, 0x00, 0x00, 0x00, 0x08, 0x6c, 0x6f, 0x63, // d - - - - l o c + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, // a t i o n - - - + 0x0c, 0x6d, 0x61, 0x78, 0x2d, 0x66, 0x6f, 0x72, // - m a x - f o r + 0x77, 0x61, 0x72, 0x64, 0x73, 0x00, 0x00, 0x00, // w a r d s - - - + 0x06, 0x70, 0x72, 0x61, 0x67, 0x6d, 0x61, 0x00, // - p r a g m a - + 0x00, 0x00, 0x12, 0x70, 0x72, 0x6f, 0x78, 0x79, // - - - p r o x y + 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, 0x74, // - a u t h e n t + 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, 0x00, // i c a t e - - - + 0x13, 0x70, 0x72, 0x6f, 0x78, 0x79, 0x2d, 0x61, // - p r o x y - a + 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, // u t h o r i z a + 0x74, 0x69, 0x6f, 0x6e, 0x00, 0x00, 0x00, 0x05, // t i o n - - - - + 0x72, 0x61, 0x6e, 0x67, 0x65, 0x00, 0x00, 0x00, // r a n g e - - - + 0x07, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x72, // - r e f e r e r + 0x00, 0x00, 0x00, 0x0b, 0x72, 0x65, 0x74, 0x72, // - - - - r e t r + 0x79, 0x2d, 0x61, 0x66, 0x74, 0x65, 0x72, 0x00, // y - a f t e r - + 0x00, 0x00, 0x06, 0x73, 0x65, 0x72, 0x76, 0x65, // - - - s e r v e + 0x72, 0x00, 0x00, 0x00, 0x02, 0x74, 0x65, 0x00, // r - - - - t e - + 0x00, 0x00, 0x07, 0x74, 0x72, 0x61, 0x69, 0x6c, // - - - t r a i l + 0x65, 0x72, 0x00, 0x00, 0x00, 0x11, 0x74, 0x72, // e r - - - - t r + 0x61, 0x6e, 0x73, 0x66, 0x65, 0x72, 0x2d, 0x65, // a n s f e r - e + 0x6e, 0x63, 0x6f, 0x64, 0x69, 0x6e, 0x67, 0x00, // n c o d i n g - + 0x00, 0x00, 0x07, 0x75, 0x70, 0x67, 0x72, 0x61, // - - - u p g r a + 0x64, 0x65, 0x00, 0x00, 0x00, 0x0a, 0x75, 0x73, // d e - - - - u s + 0x65, 0x72, 0x2d, 0x61, 0x67, 0x65, 0x6e, 0x74, // e r - a g e n t + 0x00, 0x00, 0x00, 0x04, 0x76, 0x61, 0x72, 0x79, // - - - - v a r y + 0x00, 0x00, 0x00, 0x03, 0x76, 0x69, 0x61, 0x00, // - - - - v i a - + 0x00, 0x00, 0x07, 0x77, 0x61, 0x72, 0x6e, 0x69, // - - - w a r n i + 0x6e, 0x67, 0x00, 0x00, 0x00, 0x10, 0x77, 0x77, // n g - - - - w w + 0x77, 0x2d, 0x61, 0x75, 0x74, 0x68, 0x65, 0x6e, // w - a u t h e n + 0x74, 0x69, 0x63, 0x61, 0x74, 0x65, 0x00, 0x00, // t i c a t e - - + 0x00, 0x06, 0x6d, 0x65, 0x74, 0x68, 0x6f, 0x64, // - - m e t h o d + 0x00, 0x00, 0x00, 0x03, 0x67, 0x65, 0x74, 0x00, // - - - - g e t - + 0x00, 0x00, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, // - - - s t a t u + 0x73, 0x00, 0x00, 0x00, 0x06, 0x32, 0x30, 0x30, // s - - - - 2 0 0 + 0x20, 0x4f, 0x4b, 0x00, 0x00, 0x00, 0x07, 0x76, // - O K - - - - v + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x00, 0x00, // e r s i o n - - + 0x00, 0x08, 0x48, 0x54, 0x54, 0x50, 0x2f, 0x31, // - - H T T P - 1 + 0x2e, 0x31, 0x00, 0x00, 0x00, 0x03, 0x75, 0x72, // - 1 - - - - u r + 0x6c, 0x00, 0x00, 0x00, 0x06, 0x70, 0x75, 0x62, // l - - - - p u b + 0x6c, 0x69, 0x63, 0x00, 0x00, 0x00, 0x0a, 0x73, // l i c - - - - s + 0x65, 0x74, 0x2d, 0x63, 0x6f, 0x6f, 0x6b, 0x69, // e t - c o o k i + 0x65, 0x00, 0x00, 0x00, 0x0a, 0x6b, 0x65, 0x65, // e - - - - k e e + 0x70, 0x2d, 0x61, 0x6c, 0x69, 0x76, 0x65, 0x00, // p - a l i v e - + 0x00, 0x00, 0x06, 0x6f, 0x72, 0x69, 0x67, 0x69, // - - - o r i g i + 0x6e, 0x31, 0x30, 0x30, 0x31, 0x30, 0x31, 0x32, // n 1 0 0 1 0 1 2 + 0x30, 0x31, 0x32, 0x30, 0x32, 0x32, 0x30, 0x35, // 0 1 2 0 2 2 0 5 + 0x32, 0x30, 0x36, 0x33, 0x30, 0x30, 0x33, 0x30, // 2 0 6 3 0 0 3 0 + 0x32, 0x33, 0x30, 0x33, 0x33, 0x30, 0x34, 0x33, // 2 3 0 3 3 0 4 3 + 0x30, 0x35, 0x33, 0x30, 0x36, 0x33, 0x30, 0x37, // 0 5 3 0 6 3 0 7 + 0x34, 0x30, 0x32, 0x34, 0x30, 0x35, 0x34, 0x30, // 4 0 2 4 0 5 4 0 + 0x36, 0x34, 0x30, 0x37, 0x34, 0x30, 0x38, 0x34, // 6 4 0 7 4 0 8 4 + 0x30, 0x39, 0x34, 0x31, 0x30, 0x34, 0x31, 0x31, // 0 9 4 1 0 4 1 1 + 0x34, 0x31, 0x32, 0x34, 0x31, 0x33, 0x34, 0x31, // 4 1 2 4 1 3 4 1 + 0x34, 0x34, 0x31, 0x35, 0x34, 0x31, 0x36, 0x34, // 4 4 1 5 4 1 6 4 + 0x31, 0x37, 0x35, 0x30, 0x32, 0x35, 0x30, 0x34, // 1 7 5 0 2 5 0 4 + 0x35, 0x30, 0x35, 0x32, 0x30, 0x33, 0x20, 0x4e, // 5 0 5 2 0 3 - N + 0x6f, 0x6e, 0x2d, 0x41, 0x75, 0x74, 0x68, 0x6f, // o n - A u t h o + 0x72, 0x69, 0x74, 0x61, 0x74, 0x69, 0x76, 0x65, // r i t a t i v e + 0x20, 0x49, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, // - I n f o r m a + 0x74, 0x69, 0x6f, 0x6e, 0x32, 0x30, 0x34, 0x20, // t i o n 2 0 4 - + 0x4e, 0x6f, 0x20, 0x43, 0x6f, 0x6e, 0x74, 0x65, // N o - C o n t e + 0x6e, 0x74, 0x33, 0x30, 0x31, 0x20, 0x4d, 0x6f, // n t 3 0 1 - M o + 0x76, 0x65, 0x64, 0x20, 0x50, 0x65, 0x72, 0x6d, // v e d - P e r m + 0x61, 0x6e, 0x65, 0x6e, 0x74, 0x6c, 0x79, 0x34, // a n e n t l y 4 + 0x30, 0x30, 0x20, 0x42, 0x61, 0x64, 0x20, 0x52, // 0 0 - B a d - R + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x34, 0x30, // e q u e s t 4 0 + 0x31, 0x20, 0x55, 0x6e, 0x61, 0x75, 0x74, 0x68, // 1 - U n a u t h + 0x6f, 0x72, 0x69, 0x7a, 0x65, 0x64, 0x34, 0x30, // o r i z e d 4 0 + 0x33, 0x20, 0x46, 0x6f, 0x72, 0x62, 0x69, 0x64, // 3 - F o r b i d + 0x64, 0x65, 0x6e, 0x34, 0x30, 0x34, 0x20, 0x4e, // d e n 4 0 4 - N + 0x6f, 0x74, 0x20, 0x46, 0x6f, 0x75, 0x6e, 0x64, // o t - F o u n d + 0x35, 0x30, 0x30, 0x20, 0x49, 0x6e, 0x74, 0x65, // 5 0 0 - I n t e + 0x72, 0x6e, 0x61, 0x6c, 0x20, 0x53, 0x65, 0x72, // r n a l - S e r + 0x76, 0x65, 0x72, 0x20, 0x45, 0x72, 0x72, 0x6f, // v e r - E r r o + 0x72, 0x35, 0x30, 0x31, 0x20, 0x4e, 0x6f, 0x74, // r 5 0 1 - N o t + 0x20, 0x49, 0x6d, 0x70, 0x6c, 0x65, 0x6d, 0x65, // - I m p l e m e + 0x6e, 0x74, 0x65, 0x64, 0x35, 0x30, 0x33, 0x20, // n t e d 5 0 3 - + 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x20, // S e r v i c e - + 0x55, 0x6e, 0x61, 0x76, 0x61, 0x69, 0x6c, 0x61, // U n a v a i l a + 0x62, 0x6c, 0x65, 0x4a, 0x61, 0x6e, 0x20, 0x46, // b l e J a n - F + 0x65, 0x62, 0x20, 0x4d, 0x61, 0x72, 0x20, 0x41, // e b - M a r - A + 0x70, 0x72, 0x20, 0x4d, 0x61, 0x79, 0x20, 0x4a, // p r - M a y - J + 0x75, 0x6e, 0x20, 0x4a, 0x75, 0x6c, 0x20, 0x41, // u n - J u l - A + 0x75, 0x67, 0x20, 0x53, 0x65, 0x70, 0x74, 0x20, // u g - S e p t - + 0x4f, 0x63, 0x74, 0x20, 0x4e, 0x6f, 0x76, 0x20, // O c t - N o v - + 0x44, 0x65, 0x63, 0x20, 0x30, 0x30, 0x3a, 0x30, // D e c - 0 0 - 0 + 0x30, 0x3a, 0x30, 0x30, 0x20, 0x4d, 0x6f, 0x6e, // 0 - 0 0 - M o n + 0x2c, 0x20, 0x54, 0x75, 0x65, 0x2c, 0x20, 0x57, // - - T u e - - W + 0x65, 0x64, 0x2c, 0x20, 0x54, 0x68, 0x75, 0x2c, // e d - - T h u - + 0x20, 0x46, 0x72, 0x69, 0x2c, 0x20, 0x53, 0x61, // - F r i - - S a + 0x74, 0x2c, 0x20, 0x53, 0x75, 0x6e, 0x2c, 0x20, // t - - S u n - - + 0x47, 0x4d, 0x54, 0x63, 0x68, 0x75, 0x6e, 0x6b, // G M T c h u n k + 0x65, 0x64, 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, // e d - t e x t - + 0x68, 0x74, 0x6d, 0x6c, 0x2c, 0x69, 0x6d, 0x61, // h t m l - i m a + 0x67, 0x65, 0x2f, 0x70, 0x6e, 0x67, 0x2c, 0x69, // g e - p n g - i + 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x6a, 0x70, 0x67, // m a g e - j p g + 0x2c, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x2f, 0x67, // - i m a g e - g + 0x69, 0x66, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // i f - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x6d, 0x6c, 0x2c, 0x61, 0x70, 0x70, 0x6c, 0x69, // m l - a p p l i + 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x78, // c a t i o n - x + 0x68, 0x74, 0x6d, 0x6c, 0x2b, 0x78, 0x6d, 0x6c, // h t m l - x m l + 0x2c, 0x74, 0x65, 0x78, 0x74, 0x2f, 0x70, 0x6c, // - t e x t - p l + 0x61, 0x69, 0x6e, 0x2c, 0x74, 0x65, 0x78, 0x74, // a i n - t e x t + 0x2f, 0x6a, 0x61, 0x76, 0x61, 0x73, 0x63, 0x72, // - j a v a s c r + 0x69, 0x70, 0x74, 0x2c, 0x70, 0x75, 0x62, 0x6c, // i p t - p u b l + 0x69, 0x63, 0x70, 0x72, 0x69, 0x76, 0x61, 0x74, // i c p r i v a t + 0x65, 0x6d, 0x61, 0x78, 0x2d, 0x61, 0x67, 0x65, // e m a x - a g e + 0x3d, 0x67, 0x7a, 0x69, 0x70, 0x2c, 0x64, 0x65, // - g z i p - d e + 0x66, 0x6c, 0x61, 0x74, 0x65, 0x2c, 0x73, 0x64, // f l a t e - s d + 0x63, 0x68, 0x63, 0x68, 0x61, 0x72, 0x73, 0x65, // c h c h a r s e + 0x74, 0x3d, 0x75, 0x74, 0x66, 0x2d, 0x38, 0x63, // t - u t f - 8 c + 0x68, 0x61, 0x72, 0x73, 0x65, 0x74, 0x3d, 0x69, // h a r s e t - i + 0x73, 0x6f, 0x2d, 0x38, 0x38, 0x35, 0x39, 0x2d, // s o - 8 8 5 9 - + 0x31, 0x2c, 0x75, 0x74, 0x66, 0x2d, 0x2c, 0x2a, // 1 - u t f - - - + 0x2c, 0x65, 0x6e, 0x71, 0x3d, 0x30, 0x2e // - e n q - 0 - + }; + + public static void putInt(final ByteBuffer buffer, int value) { + buffer.put((byte) (value >> 24)); + buffer.put((byte) (value >> 16)); + buffer.put((byte) (value >> 8)); + buffer.put((byte) value); + } + + public static void putInt(final ByteBuffer buffer, int value, int position) { + buffer.put(position, (byte) (value >> 24)); + buffer.put(position + 1, (byte) (value >> 16)); + buffer.put(position + 2, (byte) (value >> 8)); + buffer.put(position + 3, (byte) value); + } + + public static int readInt(ByteBuffer buffer) { + int id = (buffer.get() & 0xFF) << 24; + id += (buffer.get() & 0xFF) << 16; + id += (buffer.get() & 0xFF) << 8; + id += (buffer.get() & 0xFF); + return id; + } + + private SpdyProtocolUtils() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPushBackParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPushBackParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyPushBackParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Parser that supports push back when not all data can be read. + * + * @author Stuart Douglas + */ +public abstract class SpdyPushBackParser { + + private byte[] pushedBackData; + private boolean finished; + protected int streamId = -1; + private int remainingData; + + public SpdyPushBackParser(int frameLength) { + this.remainingData = frameLength; + } + + public void parse(ByteBuffer data) throws IOException { + int used = 0; + ByteBuffer dataToParse = data; + int oldLimit = dataToParse.limit(); + try { + if (pushedBackData != null) { + dataToParse = ByteBuffer.wrap(new byte[pushedBackData.length + data.remaining()]); + dataToParse.put(pushedBackData); + dataToParse.put(data); + dataToParse.flip(); + oldLimit = dataToParse.limit(); + } + if(dataToParse.remaining() > remainingData) { + dataToParse.limit(dataToParse.position() + remainingData); + } + int rem = dataToParse.remaining(); + handleData(dataToParse); + used = rem - dataToParse.remaining(); + + } finally { + int leftOver = dataToParse.remaining(); + if(leftOver > 0) { + pushedBackData = new byte[leftOver]; + dataToParse.get(pushedBackData); + } else { + pushedBackData = null; + } + dataToParse.limit(oldLimit); + remainingData -= used; + if(remainingData == 0) { + finished = true; + finished(); + } + } + } + + protected void finished() throws IOException { + + } + + protected abstract void handleData(ByteBuffer resource) throws IOException; + + public boolean isFinished() { + return finished; + } + + public int getStreamId() { + return streamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pool; + +import java.nio.ByteBuffer; + +/** + * Parser for SPDY ping frames. + * + * @author Stuart Douglas + */ +class SpdyRstStreamParser extends SpdyPushBackParser { + + private int statusCode; + + public SpdyRstStreamParser(Pool bufferPool, int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource) { + if (resource.remaining() < 8) { + return; + } + streamId = SpdyProtocolUtils.readInt(resource); + statusCode = SpdyProtocolUtils.readInt(resource); + + } + + public int getStatusCode() { + return statusCode; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.nio.ByteBuffer; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +/** + * @author Stuart Douglas + */ +class SpdyRstStreamSinkChannel extends SpdyControlFrameStreamSinkChannel { + + private final int streamId; + private final int statusCode; + + protected SpdyRstStreamSinkChannel(SpdyChannel channel, int streamId, int statusCode) { + super(channel); + this.statusCode = statusCode; + this.streamId = streamId; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(16); + + int firstInt = SpdyChannel.CONTROL_FRAME | (getChannel().getSpdyVersion() << 16) | SpdyChannel.RST_STREAM; + SpdyProtocolUtils.putInt(buf, firstInt); + SpdyProtocolUtils.putInt(buf, 8); + SpdyProtocolUtils.putInt(buf, streamId); + SpdyProtocolUtils.putInt(buf, statusCode); + buf.flip(); + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyRstStreamStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.nio.ByteBuffer; +import org.xnio.Pooled; + +/** + * A SPDY Ping frame + * + * @author Stuart Douglas + */ +public class SpdyRstStreamStreamSourceChannel extends SpdyStreamSourceChannel { + + private final int streamId; + + SpdyRstStreamStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining, int streamId) { + super(framedChannel, data, frameDataRemaining); + this.streamId = streamId; + lastFrame(); + } + + public int getStreamId() { + return streamId; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySetting.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySetting.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySetting.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +/** + * A Spdy Setting + * + * @author Stuart Douglas + */ +public class SpdySetting { + + public static final int FLAG_SETTINGS_PERSIST_VALUE = 0x1; + public static final int FLAG_SETTINGS_PERSISTED = 0x2; + + public static final int SETTINGS_UPLOAD_BANDWIDTH = 1; + public static final int SETTINGS_DOWNLOAD_BANDWIDTH = 2; + public static final int SETTINGS_ROUND_TRIP_TIME = 3; + public static final int SETTINGS_MAX_CONCURRENT_STREAMS = 4; + public static final int SETTINGS_CURRENT_CWND = 5; + public static final int SETTINGS_DOWNLOAD_RETRANS_RATE = 6; + public static final int SETTINGS_INITIAL_WINDOW_SIZE = 7; + public static final int SETTINGS_CLIENT_CERTIFICATE_VECTOR_SIZE = 8; + + private final int flags; + private final int id; + private final int value; + + SpdySetting(int flags, int id, int value) { + this.flags = flags; + this.id = id; + this.value = value; + } + + public int getFlags() { + return flags; + } + + public int getId() { + return id; + } + + public int getValue() { + return value; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySettingsParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySettingsParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySettingsParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,83 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +/** + * SPDY settings parser + * + * @author Stuart Douglas + */ +class SpdySettingsParser extends SpdyPushBackParser { + + private int length = -1; + + private int count = 0; + + private final List settings = new ArrayList<>(); + + public SpdySettingsParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource) { + if (length == -1) { + if (resource.remaining() < 4) { + return; + } + length = (resource.get() & 0xFF) << 24; + length += (resource.get() & 0xFF) << 16; + length += (resource.get() & 0xFF) << 8; + length += (resource.get() & 0xFF); + } + while (count < length) { + if (resource.remaining() < 8) { + return; + } + int flags = resource.get() & 0xFF; + int id = (resource.get() & 0xFF) << 16; + id += (resource.get() & 0xFF) << 8; + id += (resource.get() & 0xFF); + int value = (resource.get() & 0xFF) << 24; + value += (resource.get() & 0xFF) << 16; + value += (resource.get() & 0xFF) << 8; + value += (resource.get() & 0xFF); + boolean found = false; + //according to the spec we MUST ignore duplicates + for (SpdySetting existing : settings) { + if (existing.getId() == id) { + found = true; + break; + } + } + if (!found) { + settings.add(new SpdySetting(flags, id, value)); + } + count++; + } + } + + public List getSettings() { + return settings; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySettingsStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySettingsStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySettingsStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pooled; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; + +/** + * A spdy Settings frame + * + * + * @author Stuart Douglas + */ +public class SpdySettingsStreamSourceChannel extends SpdyStreamSourceChannel { + + private final List settings; + + + SpdySettingsStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining, List settings) { + super(framedChannel, data, frameDataRemaining); + this.settings = settings; + lastFrame(); + } + + public List getSettings() { + return Collections.unmodifiableList(settings); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSinkChannel; + +/** + * @author Stuart Douglas + */ +public class SpdyStreamSinkChannel extends AbstractFramedStreamSinkChannel { + + SpdyStreamSinkChannel(SpdyChannel channel) { + super(channel); + } + + @Override + protected boolean isLastFrame() { + return false; + } + + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.nio.ByteBuffer; +import org.xnio.Bits; +import org.xnio.Pooled; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; + +/** + * SPDY stream source channel + * + * @author Stuart Douglas + */ +public class SpdyStreamSourceChannel extends AbstractFramedStreamSourceChannel { + + SpdyStreamSourceChannel(SpdyChannel framedChannel) { + super(framedChannel); + } + + SpdyStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining) { + super(framedChannel, data, frameDataRemaining); + } + + @Override + protected void handleHeaderData(FrameHeaderData headerData) { + SpdyChannel.SpdyFrameParser data = (SpdyChannel.SpdyFrameParser) headerData; + if(Bits.anyAreSet(data.flags, SpdyChannel.FLAG_FIN)) { + this.lastFrame(); + } + } + + public SpdyChannel getSpdyChannel() { + return getFramedChannel(); + } + + @Override + protected void lastFrame() { + super.lastFrame(); + } + + void rstStream() { + //noop by default + } + + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyStreamStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,269 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.UndertowMessages; +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; + +import org.xnio.IoUtils; +import org.xnio.Pooled; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.zip.Deflater; + +/** + * @author Stuart Douglas + */ +public abstract class SpdyStreamStreamSinkChannel extends SpdyStreamSinkChannel { + + private final int streamId; + private volatile boolean reset = false; + + //flow control related items. Accessed under lock + private int flowControlWindow; + private int initialWindowSize; //we track the initial window size, and then re-query it to get any delta + + private SendFrameHeader header; + + SpdyStreamStreamSinkChannel(SpdyChannel channel, int streamId) { + super(channel); + this.streamId = streamId; + this.flowControlWindow = channel.getInitialWindowSize(); + this.initialWindowSize = this.flowControlWindow; + } + + public int getStreamId() { + return streamId; + } + + SendFrameHeader generateSendFrameHeader() { + header = createFrameHeaderImpl(); + return header; + } + + void clearHeader() { + this.header = null; + } + + @Override + protected void channelForciblyClosed() throws IOException { + getChannel().removeStreamSink(getStreamId()); + if(reset) { + return; + } + reset = true; + if (streamId % 2 == (getChannel().isClient() ? 1 : 0)) { + //we initiated the stream + //we only actually reset if we have sent something to the other endpoint + if(isFirstDataWritten()) { + getChannel().sendRstStream(streamId, SpdyChannel.RST_STATUS_CANCEL); + } + } else { + getChannel().sendRstStream(streamId, SpdyChannel.RST_STATUS_INTERNAL_ERROR); + } + } + + @Override + protected final SendFrameHeader createFrameHeader() { + SendFrameHeader header = this.header; + this.header = null; + return header; + } + + @Override + protected void handleFlushComplete(boolean finalFrame) { + if(finalFrame) { + getChannel().removeStreamSink(getStreamId()); + } + } + + protected Pooled[] createHeaderBlock(Pooled firstHeaderBuffer, Pooled[] allHeaderBuffers, ByteBuffer firstBuffer, HeaderMap headers) { + Pooled outPooled = getChannel().getHeapBufferPool().allocate(); + Pooled inPooled = getChannel().getHeapBufferPool().allocate(); + try { + + Pooled currentPooled = firstHeaderBuffer; + ByteBuffer inputBuffer = inPooled.getResource(); + ByteBuffer outputBuffer = outPooled.getResource(); + + SpdyProtocolUtils.putInt(inputBuffer, headers.size()); + + long fiCookie = headers.fastIterateNonEmpty(); + while (fiCookie != -1) { + HeaderValues headerValues = headers.fiCurrent(fiCookie); + + int valueSize = headerValues.size() - 1; //null between the characters + for (int i = 0; i < headerValues.size(); ++i) { + String val = headerValues.get(i); + valueSize += val.length(); + } + int totalSize = 8 + headerValues.getHeaderName().length() + valueSize; // 8 == two ints for name and value sizes + + if (totalSize > inputBuffer.limit()) { + //todo: support large single headers + throw UndertowMessages.MESSAGES.headersTooLargeToFitInHeapBuffer(); + } else if (totalSize > inputBuffer.remaining()) { + allHeaderBuffers = doDeflate(inputBuffer, outputBuffer, currentPooled, allHeaderBuffers); + if(allHeaderBuffers != null) { + currentPooled = allHeaderBuffers[allHeaderBuffers.length - 1]; + } + inputBuffer.clear(); + outputBuffer.clear(); + } + + //TODO: for now it just fails if there are too many headers + SpdyProtocolUtils.putInt(inputBuffer, headerValues.getHeaderName().length()); + for (int i = 0; i < headerValues.getHeaderName().length(); ++i) { + inputBuffer.put((byte) (Character.toLowerCase((char) headerValues.getHeaderName().byteAt(i)))); + } + SpdyProtocolUtils.putInt(inputBuffer, valueSize); + for (int i = 0; i < headerValues.size(); ++i) { + String val = headerValues.get(i); + for (int j = 0; j < val.length(); ++j) { + inputBuffer.put((byte) val.charAt(j)); + } + if (i != headerValues.size() - 1) { + inputBuffer.put((byte) 0); + } + } + fiCookie = headers.fiNext(fiCookie); + } + + allHeaderBuffers = doDeflate(inputBuffer, outputBuffer, currentPooled, allHeaderBuffers); + + int totalLength; + if (allHeaderBuffers != null) { + totalLength = -8; + for (Pooled b : allHeaderBuffers) { + totalLength += b.getResource().position(); + } + } else { + totalLength = firstBuffer.position() - 8; + } + + SpdyProtocolUtils.putInt(firstBuffer, ((isWritesShutdown() && !getBuffer().hasRemaining() ? SpdyChannel.FLAG_FIN : 0) << 24) | totalLength, 4); + + } finally { + inPooled.free(); + outPooled.free(); + } + return allHeaderBuffers; + } + + + protected abstract SendFrameHeader createFrameHeaderImpl(); + + /** + * This method should be called before sending. It will return the amount of + * data that can be sent, taking into account the stream and connection flow + * control windows, and the toSend parameter. + *

+ * It will decrement the flow control windows by the amount that can be sent, + * so this method should only be called as a frame is being queued. + * + * @return The number of bytes that can be sent + */ + protected synchronized int grabFlowControlBytes(int toSend) { + if(toSend == 0) { + return 0; + } + int newWindowSize = this.getChannel().getInitialWindowSize(); + int settingsDelta = newWindowSize - this.initialWindowSize; + //first adjust for any settings frame updates + this.initialWindowSize = newWindowSize; + this.flowControlWindow += settingsDelta; + + int min = Math.min(toSend, this.flowControlWindow); + int actualBytes = this.getChannel().grabFlowControlBytes(min); + this.flowControlWindow -= actualBytes; + return actualBytes; + } + + synchronized void updateFlowControlWindow(final int delta) throws IOException { + boolean exhausted = flowControlWindow == 0; + flowControlWindow += delta; + if (exhausted) { + getChannel().notifyFlowControlAllowed(); + if (isWriteResumed()) { + resumeWritesInternal(true); + } + } + } + + + private Pooled[] doDeflate(ByteBuffer inputBuffer, ByteBuffer outputBuffer, Pooled currentPooled, Pooled[] allHeaderBuffers) { + Deflater deflater = getDeflater(); + deflater.setInput(inputBuffer.array(), inputBuffer.arrayOffset(), inputBuffer.position()); + + int deflated; + do { + deflated = deflater.deflate(outputBuffer.array(), outputBuffer.arrayOffset(), outputBuffer.remaining(), Deflater.SYNC_FLUSH); + if (deflated <= currentPooled.getResource().remaining()) { + currentPooled.getResource().put(outputBuffer.array(), outputBuffer.arrayOffset(), deflated); + } else { + int pos = outputBuffer.arrayOffset(); + int remaining = deflated; + ByteBuffer current = currentPooled.getResource(); + do { + int toPut = Math.min(current.remaining(), remaining); + current.put(outputBuffer.array(), pos, toPut); + pos += toPut; + remaining -= toPut; + if (remaining > 0) { + allHeaderBuffers = allocateAll(allHeaderBuffers, currentPooled); + currentPooled = allHeaderBuffers[allHeaderBuffers.length - 1]; + current = currentPooled.getResource(); + } + } while (remaining > 0); + } + } while (!deflater.needsInput()); + return allHeaderBuffers; + } + + protected abstract Deflater getDeflater(); + + protected Pooled[] allocateAll(Pooled[] allHeaderBuffers, Pooled currentBuffer) { + Pooled[] ret; + if (allHeaderBuffers == null) { + ret = new Pooled[2]; + ret[0] = currentBuffer; + ret[1] = getChannel().getBufferPool().allocate(); + } else { + ret = new Pooled[allHeaderBuffers.length + 1]; + System.arraycopy(allHeaderBuffers, 0, ret, 0, allHeaderBuffers.length); + ret[ret.length - 1] = getChannel().getBufferPool().allocate(); + } + return ret; + } + + /** + * Method that is invoked when the stream is reset. + */ + void rstStream() { + if(reset) { + return; + } + reset = true; + IoUtils.safeClose(this); + getChannel().removeStreamSink(getStreamId()); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pool; + +import java.nio.ByteBuffer; +import java.util.zip.Inflater; + +/** + * Parser for SPDY syn reply frames. + * + * @author Stuart Douglas + */ +class SpdySynReplyParser extends SpdyHeaderBlockParser { + + public SpdySynReplyParser(Pool bufferPool, SpdyChannel channel, int frameLength, Inflater inflater) { + super(bufferPool, channel, frameLength, inflater); + } + + @Override + protected boolean handleBeforeHeader(ByteBuffer resource) { + if (resource.remaining() < 4) { + return false; + } + streamId = SpdyProtocolUtils.readInt(resource); + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,156 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.ImmediatePooled; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; +import java.util.zip.Deflater; + +/** + * @author Stuart Douglas + */ +public class SpdySynReplyStreamSinkChannel extends SpdyStreamStreamSinkChannel { + + private final HeaderMap headers = new HeaderMap(); + + private boolean first = true; + private final Deflater deflater; + private ChannelListener completionListener; + + + SpdySynReplyStreamSinkChannel(SpdyChannel channel, int streamId, Deflater deflater) { + super(channel, streamId); + this.deflater = deflater; + } + + @Override + protected SendFrameHeader createFrameHeaderImpl() { + final int fcWindow = grabFlowControlBytes(getBuffer().remaining()); + if (fcWindow == 0 && getBuffer().hasRemaining()) { + //flow control window is exhausted + return new SendFrameHeader(getBuffer().remaining(), null); + } + final boolean finalFrame = isWritesShutdown() && fcWindow >= getBuffer().remaining(); + Pooled firstHeaderBuffer = getChannel().getBufferPool().allocate(); + Pooled[] allHeaderBuffers = null; + ByteBuffer firstBuffer = firstHeaderBuffer.getResource(); + boolean firstFrame = false; + if (first) { + firstFrame = true; + first = false; + int firstInt = SpdyChannel.CONTROL_FRAME | (getChannel().getSpdyVersion() << 16) | 2; + SpdyProtocolUtils.putInt(firstBuffer, firstInt); + SpdyProtocolUtils.putInt(firstBuffer, 0); //we back fill the length + HeaderMap headers = this.headers; + + SpdyProtocolUtils.putInt(firstBuffer, getStreamId()); + + + headers.remove(Headers.CONNECTION); //todo: should this be here? + headers.remove(Headers.KEEP_ALIVE); + headers.remove(Headers.TRANSFER_ENCODING); + + allHeaderBuffers = createHeaderBlock(firstHeaderBuffer, allHeaderBuffers, firstBuffer, headers); + } + + Pooled currentPooled = allHeaderBuffers == null ? firstHeaderBuffer : allHeaderBuffers[allHeaderBuffers.length - 1]; + ByteBuffer currentBuffer = currentPooled.getResource(); + int remainingInBuffer = 0; + if (getBuffer().remaining() > 0) { + if (fcWindow > 0) { + //make sure we have room in the header buffer + if(currentBuffer.remaining() < 8) { + allHeaderBuffers = allocateAll(allHeaderBuffers, currentPooled); + currentPooled = allHeaderBuffers == null ? firstHeaderBuffer : allHeaderBuffers[allHeaderBuffers.length - 1]; + currentBuffer = currentPooled.getResource(); + } + remainingInBuffer = getBuffer().remaining() - fcWindow; + getBuffer().limit(getBuffer().position() + fcWindow); + SpdyProtocolUtils.putInt(currentBuffer, getStreamId()); + SpdyProtocolUtils.putInt(currentBuffer, ((finalFrame ? SpdyChannel.FLAG_FIN : 0) << 24) + fcWindow); + } else { + remainingInBuffer = getBuffer().remaining(); + } + } else if(finalFrame && !firstFrame) { + SpdyProtocolUtils.putInt(currentBuffer, getStreamId()); + SpdyProtocolUtils.putInt(currentBuffer, SpdyChannel.FLAG_FIN << 24); + } + if (allHeaderBuffers == null) { + //only one buffer required + currentBuffer.flip(); + return new SendFrameHeader(remainingInBuffer, currentPooled); + } else { + //headers were too big to fit in one buffer + //for now we will just copy them into a big buffer + int length = 0; + for (int i = 0; i < allHeaderBuffers.length; ++i) { + length += allHeaderBuffers[i].getResource().position(); + allHeaderBuffers[i].getResource().flip(); + } + try { + ByteBuffer newBuf = ByteBuffer.allocate(length); + + for (int i = 0; i < allHeaderBuffers.length; ++i) { + newBuf.put(allHeaderBuffers[i].getResource()); + } + newBuf.flip(); + return new SendFrameHeader(remainingInBuffer, new ImmediatePooled(newBuf)); + } finally { + //the allocate can oome + for (int i = 0; i < allHeaderBuffers.length; ++i) { + allHeaderBuffers[i].free(); + } + } + } + } + + @Override + protected Deflater getDeflater() { + return deflater; + } + + public HeaderMap getHeaders() { + return headers; + } + + @Override + protected void handleFlushComplete(boolean finalFrame) { + super.handleFlushComplete(finalFrame); + if (finalFrame) { + if (completionListener != null) { + ChannelListeners.invokeChannelListener(this, completionListener); + } + } + } + + public ChannelListener getCompletionListener() { + return completionListener; + } + + public void setCompletionListener(ChannelListener completionListener) { + this.completionListener = completionListener; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynReplyStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,152 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * @author Stuart Douglas + */ +public class SpdySynReplyStreamSourceChannel extends SpdyStreamSourceChannel { + + private final HeaderMap headers; + private final int streamId; + private volatile HeaderMap newHeaders = null; + private int flowControlWindow; + private boolean rst = false; + + SpdySynReplyStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining, HeaderMap headers, int streamId) { + super(framedChannel, data, frameDataRemaining); + this.headers = headers; + this.streamId = streamId; + this.flowControlWindow = framedChannel.getInitialWindowSize(); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + handleNewHeaders(); + int read = super.read(dst); + updateFlowControlWindow(read); + return read; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + handleNewHeaders(); + long read = super.read(dsts, offset, length); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + handleNewHeaders(); + long read = super.read(dsts); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel streamSinkChannel) throws IOException { + handleNewHeaders(); + long read = super.transferTo(count, throughBuffer, streamSinkChannel); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + handleNewHeaders(); + long read = super.transferTo(position, count, target); + updateFlowControlWindow((int) read); + return read; + } + + /** + * Merge any new headers from HEADERS blocks into the exchange. + */ + private void handleNewHeaders() { + if (newHeaders != null) { + synchronized (this) { + for (HeaderValues header : newHeaders) { + headers.addAll(header.getHeaderName(), header); + } + } + newHeaders = null; + } + } + + synchronized void addNewHeaders(HeaderMap headers) { + if (newHeaders != null) { + newHeaders = headers; + } else { + for (HeaderValues header : headers) { + newHeaders.addAll(header.getHeaderName(), header); + } + } + } + + private void updateFlowControlWindow(final int read) { + if(read == 0) { + return; + } + flowControlWindow -= read; + //TODO: RST stream if flow control limits are exceeded? + //TODO: make this configurable, we should be able to set the policy that is used to determine when to update the window size + SpdyChannel spdyChannel = getSpdyChannel(); + spdyChannel.updateReceiveFlowControlWindow(read); + int initialWindowSize = spdyChannel.getInitialWindowSize(); + if (flowControlWindow < (initialWindowSize / 2)) { + int delta = initialWindowSize - flowControlWindow; + flowControlWindow += delta; + spdyChannel.sendUpdateWindowSize(streamId, delta); + } + } + + public HeaderMap getHeaders() { + return headers; + } + + public int getStreamId() { + return streamId; + } + + @Override + void rstStream() { + if(rst) { + return; + } + rst = true; + markStreamBroken(); + getSpdyChannel().sendRstStream(streamId, SpdyChannel.RST_STATUS_REFUSED_STREAM); + } + + @Override + protected void channelForciblyClosed() { + rstStream(); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,77 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import org.xnio.Pool; + +import java.nio.ByteBuffer; +import java.util.zip.Inflater; + +/** + * Parser for SPDY syn stream frames + * + * @author Stuart Douglas + */ +class SpdySynStreamParser extends SpdyHeaderBlockParser { + + private static final int STREAM_ID_MASK = ~(1 << 7); + private int associatedToStreamId = -1; + private int priority = -1; + + public SpdySynStreamParser(Pool bufferPool, SpdyChannel channel, int frameLength, Inflater inflater) { + super(bufferPool, channel, frameLength, inflater); + } + + protected boolean handleBeforeHeader(ByteBuffer resource) { + if (streamId == -1) { + if (resource.remaining() < 4) { + return false; + } + streamId = (resource.get() & STREAM_ID_MASK & 0xFF) << 24; + streamId += (resource.get() & 0xFF) << 16; + streamId += (resource.get() & 0xFF) << 8; + streamId += (resource.get() & 0xFF); + } + if (associatedToStreamId == -1) { + if (resource.remaining() < 4) { + return false; + } + associatedToStreamId = (resource.get() & STREAM_ID_MASK & 0xFF) << 24; + associatedToStreamId += (resource.get() & 0xFF) << 16; + associatedToStreamId += (resource.get() & 0xFF) << 8; + associatedToStreamId += (resource.get() & 0xFF); + } + if (priority == -1) { + if (resource.remaining() < 2) { + return false; + } + priority = (resource.get() >> 5) & 0xFF; + resource.get(); //unused at the moment + } + return true; + } + + public int getAssociatedToStreamId() { + return associatedToStreamId; + } + + public int getPriority() { + return priority; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,126 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.ImmediatePooled; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; +import java.util.zip.Deflater; + +/** + * @author Stuart Douglas + */ +public class SpdySynStreamStreamSinkChannel extends SpdyStreamStreamSinkChannel { + + private final HeaderMap headers; + private boolean first = true; + private final Deflater deflater; + + SpdySynStreamStreamSinkChannel(SpdyChannel channel, HeaderMap headers, int streamId, Deflater deflater) { + super(channel, streamId); + this.headers = headers; + this.deflater = deflater; + } + + @Override + protected SendFrameHeader createFrameHeaderImpl() { + + int fcWindow = grabFlowControlBytes(getBuffer().remaining()); + if (fcWindow == 0 && getBuffer().hasRemaining()) { + return new SendFrameHeader(getBuffer().remaining(), null); + } + final boolean finalFrame = isWritesShutdown() && fcWindow >= getBuffer().remaining(); + Pooled firstHeaderBuffer = getChannel().getBufferPool().allocate(); + Pooled[] allHeaderBuffers = null; + ByteBuffer firstBuffer = firstHeaderBuffer.getResource(); + boolean firstFrame = false; + if (first) { + firstFrame = true; + first = false; + int firstInt = SpdyChannel.CONTROL_FRAME | (getChannel().getSpdyVersion() << 16) | 1; + SpdyProtocolUtils.putInt(firstBuffer, firstInt); + SpdyProtocolUtils.putInt(firstBuffer, 0); //we back fill the length + HeaderMap headers = this.headers; + + SpdyProtocolUtils.putInt(firstBuffer, getStreamId()); + SpdyProtocolUtils.putInt(firstBuffer, 0); + firstBuffer.put((byte) 0); + firstBuffer.put((byte) 0); + + + headers.remove(Headers.CONNECTION); //todo: should this be here? + headers.remove(Headers.KEEP_ALIVE); + headers.remove(Headers.TRANSFER_ENCODING); + + allHeaderBuffers = createHeaderBlock(firstHeaderBuffer, allHeaderBuffers, firstBuffer, headers); + } + Pooled currentPooled = allHeaderBuffers == null ? firstHeaderBuffer : allHeaderBuffers[allHeaderBuffers.length - 1]; + ByteBuffer currentBuffer = currentPooled.getResource(); + int remainingInBuffer = 0; + if (getBuffer().remaining() > 0) { + remainingInBuffer = getBuffer().remaining() - fcWindow; + getBuffer().limit(getBuffer().position() + fcWindow); + if (currentBuffer.remaining() < 8) { + allHeaderBuffers = allocateAll(allHeaderBuffers, currentPooled); + currentPooled = allHeaderBuffers[allHeaderBuffers.length - 1]; + currentBuffer = currentPooled.getResource(); + } + SpdyProtocolUtils.putInt(currentBuffer, getStreamId()); + SpdyProtocolUtils.putInt(currentBuffer, ((finalFrame ? SpdyChannel.FLAG_FIN : 0) << 24) + fcWindow); + } else if(finalFrame && !firstFrame) { + SpdyProtocolUtils.putInt(currentBuffer, getStreamId()); + SpdyProtocolUtils.putInt(currentBuffer, SpdyChannel.FLAG_FIN << 24); + } + if (allHeaderBuffers == null) { + //only one buffer required + currentBuffer.flip(); + return new SendFrameHeader(remainingInBuffer, currentPooled); + } else { + //headers were too big to fit in one buffer + //for now we will just copy them into a big buffer + int length = 0; + for (int i = 0; i < allHeaderBuffers.length; ++i) { + length += allHeaderBuffers[i].getResource().position(); + allHeaderBuffers[i].getResource().flip(); + } + try { + ByteBuffer newBuf = ByteBuffer.allocate(length); + for (int i = 0; i < allHeaderBuffers.length; ++i) { + newBuf.put(allHeaderBuffers[i].getResource()); + } + newBuf.flip(); + return new SendFrameHeader(remainingInBuffer, new ImmediatePooled(newBuf)); + } finally { + //the allocate can oome + for (int i = 0; i < allHeaderBuffers.length; ++i) { + allHeaderBuffers[i].free(); + } + } + } + } + + @Override + protected Deflater getDeflater() { + return deflater; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdySynStreamStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,180 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.zip.Deflater; + +/** + * @author Stuart Douglas + */ +public class SpdySynStreamStreamSourceChannel extends SpdyStreamSourceChannel { + + + private boolean rst = false; + private final Deflater deflater; + private final HeaderMap headers; + private final int streamId; + private HeaderMap newHeaders = null; + private SpdySynReplyStreamSinkChannel synResponse; + private int flowControlWindow; + private ChannelListener completionListener; + + SpdySynStreamStreamSourceChannel(SpdyChannel framedChannel, Pooled data, long frameDataRemaining, Deflater deflater, HeaderMap headers, int streamId) { + super(framedChannel, data, frameDataRemaining); + this.deflater = deflater; + this.headers = headers; + this.streamId = streamId; + } + + public SpdySynReplyStreamSinkChannel getResponseChannel() { + if(synResponse != null) { + return synResponse; + } + synResponse = new SpdySynReplyStreamSinkChannel(getSpdyChannel(), streamId, deflater); + getSpdyChannel().registerStreamSink(synResponse); + return synResponse; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + handleNewHeaders(); + int read = super.read(dst); + updateFlowControlWindow(read); + return read; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + handleNewHeaders(); + long read = super.read(dsts, offset, length); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + handleNewHeaders(); + long read = super.read(dsts); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel streamSinkChannel) throws IOException { + handleNewHeaders(); + long read = super.transferTo(count, throughBuffer, streamSinkChannel); + updateFlowControlWindow((int) read); + return read; + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + handleNewHeaders(); + long read = super.transferTo(position, count, target); + updateFlowControlWindow((int) read); + return read; + } + + /** + * Merge any new headers from HEADERS blocks into the exchange. + */ + private synchronized void handleNewHeaders() { + if (newHeaders != null) { + for (HeaderValues header : newHeaders) { + headers.addAll(header.getHeaderName(), header); + } + newHeaders = null; + } + } + + synchronized void addNewHeaders(HeaderMap headers) { + if (newHeaders != null) { + newHeaders = headers; + } else { + for (HeaderValues header : headers) { + newHeaders.addAll(header.getHeaderName(), header); + } + } + } + + private void updateFlowControlWindow(final int read) { + if(read <= 0) { + return; + } + flowControlWindow -= read; + //TODO: RST stream if flow control limits are exceeded? + //TODO: make this configurable, we should be able to set the policy that is used to determine when to update the window size + SpdyChannel spdyChannel = getSpdyChannel(); + spdyChannel.updateReceiveFlowControlWindow(read); + int initialWindowSize = spdyChannel.getInitialWindowSize(); + if(flowControlWindow < (initialWindowSize / 2)) { + int delta = initialWindowSize - flowControlWindow; + flowControlWindow += delta; + spdyChannel.sendUpdateWindowSize(streamId, delta); + } + } + + @Override + protected void complete() throws IOException { + super.complete(); + if(completionListener != null) { + ChannelListeners.invokeChannelListener(this, completionListener); + } + } + + public HeaderMap getHeaders() { + return headers; + } + + public ChannelListener getCompletionListener() { + return completionListener; + } + + public void setCompletionListener(ChannelListener completionListener) { + this.completionListener = completionListener; + } + + @Override + void rstStream() { + if(rst) { + return; + } + rst = true; + markStreamBroken(); + getSpdyChannel().sendRstStream(streamId, SpdyChannel.RST_STATUS_CANCEL); + } + + @Override + protected void channelForciblyClosed() { + if(completionListener != null) { + completionListener.handleEvent(this); + } + rstStream(); + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyWindowUpdateParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyWindowUpdateParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyWindowUpdateParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.nio.ByteBuffer; + +/** + * Parser for SPDY ping frames. + * + * @author Stuart Douglas + */ +class SpdyWindowUpdateParser extends SpdyPushBackParser { + + private int deltaWindowSize; + + public SpdyWindowUpdateParser(int frameLength) { + super(frameLength); + } + + @Override + protected void handleData(ByteBuffer resource) { + if (resource.remaining() < 8) { + return; + } + streamId = SpdyProtocolUtils.readInt(resource); + deltaWindowSize = SpdyProtocolUtils.readInt(resource); + + } + + public int getDeltaWindowSize() { + return deltaWindowSize; + } +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyWindowUpdateStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyWindowUpdateStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/SpdyWindowUpdateStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.util.ImmediatePooled; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +class SpdyWindowUpdateStreamSinkChannel extends SpdyControlFrameStreamSinkChannel { + + private final int streamId; + private final int deltaWindowSize; + + protected SpdyWindowUpdateStreamSinkChannel(SpdyChannel channel, int streamId, int deltaWindowSize) { + super(channel); + this.streamId = streamId; + this.deltaWindowSize = deltaWindowSize; + } + + @Override + protected SendFrameHeader createFrameHeader() { + ByteBuffer buf = ByteBuffer.allocate(16); + + int firstInt = SpdyChannel.CONTROL_FRAME | (getChannel().getSpdyVersion() << 16) | 9; + SpdyProtocolUtils.putInt(buf, firstInt); + SpdyProtocolUtils.putInt(buf, 8); + SpdyProtocolUtils.putInt(buf, streamId); + SpdyProtocolUtils.putInt(buf, deltaWindowSize); + buf.flip(); + return new SendFrameHeader(new ImmediatePooled<>(buf)); + } + +} Index: 3rdParty_sources/undertow/io/undertow/protocols/spdy/StreamErrorException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/protocols/spdy/StreamErrorException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/protocols/spdy/StreamErrorException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.protocols.spdy; + +import java.io.IOException; + +/** + * @author Stuart Douglas + */ +public class StreamErrorException extends IOException { + + public static final int PROTOCOL_ERROR = 1; + public static final int INVALID_STREAM = 2; + public static final int REFUSED_STREAM = 3; + public static final int UNSUPPORTED_VERSION = 4; + public static final int CANCEL = 5; + public static final int INTERNAL_ERROR = 6; + public static final int FLOW_CONTROL_ERROR = 7; + public static final int STREAM_IN_USE = 8; + public static final int STREAM_ALREADY_CLOSED = 9; + + public static final int FRAME_TOO_LARGE = 11; + + private final int errorId; + + public StreamErrorException(int errorId) { + this.errorId = errorId; + } + + public int getErrorId() { + return errorId; + } +} Index: 3rdParty_sources/undertow/io/undertow/security/api/AuthenticatedSessionManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/AuthenticatedSessionManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/AuthenticatedSessionManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import io.undertow.security.idm.Account; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +import java.io.Serializable; + +/** + * Interface that represents a persistent authenticated session. + * + * @author Stuart Douglas + * @author Darran Lofthouse + */ +public interface AuthenticatedSessionManager { + + /** + * The attachment key that is used to attach the manager to the exchange + */ + AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(AuthenticatedSessionManager.class); + + AuthenticatedSession lookupSession(final HttpServerExchange exchange); + + void clearSession(HttpServerExchange exchange); + + public static class AuthenticatedSession implements Serializable { + + private final Account account; + private final String mechanism; + + public AuthenticatedSession(final Account account, final String mechanism) { + this.account = account; + this.mechanism = mechanism; + } + + public Account getAccount() { + return account; + } + + public String getMechanism() { + return mechanism; + } + + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,151 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import io.undertow.server.HttpServerExchange; + +/** + * The interface to be implemented by a single authentication mechanism. + *

+ * The implementation of this interface are assumed to be stateless, if there is a need to share state between the authenticate + * and handleComplete calls then it should be held in the HttpServerExchange. + *

+ * As an in-bound request is received the authenticate method is called on each mechanism in turn until one of the following + * occurs: - - A mechanism successfully authenticates the incoming request. - A mechanism attempts but fails to authenticate the + * request. - The list of mechanisms is exhausted. + *

+ * This means that if the authenticate method is called on a mechanism it should assume it is required to check if it can + * actually authenticate the incoming request, anything that would prevent it from performing the check would have already + * stopped the authenticate method from being called. + *

+ * Authentication is allowed to proceed if either authentication was required AND one handler authenticated the request or it is + * allowed to proceed if it is not required AND no handler failed to authenticate the request. + *

+ * The handleComplete methods are used as the request processing is returning up the chain, primarily these are used to + * challenge the client to authenticate but where supported by the mechanism they could also be used to send mechanism specific + * updates back with a request. + *

+ * If a mechanism successfully authenticated the incoming request then only the handleComplete method on that mechanism is + * called. + *

+ * If any mechanism failed or if authentication was required and no mechanism succeeded in authenticating the request then + * handleComplete will be called for all mechanisms. + *

+ * Finally if authentication was not required handleComplete will not be called for any of the mechanisms. + *

+ * The mechanisms will need to double check why handleComplete is being called, if the request was authenticated then they + * should do nothing unless the mechanism has intermediate state to send back. If the request was not authenticated then a + * challenge should be sent. + * + * @author Stuart Douglas + * @author Darran Lofthouse + */ +public interface AuthenticationMechanism { + + /** + * Perform authentication of the request. Any potentially blocking work should be performed in the handoff executor provided + * + * @param exchange The exchange + * @return + */ + AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, + final SecurityContext securityContext); + + /** + * Send an authentication challenge to the remote client. + *

+ * The individual mechanisms should update the response headers and body of the message as appropriate however they should + * not set the response code, instead that should be indicated in the {@link ChallengeResult} and the most appropriate + * overall response code will be selected. + * + * @param exchange The exchange + * @param securityContext The security context + * @return A {@link ChallengeResult} indicating if a challenge was sent and the desired response code. + */ + ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext); + + /** + * The AuthenticationOutcome is used by an AuthenticationMechanism to indicate the outcome of the call to authenticate, the + * overall authentication process will then used this along with the current AuthenticationState to decide how to proceed + * with the current request. + */ + public enum AuthenticationMechanismOutcome { + /** + * Based on the current request the mechanism has successfully performed authentication. + */ + AUTHENTICATED, + + /** + * The mechanism did not attempt authentication on this request, most likely due to not discovering any applicable + * security tokens for this mechanisms in the request. + */ + NOT_ATTEMPTED, + + /** + * The mechanism attempted authentication but it did not complete, this could either be due to a failure validating the + * tokens from the client or it could be due to the mechanism requiring at least one additional round trip with the + * client - either way the request will return challenges to the client. + */ + NOT_AUTHENTICATED; + } + + /** + * Simple class to wrap the result of requesting a mechanism sends it's challenge. + */ + public class ChallengeResult { + + private final boolean challengeSent; + private final Integer statusCode; + + public ChallengeResult(final boolean challengeSent, final Integer statusCode) { + this.statusCode = statusCode; + this.challengeSent = challengeSent; + } + + public ChallengeResult(final boolean challengeSent) { + this(challengeSent, null); + } + + /** + * Obtain the response code desired by this mechanism for the challenge. + *

+ * Where multiple mechanisms are in use concurrently all of the requested response codes will be checked and the most + * suitable one selected. If no specific response code is required any value less than 0 can be set. + * + * @return The desired response code or null if no code specified. + */ + public Integer getDesiredResponseCode() { + return statusCode; + } + + /** + * Check if the mechanism did send a challenge. + *

+ * Some mechanisms do not send a challenge and just rely on the correct information to authenticate a user being + * available in the request, in that case it would be normal for the mechanism to set this to false. + * + * @return true if a challenge was sent, false otherwise. + */ + public boolean isChallengeSent() { + return challengeSent; + } + + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMechanismFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMechanismFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMechanismFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import io.undertow.server.handlers.form.FormParserFactory; + +import java.util.Map; + +/** + * + * Factory for authentication mechanisms. + * + * + * + * @author Stuart Douglas + */ +public interface AuthenticationMechanismFactory { + + String REALM = "realm"; + String LOGIN_PAGE = "login_page"; + String ERROR_PAGE = "error_page"; + String CONTEXT_PATH = "context_path"; + + + /** + * Creates an authentication mechanism using the specified properties + * + * @param mechanismName The name under which this factory was registered + * @param properties The properties + * @return The mechanism + */ + AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, final Map properties); + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMode.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMode.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/AuthenticationMode.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +/** + * Enumeration to indicate the authentication mode in use. + * + * @author Darran Lofthouse + */ +public enum AuthenticationMode { + + /** + * Where the authentication mode is set to pro-active each request on arrival will be passed to the defined authentication + * mechanisms to eagerly perform authentication if there is sufficient information available in order to do so. + * + * A pro-active authentication could be possible for a number of reasons such as already having a SSL connection + * established, an identity being cached against the current session or even a browser sending in authentication headers. + * + * Running in pro-active mode the sending of the challenge to the client is still driven by the constraints defined so this + * is not the same as mandating security for all paths. For some mechanisms such as Digest this is a recommended mode as + * without it there is a risk that clients are sending in headers with unique nonce counts that go unverified risking that a + * malicious client could make use of them. This is also useful for applications that wish to make use of the current + * authenticated user if one exists without mandating that authentication occurs. + */ + PRO_ACTIVE, + + /** + * When running in constraint driven mode the authentication mechanisms are only executed where the constraint that mandates + * authentication is triggered, for all other requests no authentication occurs unless requested by the internal APIs which + * may be exposed using the Servlet APIs. + */ + CONSTRAINT_DRIVEN; + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/GSSAPIServerSubjectFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/GSSAPIServerSubjectFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/GSSAPIServerSubjectFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import java.security.GeneralSecurityException; + +import javax.security.auth.Subject; + +/** + * The GSSAPIServerSubjectFactory is a factory responsible for returning the {@link Subject} that should be used for handing the + * GSSAPI based authentication for a specific request. + * + * The authentication handlers will not perform any caching of the returned Subject, the factory implementation can either + * return a new Subject for each request or can cache them maybe based on the expiration time of tickets contained within the + * Subject. + * + * @author Darran Lofthouse + */ +public interface GSSAPIServerSubjectFactory { + + // TODO - Does this need to be supplying some kind of wrapper that allows a try/finally approach to being and end using the Subject? + + /** + * Obtain the Subject to use for the specified host. + * + * All virtual hosts on a server could use the same Subject or each virtual host could have a different Subject, the + * implementation of the factory will make that decision. The factory implementation will also decide if there should be a + * default fallback Subject or if a Subject should only be provided for recognised hosts. + * + * @param hostName - The host name used for this request. + * @return The Subject to use for the specified host name or null if no match possible. + * @throws GeneralSecurityException if there is a security failure obtaining the {@link Subject} + */ + Subject getSubjectForHost(final String hostName) throws GeneralSecurityException; + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/NonceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/NonceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/NonceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import io.undertow.server.HttpServerExchange; + +/** + * A NonceManager is used by the HTTP Digest authentication mechanism to request nonces and to validate the nonces sent from the + * client. + * + * @author Darran Lofthouse + */ +public interface NonceManager { + + // TODO - Should a nonce manager be able to tie these to a connection or session, or any other piece of info we have about + // the client? + // Also different rules depending on HTTP method or the resource being accessed? + + /** + * Select the next nonce to be sent from the server taking into account the last valid nonce. + * + * It is both possible and likely that the nonce last used by the client will still be valid, in that case the same nonce + * will be returned. + * + * @param lastNonce - The last valid nonce received from the client or null if we don't already have a nonce. + * @return The next nonce to be sent in a challenge to the client. + */ + String nextNonce(final String lastNonce, final HttpServerExchange exchange); + + /** + * Validate that a nonce can be used. + * + * If the nonce can not be used but the related digest was correct then a new nonce should be returned to the client + * indicating that the nonce was stale. + * + * For implementations of this interface this method is not expected by be idempotent, i.e. once a nonce is validated with a + * specific nonceCount it is not expected that this method will return true again if the same combination is presented. + * + * This method is expected to ONLY be called if the users credentials are valid as a storage overhead could be incurred + * this overhead must not be accessible to unauthenticated clients. + * + * @param nonce - The nonce received from the client. + * @param nonceCount - The nonce count from the client or -1 of none specified. + * @return true if the nonce can be used otherwise return false. + */ + boolean validateNonce(final String nonce, final int nonceCount, final HttpServerExchange exchange); + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/NotificationReceiver.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/NotificationReceiver.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/NotificationReceiver.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +/** + * The interface to be interested by classes interested in processing security related notifications. + * + * @author Darran Lofthouse + */ +public interface NotificationReceiver { + + /** + * Handle a security related notification. + * + * The {@link SecurityNotification} that is sent to be handled is a security event that has occurred, this is not an + * opportunity for that event to be validated - any Exception thrown by the handler will be logged but will not affect the + * result of the security event. + * + * The notifications are sent on the same thread that is currently processing the request that triggered the notification, + * if the handling of the notification is likely to be blocking then it should be dispatched to it's own worker thread. The + * one exception to this may be where the notification must be sure to have been handled before the response continues. + * + * @param notification + */ + void handleNotification(final SecurityNotification notification); + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/SecurityContext.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/SecurityContext.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/SecurityContext.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,201 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import java.util.List; + +import io.undertow.security.idm.Account; +import io.undertow.security.idm.IdentityManager; + +/** + * The security context. + * + * This context is attached to the exchange and holds all security related information. + * + * @author Stuart Douglas + * @author Darran Lofthouse + * @see io.undertow.security.impl.SecurityContextImpl + */ +public interface SecurityContext { + + // TODO - Some of this is used within the core of undertow, some by the servlet integration and some by the mechanisms - + // once released the use by mechanisms will require the greatest level of backwards compatibility maintenance so may be + // better to split the rest out. + + /* + * Methods Used To Run Authentication Process + */ + + /** + * Performs authentication on the request. + * + * If authentication is REQUIRED then setAuthenticationRequired() should be called before calling this method. + * + * If the result indicates that a response has been sent to the client then no further attempts should be made to modify the + * response. The caller of this method is responsible for ending the exchange. + * + * If this method returns true it can still have committed the response (e.g. form auth redirects back to the original + * page). Callers should check that the exchange has not been ended before proceeding. + * + * @return true if either the request is successfully authenticated or if there is no failure validating the + * current request so that the request should continue to be processed, false if authentication was not + * completed and challenge has been prepared for the client. + */ + boolean authenticate(); + + /* + * API for Direct Control of Authentication + */ + + /** + * Attempts to log the user in using the provided credentials. This result will be stored in the current + * {@link AuthenticatedSessionManager} (if any), so subsequent requests will automatically be authenticated + * as this user. + *

+ * This operation may block + * + * @param username The username + * @param password The password + * @return true if the login succeeded, false otherwise + */ + boolean login(String username, String password); + + /** + * de-authenticates the current exchange. + * + */ + void logout(); + + /* + * Methods Used To Control/Configure The Authentication Process. + */ + + // TODO - May be better to pass a parameter to the authenticate methods to indicate that authentication is required. + + + /** + * Marks this request as requiring authentication. Authentication challenge headers will only be sent if this + * method has been called. If {@link #authenticate()} + * is called without first calling this method then the request will continue as normal even if the authentication + * was not successful. + */ + void setAuthenticationRequired(); + + /** + * Returns true if authentication is required + * + * @return true If authentication is required + */ + boolean isAuthenticationRequired(); + + /** + * Adds an authentication mechanism to this context. When {@link #authenticate()} is + * called mechanisms will be iterated over in the order they are added, and given a chance to authenticate the user. + * + * @param mechanism The mechanism to add + */ + void addAuthenticationMechanism(AuthenticationMechanism mechanism); + + /** + * + * @return A list of all authentication mechanisms in this context + */ + List getAuthenticationMechanisms(); + + /* + * Methods to access information about the current authentication status. + */ + + /** + * + * @return true if a user has been authenticated for this request, false otherwise. + */ + boolean isAuthenticated(); + + + + /** + * Obtain the {@link Account} for the currently authenticated identity. + * + * @return The {@link Account} for the currently authenticated identity or null if no account is currently authenticated. + */ + Account getAuthenticatedAccount(); + + /** + * + * @return The name of the mechanism that was used to authenticate + */ + String getMechanismName(); + + /* + * Methods Used by AuthenticationMechanism implementations. + */ + + /** + * Obtain the associated {@link IdentityManager} to use to make account verification decisions. + * + * @return The associated {@link IdentityManager} + */ + IdentityManager getIdentityManager(); + + /** + * Called by the {@link AuthenticationMechanism} to indicate that an account has been successfully authenticated. + * + * Note: A successful verification of an account using the {@link IdentityManager} is not the same as a successful + * authentication decision, other factors could be taken into account to make the final decision. + * + * @param account - The authenticated {@link Account} + * @param mechanismName - The name of the mechanism used to authenticate the account. + * @param cachingRequired - If this mechanism requires caching + */ + void authenticationComplete(final Account account, final String mechanismName, final boolean cachingRequired); + + /** + * Called by the {@link AuthenticationMechanism} to indicate that an authentication attempt has failed. + * + * This should only be called where an authentication attempt has truly failed, for authentication mechanisms where an + * additional round trip with the client is expected this should not be called. + * + * Where possible the failure message should contain the name of the identity that authentication was being attempted for, + * however as this is not always possible to identify in advance a generic message may be all that can be reported. + * + * @param message - The message describing the failure. + * @param mechanismName - The name of the mechanism reporting the failure. + */ + void authenticationFailed(final String message, final String mechanismName); + + /* + * Methods for the management of NotificationHandler registrations. + */ + + /** + * Register a {@link NotificationReceiver} interested in receiving notifications for security events that happen on this SecurityContext. + * + * @param receiver - The {@link NotificationReceiver} to register. + */ + void registerNotificationReceiver(final NotificationReceiver receiver); + + /** + * Remove a previously registered {@link NotificationReceiver} from this SecurityContext. + * + * If the supplied receiver has not been previously registered this method will fail silently. + * + * @param receiver - The {@link NotificationReceiver} to remove. + */ + void removeNotificationReceiver(final NotificationReceiver receiver); +} Index: 3rdParty_sources/undertow/io/undertow/security/api/SecurityContextFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/SecurityContextFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/SecurityContextFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpServerExchange; + +/** + *

+ * Interface that must be implemented by factories of {@link io.undertow.security.api.SecurityContext} instances. + *

+ * + * @author Stefan Guilhen + */ +public interface SecurityContextFactory { + + /** + *

+ * Instantiates and returns a {@code SecurityContext} using the specified parameters. + *

+ * + * @param exchange the {@code HttpServerExchange} instance. + * @param mode the {@code AuthenticationMode}. + * @param identityManager the {@code IdentityManager} instance. + * @param programmaticMechName a {@code String} representing the programmatic mechanism name. Can be null. + * @return the constructed {@code SecurityContext} instance. + */ + SecurityContext createSecurityContext(final HttpServerExchange exchange, final AuthenticationMode mode, + final IdentityManager identityManager, final String programmaticMechName); +} Index: 3rdParty_sources/undertow/io/undertow/security/api/SecurityNotification.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/SecurityNotification.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/SecurityNotification.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,80 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +import io.undertow.security.idm.Account; +import io.undertow.server.HttpServerExchange; + +/** + * Notification representing a security event such as a successful or failed authentication. + * + * @author Darran Lofthouse + */ +public class SecurityNotification { + + private final HttpServerExchange exchange; + private final EventType eventType; + private final Account account; + private final String mechanism; + private final boolean programatic; + private final String message; + private final boolean cachingRequired; + + public SecurityNotification(final HttpServerExchange exchange, final EventType eventType, final Account account, final String mechanism, final boolean programatic, final String message, boolean cachingRequired) { + this.exchange = exchange; + this.eventType = eventType; + this.account = account; + this.mechanism = mechanism; + this.programatic = programatic; + this.message = message; + this.cachingRequired = cachingRequired; + } + + public HttpServerExchange getExchange() { + return exchange; + } + + public EventType getEventType() { + return eventType; + } + + public Account getAccount() { + return account; + } + + public String getMechanism() { + return mechanism; + } + + public boolean isProgramatic() { + return programatic; + } + + public String getMessage() { + return message; + } + + public boolean isCachingRequired() { + return cachingRequired; + } + + public enum EventType { + AUTHENTICATED, FAILED_AUTHENTICATION, LOGGED_OUT; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/api/SessionNonceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/api/SessionNonceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/api/SessionNonceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.api; + +/** + * An extension to the {@link NonceManager} interface for Nonce managers that also support the association of a pre-prepared + * hash against a currently valid nonce. + * + * If the nonce manager replaces in-use nonces as old ones expire then the associated session hash should be migrated to the + * replacement nonce. + * + * @author Darran Lofthouse + */ +public interface SessionNonceManager extends NonceManager { + + /** + * Associate the supplied hash with the nonce specified. + * + * @param nonce - The nonce the hash is to be associated with. + * @param hash - The hash to associate. + */ + void associateHash(final String nonce, final byte[] hash); + + /** + * Retrieve the existing hash associated with the nonce specified. + * + * If there is no association then null should be returned. + * + * @param nonce - The nonce the hash is required for. + * @return The associated hash or null if there is no association. + */ + byte[] lookupHash(final String nonce); + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/AbstractConfidentialityHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/AbstractConfidentialityHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/AbstractConfidentialityHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,95 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import java.net.URI; +import java.net.URISyntaxException; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; + +/** + * Handler responsible for checking of confidentiality is required for the requested resource and if so rejecting the request + * and redirecting to a secure address. + * + * @author Darran Lofthouse + */ +public abstract class AbstractConfidentialityHandler implements HttpHandler { + + private final HttpHandler next; + + protected AbstractConfidentialityHandler(final HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if (isConfidential(exchange) || !confidentialityRequired(exchange)) { + next.handleRequest(exchange); + } else { + try { + URI redirectUri = getRedirectURI(exchange); + + exchange.setResponseCode(302); + exchange.getResponseHeaders().put(Headers.LOCATION, redirectUri.toString()); + } catch (URISyntaxException e) { + UndertowLogger.REQUEST_LOGGER.exceptionProcessingRequest(e); + exchange.setResponseCode(500); + } + exchange.endExchange(); + } + } + + /** + * Use the HttpServerExchange supplied to check if this request is already 'sufficiently' confidential. + * + * Here we say 'sufficiently' as sub-classes can override this and maybe even go so far as querying the actual SSLSession. + * + * @param exchange - The {@see HttpServerExchange} for the request being processed. + * @return true if the request is 'sufficiently' confidential, false otherwise. + */ + protected boolean isConfidential(final HttpServerExchange exchange) { + return exchange.getRequestScheme().equals("https"); + } + + /** + * Use the HttpServerExchange to identify if confidentiality is required. + * + * This method currently returns true for all requests, sub-classes can override this to provide a custom check. + * + * TODO: we should deprecate this and just use a predicate to decide to execute the handler instead + * + * @param exchange - The {@see HttpServerExchange} for the request being processed. + * @return true if the request requires confidentiality, false otherwise. + */ + protected boolean confidentialityRequired(final HttpServerExchange exchange) { + return true; + } + + /** + * All sub-classes are required to provide an implementation of this method, using the HttpServerExchange for the current + * request return the address to use for a redirect should confidentiality be required and the request not be confidential. + * + * @param exchange - The {@see HttpServerExchange} for the request being processed. + * @return The {@see URI} to redirect to. + */ + protected abstract URI getRedirectURI(final HttpServerExchange exchange) throws URISyntaxException; + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationCallHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationCallHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationCallHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,59 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * This is the final {@link HttpHandler} in the security chain, it's purpose is to act as a barrier at the end of the chain to + * ensure authenticate is called after the mechanisms have been associated with the context and the constraint checked. + * + * @author Darran Lofthouse + */ +public class AuthenticationCallHandler implements HttpHandler { + + private final HttpHandler next; + + public AuthenticationCallHandler(final HttpHandler next) { + this.next = next; + } + + /** + * Only allow the request through if successfully authenticated or if authentication is not required. + * + * @see io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange) + */ + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if(exchange.isInIoThread()) { + exchange.dispatch(this); + return; + } + SecurityContext context = exchange.getSecurityContext(); + if (context.authenticate()) { + if(!exchange.isComplete()) { + next.handleRequest(exchange); + } + } else { + exchange.endExchange(); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationConstraintHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationConstraintHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationConstraintHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * Handler responsible for checking the constraints for the current request and marking authentication as required if + * applicable. + * + * Sub classes can override isAuthenticationRequired to provide a constraint check, by default this handler will set + * authentication as always required, authentication will be optional if this handler is omitted. + * + * @author Darran Lofthouse + */ +public class AuthenticationConstraintHandler implements HttpHandler { + + private final HttpHandler next; + + public AuthenticationConstraintHandler(final HttpHandler next) { + this.next = next; + } + + /** + * @see io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange) + */ + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + if (isAuthenticationRequired(exchange)) { + SecurityContext context = exchange.getSecurityContext(); + context.setAuthenticationRequired(); + } + + next.handleRequest(exchange); + } + + /** + * Evaluate the current request and indicate if authentication is required for the current request. + * + * By default this will always return true, sub-classes will override this method to provide a more specific check. + * + * @param exchange - the {@link HttpServerExchange} for the current request to decide if authentication is required. + * @return true if authentication is required, false otherwise. + */ + protected boolean isAuthenticationRequired(final HttpServerExchange exchange) { + return true; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationMechanismsHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationMechanismsHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/AuthenticationMechanismsHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,71 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import io.undertow.Handlers; +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; + +import java.util.List; + +/** + * Authentication handler that adds one or more authentication + * mechanisms to the security context + * + * @author Stuart Douglas + */ +public class AuthenticationMechanismsHandler implements HttpHandler { + + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + private final List authenticationMechanisms; + + public AuthenticationMechanismsHandler(final HttpHandler next, final List authenticationMechanisms) { + this.next = next; + this.authenticationMechanisms = authenticationMechanisms; + } + + public AuthenticationMechanismsHandler(final List authenticationHandlers) { + this.authenticationMechanisms = authenticationHandlers; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final SecurityContext sc = exchange.getSecurityContext(); + if(sc != null) { + for(AuthenticationMechanism mechanism : authenticationMechanisms) { + sc.addAuthenticationMechanism(mechanism); + } + } + next.handleRequest(exchange); + } + + public HttpHandler getNext() { + return next; + } + + public AuthenticationMechanismsHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/NotificationReceiverHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/NotificationReceiverHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/NotificationReceiverHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import java.util.Collection; + +import io.undertow.security.api.NotificationReceiver; +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * A {@link HttpHandler} to register a list of {@link NotificationReceiver} instances with the current {@link SecurityContext}. + * + * @author Darran Lofthouse + */ +public class NotificationReceiverHandler implements HttpHandler { + + private final HttpHandler next; + private final Collection receivers; + + public NotificationReceiverHandler(final HttpHandler next, final Collection receivers) { + this.next = next; + this.receivers = receivers; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + SecurityContext sc = exchange.getSecurityContext(); + for (NotificationReceiver receiver : receivers) { + sc.registerNotificationReceiver(receiver); + } + + next.handleRequest(exchange); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/SecurityActions.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/SecurityActions.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/SecurityActions.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,41 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import io.undertow.security.api.SecurityContext; +import io.undertow.server.HttpServerExchange; + +class SecurityActions { + static void setSecurityContext(final HttpServerExchange exchange, final SecurityContext securityContext) { + if (System.getSecurityManager() == null) { + exchange.setSecurityContext(securityContext); + } else { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + exchange.setSecurityContext(securityContext); + return null; + } + }); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/SecurityInitialHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/SecurityInitialHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/SecurityInitialHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,79 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import io.undertow.security.api.AuthenticationMode; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.api.SecurityContextFactory; +import io.undertow.security.idm.IdentityManager; +import io.undertow.security.impl.SecurityContextFactoryImpl; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * The security handler responsible for attaching the SecurityContext to the current {@link HttpServerExchange}. + * + * This handler is called early in the processing of the incoming request, subsequently supported authentication mechanisms will + * be added to the context, a decision will then be made if authentication is required or optional and the associated mechanisms + * will be called. + * + * In addition to the HTTPExchange authentication state can also be associated with the + * {@link io.undertow.server.protocol.http.HttpServerConnection} and with the {@link io.undertow.server.session.Session} however this is + * mechanism specific so it is down to the actual mechanisms to decide if there is state that can be re-used. + * + * @author Darran Lofthouse + */ +public class SecurityInitialHandler implements HttpHandler { + + private final AuthenticationMode authenticationMode; + private final IdentityManager identityManager; + private final HttpHandler next; + private final String programaticMechName; + private final SecurityContextFactory contextFactory; + + public SecurityInitialHandler(final AuthenticationMode authenticationMode, final IdentityManager identityManager, + final String programaticMechName, final SecurityContextFactory contextFactory, final HttpHandler next) { + this.authenticationMode = authenticationMode; + this.identityManager = identityManager; + this.programaticMechName = programaticMechName; + this.contextFactory = contextFactory; + this.next = next; + } + + public SecurityInitialHandler(final AuthenticationMode authenticationMode, final IdentityManager identityManager, + final String programaticMechName, final HttpHandler next) { + this(authenticationMode, identityManager, programaticMechName, SecurityContextFactoryImpl.INSTANCE, next); + } + + public SecurityInitialHandler(final AuthenticationMode authenticationMode, final IdentityManager identityManager, + final HttpHandler next) { + this(authenticationMode, identityManager, null, SecurityContextFactoryImpl.INSTANCE, next); + } + + /** + * @see io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange) + */ + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + SecurityContext newContext = this.contextFactory.createSecurityContext(exchange, authenticationMode, identityManager, + programaticMechName); + SecurityActions.setSecurityContext(exchange, newContext); + next.handleRequest(exchange); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/handlers/SinglePortConfidentialityHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/handlers/SinglePortConfidentialityHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/handlers/SinglePortConfidentialityHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,54 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.handlers; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * An extension to {@see AbstractConfidentialityHandler} that uses the Host header from the incoming message and specifies the + * confidential address by just switching the port. + * + * @author Darran Lofthouse + */ +public class SinglePortConfidentialityHandler extends AbstractConfidentialityHandler { + + private final int redirectPort; + + public SinglePortConfidentialityHandler(final HttpHandler next, final int redirectPort) { + super(next); + this.redirectPort = redirectPort == 443 ? -1 : redirectPort; + } + + @Override + protected URI getRedirectURI(HttpServerExchange exchange) throws URISyntaxException { + return getRedirectURI(exchange, redirectPort); + } + + protected URI getRedirectURI(HttpServerExchange exchange, int port) throws URISyntaxException { + String host = exchange.getHostName(); + + String queryString = exchange.getQueryString(); + return new URI("https", null, host, port, exchange.getRequestURI(), + queryString == null || queryString.length() == 0 ? null : queryString, null); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/Account.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/Account.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/Account.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +import java.security.Principal; +import java.util.Set; + +/** + * Representation of an account, most likely a user account. + * + * @author Darran Lofthouse + */ +public interface Account { + + Principal getPrincipal(); + + /** + * Returns the users roles. + * + * @return A set of the users roles + */ + Set getRoles(); + + // TODO - Do we need a way to pass back to IDM that account is logging out? A few scenarios: - + // 1 - Session expiration so cached account known to be logging out. + // 2 - API call to logout. + // 3 - End of HTTP request where account not cached, not strictly logging out but then again no real log-in. + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/Credential.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/Credential.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/Credential.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,27 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +/** + * Representation of a users Credential. + * + * @author Darran Lofthouse + */ +public interface Credential { + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/DigestAlgorithm.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/DigestAlgorithm.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/DigestAlgorithm.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration of the supported digest algorithms. + * + * @author Darran Lofthouse + */ +public enum DigestAlgorithm { + + MD5("MD5", "MD5", false), MD5_SESS("MD5-sess", "MD5", true); + + private static final Map BY_TOKEN; + + static { + DigestAlgorithm[] algorithms = DigestAlgorithm.values(); + + Map byToken = new HashMap<>(algorithms.length); + for (DigestAlgorithm current : algorithms) { + byToken.put(current.token, current); + } + + BY_TOKEN = Collections.unmodifiableMap(byToken); + } + + private final String token; + private final String digestAlgorithm; + private final boolean session; + + private DigestAlgorithm(final String token, final String digestAlgorithm, final boolean session) { + this.token = token; + this.digestAlgorithm = digestAlgorithm; + this.session = session; + } + + public String getToken() { + return token; + } + + public String getAlgorithm() { + return digestAlgorithm; + } + + public boolean isSession() { + return session; + } + + public MessageDigest getMessageDigest() throws NoSuchAlgorithmException { + return MessageDigest.getInstance(digestAlgorithm); + } + + public static DigestAlgorithm forName(final String name) { + return BY_TOKEN.get(name); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/DigestCredential.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/DigestCredential.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/DigestCredential.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +/** + * An extension of {@link Credential} to provide some additional methods needed to enable verification of a request where + * {@link DigestAuthenticationMechanism} is in use. + * + * @author Darran Lofthouse + */ +public interface DigestCredential extends Credential { + + /** + * Obtain the selected {@link DigestAlgorithm} for the request being authenticated. + * + * @return The {@link DigestAlgorithm} for the request being authenticated. + */ + DigestAlgorithm getAlgorithm(); + + /** + * Called by the {@link IdentityManager} implementation to pass in the hex encoded a1 representation for validation against + * the current request. + * + * The {@link Credential} is self validating based on the information passed in here, if verification is successful then the + * {@link IdentityManager} can return the appropriate {@link Account} representation. + * + * @param ha1 - The hex encoded a1 value. + * @return true if verification was successful, false otherwise. + */ + boolean verifyHA1(final byte[] ha1); + + /** + * Get the realm name the credential is being validated against. + * + * @return The realm name. + */ + String getRealm(); + + /** + * If the algorithm is session based return the session data to be included when generating the ha1. + * + * @return The session data. + * @throws IllegalStateException where the algorithm is not session based. + */ + byte[] getSessionData(); + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/ExternalCredential.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/ExternalCredential.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/ExternalCredential.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,37 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +import java.io.Serializable; + +/** + * Representation of an external credential. This basically represents a trusted + * 3rd party, e.g. a front end server that has performed authentication. + * + * @author Stuart Douglas + */ +public class ExternalCredential implements Serializable, Credential { + + public static final ExternalCredential INSTANCE = new ExternalCredential(); + + private ExternalCredential() { + + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/GSSContextCredential.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/GSSContextCredential.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/GSSContextCredential.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +import org.ietf.jgss.GSSContext; + +/** + * A {@link Credential} to wrap an established GSSContext. + * + * @author Darran Lofthouse + */ +public class GSSContextCredential implements Credential { + + private final GSSContext gssContext; + + public GSSContextCredential(final GSSContext gssContext) { + this.gssContext = gssContext; + } + + public GSSContext getGssContext() { + return gssContext; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/IdentityManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/IdentityManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/IdentityManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +/** + * The IdentityManager interface to be implemented by an identity manager implementation providing user verification and + * identity loading to Undertow. + * + * Note: The IdentityManager interface is very much work in progress, methods are added to cover use cases as identified and + * then simplified as common cases are defined. + * + * @author Darran Lofthouse + */ +public interface IdentityManager { + + /** + * Verify a previously authenticated account. + * + * Typical checks could be along the lines of verifying that the account is not now locked or that the password has not been + * reset since last verified, also this provides an opportunity for roles to be re-loaded if membership information has + * changed. + * + * @param account - The {@link Account} to verify. + * @return An updates {@link Account} if verification is successful, null otherwise. + */ + Account verify(final Account account); + + /** + * Verify a supplied {@link Credential} against a requested ID. + * + * @param id - The requested ID for the account. + * @param credential - The {@link Credential} to verify. + * @return The {@link Account} for the user if verification was successful, null otherwise. + */ + Account verify(final String id, final Credential credential); + + /** + * Perform verification when all we have is the Credential, in this case the IdentityManager is also responsible for mapping the Credential to an account. + * + * The most common scenario for this would be mapping an X509Certificate to the user it is associated with. + * + * @param credential + * @return + */ + Account verify(final Credential credential); + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/PasswordCredential.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/PasswordCredential.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/PasswordCredential.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,37 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +/** + * A Credential representing the password of an Account. + * + * @author Darran Lofthouse + */ +public final class PasswordCredential implements Credential { + + private final char[] password; + + public PasswordCredential(final char[] password) { + this.password = password; + } + + public char[] getPassword() { + return password; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/idm/X509CertificateCredential.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/idm/X509CertificateCredential.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/idm/X509CertificateCredential.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.idm; + +import java.security.cert.X509Certificate; + +/** + * A {@see Credential} implementation which wraps an X.509 certificate. + * + * @author Darran Lofthouse + */ +public final class X509CertificateCredential implements Credential { + + private final X509Certificate certificate; + + public X509CertificateCredential(final X509Certificate certificate) { + this.certificate = certificate; + } + + public X509Certificate getCertificate() { + return certificate; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/AuthenticationInfoToken.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/AuthenticationInfoToken.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/AuthenticationInfoToken.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.undertow.util.HeaderToken; +import io.undertow.util.HeaderTokenParser; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +/** + * Enumeration of tokens expected in a HTTP Digest 'Authentication-Info' header. + * + * @author Darran Lofthouse + */ +public enum AuthenticationInfoToken implements HeaderToken { + NEXT_NONCE(Headers.NEXT_NONCE, true), + MESSAGE_QOP(Headers.QOP, true), + RESPONSE_AUTH(Headers.RESPONSE_AUTH, true), + CNONCE(Headers.CNONCE, true), + NONCE_COUNT(Headers.NONCE_COUNT, false); + + private static final HeaderTokenParser TOKEN_PARSER; + + static { + Map expected = new LinkedHashMap<>( + AuthenticationInfoToken.values().length); + for (AuthenticationInfoToken current : AuthenticationInfoToken.values()) { + expected.put(current.getName(), current); + } + + TOKEN_PARSER = new HeaderTokenParser<>(Collections.unmodifiableMap(expected)); + } + + private final String name; + private final boolean quoted; + + private AuthenticationInfoToken(final HttpString name, final boolean quoted) { + this.name = name.toString(); + this.quoted = quoted; + } + + public String getName() { + return name; + } + + public boolean isAllowQuoted() { + return quoted; + } + + public static Map parseHeader(final String header) { + return TOKEN_PARSER.parseHeader(header); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/BasicAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/BasicAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/BasicAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,165 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import static io.undertow.UndertowMessages.MESSAGES; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.List; +import java.util.Map; + +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.AuthenticationMechanismFactory; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.IdentityManager; +import io.undertow.security.idm.PasswordCredential; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormParserFactory; +import io.undertow.util.FlexBase64; + +import static io.undertow.util.Headers.AUTHORIZATION; +import static io.undertow.util.Headers.BASIC; +import static io.undertow.util.Headers.WWW_AUTHENTICATE; +import static io.undertow.util.StatusCodes.UNAUTHORIZED; + +/** + * The authentication handler responsible for BASIC authentication as described by RFC2617 + * + * @author Darran Lofthouse + */ +public class BasicAuthenticationMechanism implements AuthenticationMechanism { + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + public static final String SILENT = "silent"; + + private final String name; + private final String challenge; + + private static final String BASIC_PREFIX = BASIC + " "; + private static final int PREFIX_LENGTH = BASIC_PREFIX.length(); + private static final String COLON = ":"; + + /** + * If silent is true then this mechanism will only take effect if there is an Authorization header. + * + * This allows you to combine basic auth with form auth, so human users will use form based auth, but allows + * programmatic clients to login using basic auth. + */ + private final boolean silent; + + public static final Factory FACTORY = new Factory(); + + // TODO - Can we get the realm name from the IDM? + public BasicAuthenticationMechanism(final String realmName) { + this(realmName, "BASIC"); + } + + public BasicAuthenticationMechanism(final String realmName, final String mechanismName) { + this(realmName, mechanismName, false); + } + + public BasicAuthenticationMechanism(final String realmName, final String mechanismName, final boolean silent) { + this.challenge = BASIC_PREFIX + "realm=\"" + realmName + "\""; + this.name = mechanismName; + this.silent = silent; + } + + /** + * @see io.undertow.server.HttpHandler#handleRequest(io.undertow.server.HttpServerExchange) + */ + @Override + public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { + + List authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION); + if (authHeaders != null) { + for (String current : authHeaders) { + if (current.startsWith(BASIC_PREFIX)) { + String base64Challenge = current.substring(PREFIX_LENGTH); + String plainChallenge = null; + try { + ByteBuffer decode = FlexBase64.decode(base64Challenge); + plainChallenge = new String(decode.array(), decode.arrayOffset(), decode.limit(), UTF_8); + } catch (IOException e) { + } + int colonPos; + if (plainChallenge != null && (colonPos = plainChallenge.indexOf(COLON)) > -1) { + String userName = plainChallenge.substring(0, colonPos); + char[] password = plainChallenge.substring(colonPos + 1).toCharArray(); + + IdentityManager idm = securityContext.getIdentityManager(); + PasswordCredential credential = new PasswordCredential(password); + try { + final AuthenticationMechanismOutcome result; + Account account = idm.verify(userName, credential); + if (account != null) { + securityContext.authenticationComplete(account, name, false); + result = AuthenticationMechanismOutcome.AUTHENTICATED; + } else { + securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), name); + result = AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + return result; + } finally { + clear(password); + } + } + + // By this point we had a header we should have been able to verify but for some reason + // it was not correctly structured. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + } + + // No suitable header has been found in this request, + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + + @Override + public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { + if(silent) { + //if this is silent we only send a challenge if the request contained auth headers + //otherwise we assume another method will send the challenge + String authHeader = exchange.getRequestHeaders().getFirst(AUTHORIZATION); + if(authHeader == null) { + return new ChallengeResult(false); + } + } + exchange.getResponseHeaders().add(WWW_AUTHENTICATE, challenge); + return new ChallengeResult(true, UNAUTHORIZED); + } + + private static void clear(final char[] array) { + for (int i = 0; i < array.length; i++) { + array[i] = 0x00; + } + } + + public static class Factory implements AuthenticationMechanismFactory { + + @Override + public AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, Map properties) { + String realm = properties.get(REALM); + String silent = properties.get(SILENT); + return new BasicAuthenticationMechanism(realm, mechanismName, silent != null && silent.equals("true")); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/CachedAuthenticatedSessionMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/CachedAuthenticatedSessionMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/CachedAuthenticatedSessionMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,71 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.api.AuthenticatedSessionManager; +import io.undertow.security.api.AuthenticatedSessionManager.AuthenticatedSession; +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.server.HttpServerExchange; + +/** + * An {@link AuthenticationMechanism} which uses any cached {@link AuthenticationSession}s. + * + * @author Darran Lofthouse + */ +public class CachedAuthenticatedSessionMechanism implements AuthenticationMechanism { + + @Override + public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { + AuthenticatedSessionManager sessionManager = exchange.getAttachment(AuthenticatedSessionManager.ATTACHMENT_KEY); + if (sessionManager != null) { + return runCached(exchange, securityContext, sessionManager); + } else { + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + } + + public AuthenticationMechanismOutcome runCached(final HttpServerExchange exchange, final SecurityContext securityContext, final AuthenticatedSessionManager sessionManager) { + AuthenticatedSession authSession = sessionManager.lookupSession(exchange); + if (authSession != null) { + Account account = securityContext.getIdentityManager().verify(authSession.getAccount()); + if (account != null) { + securityContext.authenticationComplete(account, authSession.getMechanism(), false); + return AuthenticationMechanismOutcome.AUTHENTICATED; + } else { + sessionManager.clearSession(exchange); + // We know we had a previously authenticated account but for some reason the IdentityManager is no longer + // accepting it, we now + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + } else { + // It is possible an AuthenticatedSessionManager could have been available even if there was no chance of it + // loading a session. + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + + } + + @Override + public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { + // This mechanism can only use what is already available and can not send a challenge of it's own. + return new ChallengeResult(false); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/ClientCertAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/ClientCertAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/ClientCertAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,140 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.AuthenticationMechanismFactory; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.Credential; +import io.undertow.security.idm.IdentityManager; +import io.undertow.security.idm.X509CertificateCredential; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.RenegotiationRequiredException; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.handlers.form.FormParserFactory; +import org.xnio.SslClientAuthMode; + +import javax.net.ssl.SSLPeerUnverifiedException; +import java.io.IOException; +import java.security.cert.Certificate; +import java.security.cert.X509Certificate; +import java.util.Map; + +/** + * The Client Cert based authentication mechanism. + *

+ * When authenticate is called the current request is checked to see if it a SSL request, this is further checked to identify if + * the client has been verified at the SSL level. + * + * @author Darran Lofthouse + */ +public class ClientCertAuthenticationMechanism implements AuthenticationMechanism { + + public static final String FORCE_RENEGOTIATION = "force_renegotiation"; + + private final String name; + /** + * If we should force a renegotiation if client certs were not supplied. true by default + */ + private final boolean forceRenegotiation; + + public static final Factory FACTORY = new Factory(); + + public ClientCertAuthenticationMechanism() { + this(true); + } + + public ClientCertAuthenticationMechanism(boolean forceRenegotiation) { + this("CLIENT_CERT", forceRenegotiation); + } + + public ClientCertAuthenticationMechanism(final String mechanismName) { + this(mechanismName, true); + } + + public ClientCertAuthenticationMechanism(final String mechanismName, boolean forceRenegotiation) { + this.name = mechanismName; + this.forceRenegotiation = forceRenegotiation; + } + + public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, final SecurityContext securityContext) { + SSLSessionInfo sslSession = exchange.getConnection().getSslSessionInfo(); + if (sslSession != null) { + try { + Certificate[] clientCerts = getPeerCertificates(exchange, sslSession, securityContext); + if (clientCerts[0] instanceof X509Certificate) { + Credential credential = new X509CertificateCredential((X509Certificate) clientCerts[0]); + + IdentityManager idm = securityContext.getIdentityManager(); + Account account = idm.verify(credential); + if (account != null) { + securityContext.authenticationComplete(account, name, false); + return AuthenticationMechanismOutcome.AUTHENTICATED; + } + } + } catch (SSLPeerUnverifiedException e) { + // No action - this mechanism can not attempt authentication without peer certificates so allow it to drop out + // to NOT_ATTEMPTED. + } + } + + /* + * For ClientCert we do not have a concept of a failed authentication, if the client did use a key then it was deemed + * acceptable for the connection to be established, this mechanism then just 'attempts' to use it for authentication but + * does not mandate success. + */ + + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + + private Certificate[] getPeerCertificates(final HttpServerExchange exchange, SSLSessionInfo sslSession, SecurityContext securityContext) throws SSLPeerUnverifiedException { + try { + return sslSession.getPeerCertificates(); + } catch (RenegotiationRequiredException e) { + //we only renegotiate if authentication is required + if (forceRenegotiation && securityContext.isAuthenticationRequired()) { + try { + sslSession.renegotiate(exchange, SslClientAuthMode.REQUESTED); + return sslSession.getPeerCertificates(); + + } catch (IOException e1) { + //ignore + } catch (RenegotiationRequiredException e1) { + //ignore + } + } + } + throw new SSLPeerUnverifiedException(""); + } + + @Override + public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { + return new ChallengeResult(false); + } + + private static final class Factory implements AuthenticationMechanismFactory { + + @Override + public AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, Map properties) { + String forceRenegotiation = properties.get(FORCE_RENEGOTIATION); + return new ClientCertAuthenticationMechanism(mechanismName, forceRenegotiation == null ? true : "true".equals(forceRenegotiation)); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/DigestAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/DigestAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/DigestAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,638 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import static io.undertow.UndertowLogger.REQUEST_LOGGER; +import static io.undertow.UndertowMessages.MESSAGES; +import static io.undertow.security.impl.DigestAuthorizationToken.parseHeader; +import static io.undertow.util.Headers.AUTHENTICATION_INFO; +import static io.undertow.util.Headers.AUTHORIZATION; +import static io.undertow.util.Headers.DIGEST; +import static io.undertow.util.Headers.NEXT_NONCE; +import static io.undertow.util.Headers.WWW_AUTHENTICATE; +import static io.undertow.util.StatusCodes.UNAUTHORIZED; +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.AuthenticationMechanismFactory; +import io.undertow.security.api.NonceManager; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.DigestAlgorithm; +import io.undertow.security.idm.DigestCredential; +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormParserFactory; +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.HexConverter; + +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * {@link io.undertow.server.HttpHandler} to handle HTTP Digest authentication, both according to RFC-2617 and draft update to allow additional + * algorithms to be used. + * + * @author Darran Lofthouse + */ +public class DigestAuthenticationMechanism implements AuthenticationMechanism { + + private static final String DEFAULT_NAME = "DIGEST"; + private final String mechanismName; + private static final String DIGEST_PREFIX = DIGEST + " "; + private static final int PREFIX_LENGTH = DIGEST_PREFIX.length(); + private static final String OPAQUE_VALUE = "00000000000000000000000000000000"; + private static final byte COLON = ':'; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + public static final Factory FACTORY = new Factory(); + + private static final Set MANDATORY_REQUEST_TOKENS; + + static { + Set mandatoryTokens = new HashSet<>(); + mandatoryTokens.add(DigestAuthorizationToken.USERNAME); + mandatoryTokens.add(DigestAuthorizationToken.REALM); + mandatoryTokens.add(DigestAuthorizationToken.NONCE); + mandatoryTokens.add(DigestAuthorizationToken.DIGEST_URI); + mandatoryTokens.add(DigestAuthorizationToken.RESPONSE); + + MANDATORY_REQUEST_TOKENS = Collections.unmodifiableSet(mandatoryTokens); + } + + /** + * The {@link List} of supported algorithms, this is assumed to be in priority order. + */ + private final List supportedAlgorithms; + private final List supportedQops; + private final String qopString; + private final String realmName; // TODO - Will offer choice once backing store API/SPI is in. + private final String domain; + private final NonceManager nonceManager; + + // Where do session keys fit? Do we just hang onto a session key or keep visiting the user store to check if the password + // has changed? + // Maybe even support registration of a session so it can be invalidated? + // 2013-05-29 - Session keys will be cached, where a cached key is used the IdentityManager is still given the + // opportunity to check the Account is still valid. + + public DigestAuthenticationMechanism(final List supportedAlgorithms, final List supportedQops, + final String realmName, final String domain, final NonceManager nonceManager) { + this(supportedAlgorithms, supportedQops, realmName, domain, nonceManager, DEFAULT_NAME); + } + + public DigestAuthenticationMechanism(final List supportedAlgorithms, final List supportedQops, + final String realmName, final String domain, final NonceManager nonceManager, final String mechanismName) { + this.supportedAlgorithms = supportedAlgorithms; + this.supportedQops = supportedQops; + this.realmName = realmName; + this.domain = domain; + this.nonceManager = nonceManager; + this.mechanismName = mechanismName; + + if (!supportedQops.isEmpty()) { + StringBuilder sb = new StringBuilder(); + Iterator it = supportedQops.iterator(); + sb.append(it.next().getToken()); + while (it.hasNext()) { + sb.append(",").append(it.next().getToken()); + } + qopString = sb.toString(); + } else { + qopString = null; + } + } + + public DigestAuthenticationMechanism(final String realmName, final String domain, final String mechanismName) { + this(Collections.singletonList(DigestAlgorithm.MD5), new ArrayList(0), realmName, domain, + new SimpleNonceManager()); + } + + public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, + final SecurityContext securityContext) { + List authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION); + if (authHeaders != null) { + for (String current : authHeaders) { + if (current.startsWith(DIGEST_PREFIX)) { + String digestChallenge = current.substring(PREFIX_LENGTH); + + try { + DigestContext context = new DigestContext(); + Map parsedHeader = parseHeader(digestChallenge); + context.setMethod(exchange.getRequestMethod().toString()); + context.setParsedHeader(parsedHeader); + // Some form of Digest authentication is going to occur so get the DigestContext set on the exchange. + exchange.putAttachment(DigestContext.ATTACHMENT_KEY, context); + + return handleDigestHeader(exchange, securityContext); + } catch (Exception e) { + e.printStackTrace(); + } + } + + // By this point we had a header we should have been able to verify but for some reason + // it was not correctly structured. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + + // No suitable header has been found in this request, + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + + public AuthenticationMechanismOutcome handleDigestHeader(HttpServerExchange exchange, final SecurityContext securityContext) { + DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY); + Map parsedHeader = context.getParsedHeader(); + // Step 1 - Verify the set of tokens received to ensure valid values. + Set mandatoryTokens = new HashSet<>(MANDATORY_REQUEST_TOKENS); + if (!supportedAlgorithms.contains(DigestAlgorithm.MD5)) { + // If we don't support MD5 then the client must choose an algorithm as we can not fall back to MD5. + mandatoryTokens.add(DigestAuthorizationToken.ALGORITHM); + } + if (!supportedQops.isEmpty() && !supportedQops.contains(DigestQop.AUTH)) { + // If we do not support auth then we are mandating auth-int so force the client to send a QOP + mandatoryTokens.add(DigestAuthorizationToken.MESSAGE_QOP); + } + + DigestQop qop = null; + // This check is early as is increases the list of mandatory tokens. + if (parsedHeader.containsKey(DigestAuthorizationToken.MESSAGE_QOP)) { + qop = DigestQop.forName(parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP)); + if (qop == null || !supportedQops.contains(qop)) { + // We are also ensuring the client is not trying to force a qop that has been disabled. + REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.MESSAGE_QOP.getName(), + parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP)); + // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + context.setQop(qop); + mandatoryTokens.add(DigestAuthorizationToken.CNONCE); + mandatoryTokens.add(DigestAuthorizationToken.NONCE_COUNT); + } + + // Check all mandatory tokens are present. + mandatoryTokens.removeAll(parsedHeader.keySet()); + if (mandatoryTokens.size() > 0) { + for (DigestAuthorizationToken currentToken : mandatoryTokens) { + // TODO - Need a better check and possible concatenate the list of tokens - however + // even having one missing token is not something we should routinely expect. + REQUEST_LOGGER.missingAuthorizationToken(currentToken.getName()); + } + // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + // Perform some validation of the remaining tokens. + if (!realmName.equals(parsedHeader.get(DigestAuthorizationToken.REALM))) { + REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.REALM.getName(), + parsedHeader.get(DigestAuthorizationToken.REALM)); + // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + // TODO - Validate the URI + + if (parsedHeader.containsKey(DigestAuthorizationToken.OPAQUE)) { + if (!OPAQUE_VALUE.equals(parsedHeader.get(DigestAuthorizationToken.OPAQUE))) { + REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.OPAQUE.getName(), + parsedHeader.get(DigestAuthorizationToken.OPAQUE)); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + + DigestAlgorithm algorithm; + if (parsedHeader.containsKey(DigestAuthorizationToken.ALGORITHM)) { + algorithm = DigestAlgorithm.forName(parsedHeader.get(DigestAuthorizationToken.ALGORITHM)); + if (algorithm == null || !supportedAlgorithms.contains(algorithm)) { + // We are also ensuring the client is not trying to force an algorithm that has been disabled. + REQUEST_LOGGER.invalidTokenReceived(DigestAuthorizationToken.ALGORITHM.getName(), + parsedHeader.get(DigestAuthorizationToken.ALGORITHM)); + // TODO - This actually needs to result in a HTTP 400 Bad Request response and not a new challenge. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } else { + // We know this is safe as the algorithm token was made mandatory + // if MD5 is not supported. + algorithm = DigestAlgorithm.MD5; + } + + try { + context.setAlgorithm(algorithm); + } catch (NoSuchAlgorithmException e) { + /* + * This should not be possible in a properly configured installation. + */ + REQUEST_LOGGER.exceptionProcessingRequest(e); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + final String userName = parsedHeader.get(DigestAuthorizationToken.USERNAME); + final IdentityManager identityManager = securityContext.getIdentityManager(); + final Account account; + + if (algorithm.isSession()) { + /* This can follow one of the following: - + * 1 - New session so use DigestCredentialImpl with the IdentityManager to + * create a new session key. + * 2 - Obtain the existing session key from the session store and validate it, just use + * IdentityManager to validate account is still active and the current role assignment. + */ + throw new IllegalStateException("Not yet implemented."); + } else { + final DigestCredential credential = new DigestCredentialImpl(context); + account = identityManager.verify(userName, credential); + } + + if (account == null) { + // Authentication has failed, this could either be caused by the user not-existing or it + // could be caused due to an invalid hash. + securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), mechanismName); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + // Step 3 - Verify that the nonce was eligible to be used. + if (!validateNonceUse(context, parsedHeader, exchange)) { + // TODO - This is the right place to make use of the decision but the check needs to be much much sooner + // otherwise a failure server + // side could leave a packet that could be 're-played' after the failed auth. + // The username and password verification passed but for some reason we do not like the nonce. + context.markStale(); + // We do not mark as a failure on the security context as this is not quite a failure, a client with a cached nonce + // can easily hit this point. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + // We have authenticated the remote user. + + sendAuthenticationInfoHeader(exchange); + securityContext.authenticationComplete(account, mechanismName, false); + return AuthenticationMechanismOutcome.AUTHENTICATED; + + // Step 4 - Set up any QOP related requirements. + + // TODO - Do QOP + } + + private boolean validateRequest(final DigestContext context, final byte[] ha1) { + byte[] ha2; + DigestQop qop = context.getQop(); + // Step 2.2 Calculate H(A2) + if (qop == null || qop.equals(DigestQop.AUTH)) { + ha2 = createHA2Auth(context, context.getParsedHeader()); + } else { + ha2 = createHA2AuthInt(); + } + + byte[] requestDigest; + if (qop == null) { + requestDigest = createRFC2069RequestDigest(ha1, ha2, context); + } else { + requestDigest = createRFC2617RequestDigest(ha1, ha2, context); + } + + byte[] providedResponse = context.getParsedHeader().get(DigestAuthorizationToken.RESPONSE).getBytes(UTF_8); + + return MessageDigest.isEqual(requestDigest, providedResponse); + } + + private boolean validateNonceUse(DigestContext context, Map parsedHeader, final HttpServerExchange exchange) { + String suppliedNonce = parsedHeader.get(DigestAuthorizationToken.NONCE); + int nonceCount = -1; + if (parsedHeader.containsKey(DigestAuthorizationToken.NONCE_COUNT)) { + String nonceCountHex = parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT); + + nonceCount = Integer.parseInt(nonceCountHex, 16); + } + + context.setNonce(suppliedNonce); + // TODO - A replay attempt will need an exception. + return (nonceManager.validateNonce(suppliedNonce, nonceCount, exchange)); + } + + private byte[] createHA2Auth(final DigestContext context, Map parsedHeader) { + byte[] method = context.getMethod().getBytes(UTF_8); + byte[] digestUri = parsedHeader.get(DigestAuthorizationToken.DIGEST_URI).getBytes(UTF_8); + + MessageDigest digest = context.getDigest(); + try { + digest.update(method); + digest.update(COLON); + digest.update(digestUri); + + return HexConverter.convertToHexBytes(digest.digest()); + } finally { + digest.reset(); + } + } + + private byte[] createHA2AuthInt() { + // TODO - Implement method. + throw new IllegalStateException("Method not implemented."); + } + + private byte[] createRFC2069RequestDigest(final byte[] ha1, final byte[] ha2, final DigestContext context) { + final MessageDigest digest = context.getDigest(); + final Map parsedHeader = context.getParsedHeader(); + + byte[] nonce = parsedHeader.get(DigestAuthorizationToken.NONCE).getBytes(UTF_8); + + try { + digest.update(ha1); + digest.update(COLON); + digest.update(nonce); + digest.update(COLON); + digest.update(ha2); + + return HexConverter.convertToHexBytes(digest.digest()); + } finally { + digest.reset(); + } + } + + private byte[] createRFC2617RequestDigest(final byte[] ha1, final byte[] ha2, final DigestContext context) { + final MessageDigest digest = context.getDigest(); + final Map parsedHeader = context.getParsedHeader(); + + byte[] nonce = parsedHeader.get(DigestAuthorizationToken.NONCE).getBytes(UTF_8); + byte[] nonceCount = parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT).getBytes(UTF_8); + byte[] cnonce = parsedHeader.get(DigestAuthorizationToken.CNONCE).getBytes(UTF_8); + byte[] qop = parsedHeader.get(DigestAuthorizationToken.MESSAGE_QOP).getBytes(UTF_8); + + try { + digest.update(ha1); + digest.update(COLON); + digest.update(nonce); + digest.update(COLON); + digest.update(nonceCount); + digest.update(COLON); + digest.update(cnonce); + digest.update(COLON); + digest.update(qop); + digest.update(COLON); + digest.update(ha2); + + return HexConverter.convertToHexBytes(digest.digest()); + } finally { + digest.reset(); + } + } + + @Override + public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) { + DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY); + boolean stale = context == null ? false : context.isStale(); + + StringBuilder rb = new StringBuilder(DIGEST_PREFIX); + rb.append(Headers.REALM.toString()).append("=\"").append(realmName).append("\","); + rb.append(Headers.DOMAIN.toString()).append("=\"").append(domain).append("\","); + // based on security constraints. + rb.append(Headers.NONCE.toString()).append("=\"").append(nonceManager.nextNonce(null, exchange)).append("\","); + // Not currently using OPAQUE as it offers no integrity, used for session data leaves it vulnerable to + // session fixation type issues as well. + rb.append(Headers.OPAQUE.toString()).append("=\"00000000000000000000000000000000\""); + if (stale) { + rb.append(",stale=true"); + } + if (supportedAlgorithms.size() > 0) { + // This header will need to be repeated once for each algorithm. + rb.append(",").append(Headers.ALGORITHM.toString()).append("=%s"); + } + if (qopString != null) { + rb.append(",").append(Headers.QOP.toString()).append("=\"").append(qopString).append("\""); + } + + String theChallenge = rb.toString(); + HeaderMap responseHeader = exchange.getResponseHeaders(); + if (supportedAlgorithms.isEmpty()) { + responseHeader.add(WWW_AUTHENTICATE, theChallenge); + } else { + for (DigestAlgorithm current : supportedAlgorithms) { + responseHeader.add(WWW_AUTHENTICATE, String.format(theChallenge, current.getToken())); + } + } + + return new ChallengeResult(true, UNAUTHORIZED); + } + + public void sendAuthenticationInfoHeader(final HttpServerExchange exchange) { + DigestContext context = exchange.getAttachment(DigestContext.ATTACHMENT_KEY); + DigestQop qop = context.getQop(); + String currentNonce = context.getNonce(); + String nextNonce = nonceManager.nextNonce(currentNonce, exchange); + if (qop != null || !nextNonce.equals(currentNonce)) { + StringBuilder sb = new StringBuilder(); + sb.append(NEXT_NONCE).append("=\"").append(nextNonce).append("\""); + if (qop != null) { + Map parsedHeader = context.getParsedHeader(); + sb.append(",").append(Headers.QOP.toString()).append("=\"").append(qop.getToken()).append("\""); + byte[] ha1 = context.getHa1(); + byte[] ha2; + + if (qop == DigestQop.AUTH) { + ha2 = createHA2Auth(context); + } else { + ha2 = createHA2AuthInt(); + } + String rspauth = new String(createRFC2617RequestDigest(ha1, ha2, context), UTF_8); + sb.append(",").append(Headers.RESPONSE_AUTH.toString()).append("=\"").append(rspauth).append("\""); + sb.append(",").append(Headers.CNONCE.toString()).append("=\"").append(parsedHeader.get(DigestAuthorizationToken.CNONCE)).append("\""); + sb.append(",").append(Headers.NONCE_COUNT.toString()).append("=").append(parsedHeader.get(DigestAuthorizationToken.NONCE_COUNT)); + } + + HeaderMap responseHeader = exchange.getResponseHeaders(); + responseHeader.add(AUTHENTICATION_INFO, sb.toString()); + } + + exchange.removeAttachment(DigestContext.ATTACHMENT_KEY); + } + + private byte[] createHA2Auth(final DigestContext context) { + byte[] digestUri = context.getParsedHeader().get(DigestAuthorizationToken.DIGEST_URI).getBytes(UTF_8); + + MessageDigest digest = context.getDigest(); + try { + digest.update(COLON); + digest.update(digestUri); + + return HexConverter.convertToHexBytes(digest.digest()); + } finally { + digest.reset(); + } + } + + private static class DigestContext { + + static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(DigestContext.class); + + private String method; + private String nonce; + private DigestQop qop; + private byte[] ha1; + private DigestAlgorithm algorithm; + private MessageDigest digest; + private boolean stale = false; + Map parsedHeader; + + String getMethod() { + return method; + } + + void setMethod(String method) { + this.method = method; + } + + boolean isStale() { + return stale; + } + + void markStale() { + this.stale = true; + } + + String getNonce() { + return nonce; + } + + void setNonce(String nonce) { + this.nonce = nonce; + } + + DigestQop getQop() { + return qop; + } + + void setQop(DigestQop qop) { + this.qop = qop; + } + + byte[] getHa1() { + return ha1; + } + + void setHa1(byte[] ha1) { + this.ha1 = ha1; + } + + DigestAlgorithm getAlgorithm() { + return algorithm; + } + + void setAlgorithm(DigestAlgorithm algorithm) throws NoSuchAlgorithmException { + this.algorithm = algorithm; + digest = algorithm.getMessageDigest(); + } + + MessageDigest getDigest() { + return digest; + } + + Map getParsedHeader() { + return parsedHeader; + } + + void setParsedHeader(Map parsedHeader) { + this.parsedHeader = parsedHeader; + } + + } + + private class DigestCredentialImpl implements DigestCredential { + + private final DigestContext context; + + private DigestCredentialImpl(final DigestContext digestContext) { + this.context = digestContext; + } + + @Override + public DigestAlgorithm getAlgorithm() { + return context.getAlgorithm(); + } + + @Override + public boolean verifyHA1(byte[] ha1) { + context.setHa1(ha1); // Cache for subsequent use. + + return validateRequest(context, ha1); + } + + @Override + public String getRealm() { + return realmName; + } + + @Override + public byte[] getSessionData() { + if (!context.getAlgorithm().isSession()) { + throw MESSAGES.noSessionData(); + } + + byte[] nonce = context.getParsedHeader().get(DigestAuthorizationToken.NONCE).getBytes(UTF_8); + byte[] cnonce = context.getParsedHeader().get(DigestAuthorizationToken.CNONCE).getBytes(UTF_8); + + byte[] response = new byte[nonce.length + cnonce.length + 1]; + System.arraycopy(nonce, 0, response, 0, nonce.length); + response[nonce.length] = ':'; + System.arraycopy(cnonce, 0, response, nonce.length + 1, cnonce.length); + + return response; + } + + } + + private class AuthenticationException extends Exception { + + private static final long serialVersionUID = 4123187263595319747L; + + // TODO - Remove unused constructors and maybe even move exception to higher level. + + public AuthenticationException() { + super(); + } + + public AuthenticationException(String message, Throwable cause) { + super(message, cause); + } + + public AuthenticationException(String message) { + super(message); + } + + public AuthenticationException(Throwable cause) { + super(cause); + } + + } + + private static final class Factory implements AuthenticationMechanismFactory { + + @Override + public AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, Map properties) { + return new DigestAuthenticationMechanism(properties.get(REALM), properties.get(CONTEXT_PATH), mechanismName); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/DigestAuthorizationToken.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/DigestAuthorizationToken.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/DigestAuthorizationToken.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.util.HeaderToken; +import io.undertow.util.HeaderTokenParser; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Enumeration of tokens expected in a HTTP Digest 'Authorization' header. + * + * @author Darran Lofthouse + */ +public enum DigestAuthorizationToken implements HeaderToken { + + USERNAME(Headers.USERNAME, true), + REALM(Headers.REALM, true), + NONCE(Headers.NONCE, true), + DIGEST_URI(Headers.URI, true), + RESPONSE(Headers.RESPONSE, true), + ALGORITHM(Headers.ALGORITHM, true), + CNONCE(Headers.CNONCE, true), + OPAQUE(Headers.OPAQUE, true), + MESSAGE_QOP(Headers.QOP, true), + NONCE_COUNT(Headers.NONCE_COUNT, false), + AUTH_PARAM(Headers.AUTH_PARAM, false); + + private static final HeaderTokenParser TOKEN_PARSER; + + static { + Map expected = new LinkedHashMap<>( + DigestAuthorizationToken.values().length); + for (DigestAuthorizationToken current : DigestAuthorizationToken.values()) { + expected.put(current.getName(), current); + } + + TOKEN_PARSER = new HeaderTokenParser<>(Collections.unmodifiableMap(expected)); + } + + private final String name; + private final boolean quoted; + + private DigestAuthorizationToken(final HttpString name, final boolean quoted) { + this.name = name.toString(); + this.quoted = quoted; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isAllowQuoted() { + return quoted; + } + + public static Map parseHeader(final String header) { + return TOKEN_PARSER.parseHeader(header); + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/DigestQop.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/DigestQop.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/DigestQop.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Enumeration to represent the Digest quality of protection options. + * + * @author Darran Lofthouse + */ +public enum DigestQop { + + AUTH("auth", false), AUTH_INT("auth-int", true); + + private static final Map BY_TOKEN; + + static { + DigestQop[] qops = DigestQop.values(); + + Map byToken = new HashMap<>(qops.length); + for (DigestQop current : qops) { + byToken.put(current.token, current); + } + + BY_TOKEN = Collections.unmodifiableMap(byToken); + } + + private final String token; + private final boolean integrity; + + private DigestQop(final String token, final boolean integrity) { + this.token = token; + this.integrity = integrity; + } + + public String getToken() { + return token; + } + + public boolean isMessageIntegrity() { + return integrity; + } + + public static DigestQop forName(final String name) { + return BY_TOKEN.get(name); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/DigestWWWAuthenticateToken.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/DigestWWWAuthenticateToken.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/DigestWWWAuthenticateToken.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,77 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.undertow.util.HeaderToken; +import io.undertow.util.HeaderTokenParser; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +/** + * Enumeration of tokens expected in a HTTP Digest 'WWW_Authenticate' header. + * + * @author Darran Lofthouse + */ +public enum DigestWWWAuthenticateToken implements HeaderToken { + + REALM(Headers.REALM, true), + DOMAIN(Headers.DOMAIN, true), + NONCE(Headers.NONCE, true), + OPAQUE(Headers.OPAQUE, true), + STALE(Headers.STALE, false), + ALGORITHM(Headers.ALGORITHM, false), + MESSAGE_QOP(Headers.QOP, true), + AUTH_PARAM(Headers.AUTH_PARAM, false); + + private static final HeaderTokenParser TOKEN_PARSER; + + static { + Map expected = new LinkedHashMap<>( + DigestWWWAuthenticateToken.values().length); + for (DigestWWWAuthenticateToken current : DigestWWWAuthenticateToken.values()) { + expected.put(current.getName(), current); + } + + TOKEN_PARSER = new HeaderTokenParser<>(Collections.unmodifiableMap(expected)); + } + + private final String name; + private final boolean quoted; + + private DigestWWWAuthenticateToken(final HttpString name, final boolean quoted) { + this.name = name.toString(); + this.quoted = quoted; + } + + public String getName() { + return name; + } + + public boolean isAllowQuoted() { + return quoted; + } + + public static Map parseHeader(final String header) { + return TOKEN_PARSER.parseHeader(header); + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/ExternalAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/ExternalAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/ExternalAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,90 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.AuthenticationMechanismFactory; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.ExternalCredential; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormParserFactory; +import io.undertow.util.AttachmentKey; + +import java.util.Map; + +/** + * + * Authentication mechanism that uses an externally provided principal. + * + * WARNING: This method performs no verification. It must only be used if there is no + * way for an end user to modify the principal, for example if Undertow is behind a + * front end server that is responsible for authentication. + * + * @author Stuart Douglas + */ +public class ExternalAuthenticationMechanism implements AuthenticationMechanism { + + + public static final Factory FACTORY = new Factory(); + public static final String NAME = "EXTERNAL"; + + private final String name; + + public static final AttachmentKey EXTERNAL_PRINCIPAL = AttachmentKey.create(String.class); + public static final AttachmentKey EXTERNAL_AUTHENTICATION_TYPE = AttachmentKey.create(String.class); + + public ExternalAuthenticationMechanism(String name) { + this.name = name; + } + public ExternalAuthenticationMechanism() { + this(NAME); + } + + @Override + public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { + String principal = exchange.getAttachment(EXTERNAL_PRINCIPAL); + if(principal == null) { + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + Account account = securityContext.getIdentityManager().verify(principal, ExternalCredential.INSTANCE); + if(account == null) { + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + String name = exchange.getAttachment(EXTERNAL_AUTHENTICATION_TYPE); + securityContext.authenticationComplete(account, name != null ? name: this.name, false); + + return AuthenticationMechanismOutcome.AUTHENTICATED; + } + + @Override + public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { + return new ChallengeResult(false); + } + + public static final class Factory implements AuthenticationMechanismFactory { + + private Factory() {} + + @Override + public AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, Map properties) { + return new ExternalAuthenticationMechanism(mechanismName); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/FormAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/FormAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/FormAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,179 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.UndertowLogger; +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.IdentityManager; +import io.undertow.security.idm.PasswordCredential; +import io.undertow.server.DefaultResponseListener; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormData; +import io.undertow.server.handlers.form.FormDataParser; +import io.undertow.server.handlers.form.FormParserFactory; +import io.undertow.server.session.Session; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import io.undertow.util.RedirectBuilder; +import io.undertow.util.Sessions; + +import java.io.IOException; + +import static io.undertow.UndertowMessages.MESSAGES; +import static io.undertow.util.StatusCodes.FOUND; +import static io.undertow.util.StatusCodes.TEMPORARY_REDIRECT; + +/** + * @author Stuart Douglas + */ +public class FormAuthenticationMechanism implements AuthenticationMechanism { + + public static final String LOCATION_ATTRIBUTE = FormAuthenticationMechanism.class.getName() + ".LOCATION"; + + public static final String DEFAULT_POST_LOCATION = "/j_security_check"; + + private final String name; + private final String loginPage; + private final String errorPage; + private final String postLocation; + private final FormParserFactory formParserFactory; + + public FormAuthenticationMechanism(final String name, final String loginPage, final String errorPage) { + this(FormParserFactory.builder().build(), name, loginPage, errorPage); + } + + public FormAuthenticationMechanism(final String name, final String loginPage, final String errorPage, final String postLocation) { + this(FormParserFactory.builder().build(), name, loginPage, errorPage, postLocation); + } + + public FormAuthenticationMechanism(final FormParserFactory formParserFactory, final String name, final String loginPage, final String errorPage) { + this(formParserFactory, name, loginPage, errorPage, DEFAULT_POST_LOCATION); + } + + public FormAuthenticationMechanism(final FormParserFactory formParserFactory, final String name, final String loginPage, final String errorPage, final String postLocation) { + this.name = name; + this.loginPage = loginPage; + this.errorPage = errorPage; + this.postLocation = postLocation; + this.formParserFactory = formParserFactory; + } + + @Override + public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, + final SecurityContext securityContext) { + if (exchange.getRequestURI().endsWith(postLocation) && exchange.getRequestMethod().equals(Methods.POST)) { + return runFormAuth(exchange, securityContext); + } else { + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + } + + public AuthenticationMechanismOutcome runFormAuth(final HttpServerExchange exchange, final SecurityContext securityContext) { + final FormDataParser parser = formParserFactory.createParser(exchange); + if (parser == null) { + UndertowLogger.REQUEST_LOGGER.debug("Could not authenticate as no form parser is present"); + // TODO - May need a better error signaling mechanism here to prevent repeated attempts. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + + try { + final FormData data = parser.parseBlocking(); + final FormData.FormValue jUsername = data.getFirst("j_username"); + final FormData.FormValue jPassword = data.getFirst("j_password"); + if (jUsername == null || jPassword == null) { + UndertowLogger.REQUEST_LOGGER.debug("Could not authenticate as username or password was not present in the posted result"); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + final String userName = jUsername.getValue(); + final String password = jPassword.getValue(); + AuthenticationMechanismOutcome outcome = null; + PasswordCredential credential = new PasswordCredential(password.toCharArray()); + try { + IdentityManager identityManager = securityContext.getIdentityManager(); + Account account = identityManager.verify(userName, credential); + if (account != null) { + securityContext.authenticationComplete(account, name, true); + outcome = AuthenticationMechanismOutcome.AUTHENTICATED; + } else { + securityContext.authenticationFailed(MESSAGES.authenticationFailed(userName), name); + } + } finally { + if (outcome == AuthenticationMechanismOutcome.AUTHENTICATED) { + handleRedirectBack(exchange); + exchange.endExchange(); + } + return outcome != null ? outcome : AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + protected void handleRedirectBack(final HttpServerExchange exchange) { + final Session session = Sessions.getSession(exchange); + if (session != null) { + final String location = (String) session.removeAttribute(LOCATION_ATTRIBUTE); + if(location != null) { + exchange.addDefaultResponseListener(new DefaultResponseListener() { + @Override + public boolean handleDefaultResponse(final HttpServerExchange exchange) { + FormAuthenticationMechanism.sendRedirect(exchange, location); + exchange.setResponseCode(FOUND); + exchange.endExchange(); + return true; + } + }); + } + + } + } + + public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) { + if (exchange.getRequestURI().endsWith(postLocation) && exchange.getRequestMethod().equals(Methods.POST)) { + // This method would no longer be called if authentication had already occurred. + Integer code = servePage(exchange, errorPage); + return new ChallengeResult(true, code); + } else { + // we need to store the URL + storeInitialLocation(exchange); + // TODO - Rather than redirecting, in order to make this mechanism compatible with the other mechanisms we need to + // return the actual error page not a redirect. + Integer code = servePage(exchange, loginPage); + return new ChallengeResult(true, code); + } + } + + protected void storeInitialLocation(final HttpServerExchange exchange) { + Session session = Sessions.getOrCreateSession(exchange); + session.setAttribute(LOCATION_ATTRIBUTE, RedirectBuilder.redirect(exchange, exchange.getRelativePath())); + } + + protected Integer servePage(final HttpServerExchange exchange, final String location) { + sendRedirect(exchange, location); + return TEMPORARY_REDIRECT; + } + + + static void sendRedirect(final HttpServerExchange exchange, final String location) { + // TODO - String concatenation to construct URLS is extremely error prone - switch to a URI which will better handle this. + String loc = exchange.getRequestScheme() + "://" + exchange.getHostAndPort() + location; + exchange.getResponseHeaders().put(Headers.LOCATION, loc); + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/GSSAPIAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/GSSAPIAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/GSSAPIAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,287 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.GeneralSecurityException; +import java.security.Principal; +import java.security.PrivilegedActionException; +import java.security.PrivilegedExceptionAction; +import java.util.List; + +import javax.security.auth.Subject; +import javax.security.auth.kerberos.KerberosPrincipal; + +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.GSSAPIServerSubjectFactory; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.GSSContextCredential; +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.ServerConnection; +import io.undertow.server.handlers.proxy.ExclusivityChecker; +import io.undertow.util.AttachmentKey; +import io.undertow.util.FlexBase64; +import org.ietf.jgss.GSSContext; +import org.ietf.jgss.GSSCredential; +import org.ietf.jgss.GSSException; +import org.ietf.jgss.GSSManager; + +import static io.undertow.util.Headers.AUTHORIZATION; +import static io.undertow.util.Headers.HOST; +import static io.undertow.util.Headers.NEGOTIATE; +import static io.undertow.util.Headers.WWW_AUTHENTICATE; +import static io.undertow.util.StatusCodes.UNAUTHORIZED; + +/** + * {@link io.undertow.security.api.AuthenticationMechanism} for GSSAPI / SPNEGO based authentication. + *

+ * GSSAPI authentication is associated with the HTTP connection, as long as a connection is being re-used allow the + * authentication state to be re-used. + *

+ * TODO - May consider an option to allow it to also be associated with the underlying session but that has it's own risks so + * would need to come with a warning. + * + * @author Darran Lofthouse + */ +public class GSSAPIAuthenticationMechanism implements AuthenticationMechanism { + + public static final ExclusivityChecker EXCLUSIVITY_CHECKER = new ExclusivityChecker() { + + @Override + public boolean isExclusivityRequired(HttpServerExchange exchange) { + List authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION); + if (authHeaders != null) { + for (String current : authHeaders) { + if (current.startsWith(NEGOTIATE_PREFIX)) { + return true; + } + } + } + + return false; + } + }; + + private static final String NEGOTIATION_PLAIN = NEGOTIATE.toString(); + private static final String NEGOTIATE_PREFIX = NEGOTIATE + " "; + private final String name = "SPNEGO"; + + private final GSSAPIServerSubjectFactory subjectFactory; + + public GSSAPIAuthenticationMechanism(final GSSAPIServerSubjectFactory subjectFactory) { + this.subjectFactory = subjectFactory; + } + + @Override + public AuthenticationMechanismOutcome authenticate(final HttpServerExchange exchange, + final SecurityContext securityContext) { + ServerConnection connection = exchange.getConnection(); + NegotiationContext negContext = connection.getAttachment(NegotiationContext.ATTACHMENT_KEY); + if (negContext != null) { + exchange.putAttachment(NegotiationContext.ATTACHMENT_KEY, negContext); + if (negContext.isEstablished()) { + IdentityManager identityManager = securityContext.getIdentityManager(); + final Account account = identityManager.verify(new GSSContextCredential(negContext.getGssContext())); + if (account != null) { + securityContext.authenticationComplete(account, name, false); + return AuthenticationMechanismOutcome.AUTHENTICATED; + } else { + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + } + + List authHeaders = exchange.getRequestHeaders().get(AUTHORIZATION); + if (authHeaders != null) { + for (String current : authHeaders) { + if (current.startsWith(NEGOTIATE_PREFIX)) { + String base64Challenge = current.substring(NEGOTIATE_PREFIX.length()); + try { + ByteBuffer challenge = FlexBase64.decode(base64Challenge); + return runGSSAPI(exchange, challenge, securityContext); + } catch (IOException e) { + } + + // By this point we had a header we should have been able to verify but for some reason + // it was not correctly structured. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + } + + // No suitable header was found so authentication was not even attempted. + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + + public ChallengeResult sendChallenge(final HttpServerExchange exchange, final SecurityContext securityContext) { + NegotiationContext negContext = exchange.getAttachment(NegotiationContext.ATTACHMENT_KEY); + + String header = NEGOTIATION_PLAIN; + + if (negContext != null) { + byte[] responseChallenge = negContext.useResponseToken(); + exchange.putAttachment(NegotiationContext.ATTACHMENT_KEY, null); + if (responseChallenge != null) { + header = NEGOTIATE_PREFIX + FlexBase64.encodeString(responseChallenge, false); + } + } + + exchange.getResponseHeaders().add(WWW_AUTHENTICATE, header); + + return new ChallengeResult(true, UNAUTHORIZED); + } + + + public AuthenticationMechanismOutcome runGSSAPI(final HttpServerExchange exchange, + final ByteBuffer challenge, final SecurityContext securityContext) { + try { + Subject server = subjectFactory.getSubjectForHost(getHostName(exchange)); + // The AcceptSecurityContext takes over responsibility for setting the result. + return Subject.doAs(server, new AcceptSecurityContext(exchange, challenge, securityContext)); + } catch (GeneralSecurityException e) { + e.printStackTrace(); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } catch (PrivilegedActionException e) { + e.printStackTrace(); + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + + private String getHostName(final HttpServerExchange exchange) { + String hostName = exchange.getRequestHeaders().getFirst(HOST); + if (hostName != null) { + if (hostName.contains(":")) { + hostName = hostName.substring(0, hostName.indexOf(":")); + } + return hostName; + } + + return null; + } + + + private class AcceptSecurityContext implements PrivilegedExceptionAction { + + private final HttpServerExchange exchange; + private final ByteBuffer challenge; + private final SecurityContext securityContext; + + private AcceptSecurityContext(final HttpServerExchange exchange, + final ByteBuffer challenge, final SecurityContext securityContext) { + this.exchange = exchange; + this.challenge = challenge; + this.securityContext = securityContext; + } + + public AuthenticationMechanismOutcome run() throws GSSException { + NegotiationContext negContext = exchange.getAttachment(NegotiationContext.ATTACHMENT_KEY); + if (negContext == null) { + negContext = new NegotiationContext(); + exchange.putAttachment(NegotiationContext.ATTACHMENT_KEY, negContext); + // Also cache it on the connection for future calls. + exchange.getConnection().putAttachment(NegotiationContext.ATTACHMENT_KEY, negContext); + } + + GSSContext gssContext = negContext.getGssContext(); + if (gssContext == null) { + GSSManager manager = GSSManager.getInstance(); + gssContext = manager.createContext((GSSCredential) null); + + negContext.setGssContext(gssContext); + } + + byte[] respToken = gssContext.acceptSecContext(challenge.array(), challenge.arrayOffset(), challenge.limit()); + negContext.setResponseToken(respToken); + + if (negContext.isEstablished()) { + + if (respToken != null) { + // There will be no further challenge but we do have a token so set it here. + exchange.getResponseHeaders().add(WWW_AUTHENTICATE, + NEGOTIATE_PREFIX + FlexBase64.encodeString(respToken, false)); + } + IdentityManager identityManager = securityContext.getIdentityManager(); + final Account account = identityManager.verify(new GSSContextCredential(negContext.getGssContext())); + if (account != null) { + securityContext.authenticationComplete(account, name, false); + return AuthenticationMechanismOutcome.AUTHENTICATED; + } else { + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } else { + // This isn't a failure but as the context is not established another round trip with the client is needed. + return AuthenticationMechanismOutcome.NOT_AUTHENTICATED; + } + } + } + + private static class NegotiationContext { + + static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(NegotiationContext.class); + + private GSSContext gssContext; + private byte[] responseToken; + private Principal principal; + + GSSContext getGssContext() { + return gssContext; + } + + void setGssContext(GSSContext gssContext) { + this.gssContext = gssContext; + } + + byte[] useResponseToken() { + // The token only needs to be returned once so clear it once used. + try { + return responseToken; + } finally { + responseToken = null; + } + } + + void setResponseToken(byte[] responseToken) { + this.responseToken = responseToken; + } + + boolean isEstablished() { + return gssContext != null ? gssContext.isEstablished() : false; + } + + Principal getPrincipal() { + if (!isEstablished()) { + throw new IllegalStateException("No established GSSContext to use for the Principal."); + } + + if (principal == null) { + try { + principal = new KerberosPrincipal(gssContext.getSrcName().toString()); + } catch (GSSException e) { + throw new IllegalStateException("Unable to create Principal", e); + } + + } + + return principal; + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/InMemorySingleSignOnManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/InMemorySingleSignOnManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/InMemorySingleSignOnManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,117 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import io.undertow.security.idm.Account; +import io.undertow.server.session.SecureRandomSessionIdGenerator; +import io.undertow.server.session.Session; +import io.undertow.server.session.SessionManager; +import io.undertow.util.CopyOnWriteMap; + +/** + * @author Stuart Douglas + * @author Paul Ferraro + */ +public class InMemorySingleSignOnManager implements SingleSignOnManager { + + private static final SecureRandomSessionIdGenerator SECURE_RANDOM_SESSION_ID_GENERATOR = new SecureRandomSessionIdGenerator(); + + private final Map ssoEntries = new ConcurrentHashMap<>(); + + @Override + public SingleSignOn findSingleSignOn(String ssoId) { + return this.ssoEntries.get(ssoId); + } + + @Override + public SingleSignOn createSingleSignOn(Account account, String mechanism) { + String id = SECURE_RANDOM_SESSION_ID_GENERATOR.createSessionId(); + SingleSignOn entry = new SimpleSingleSignOnEntry(id, account, mechanism); + this.ssoEntries.put(id, entry); + return entry; + } + + @Override + public void removeSingleSignOn(String ssoId) { + this.ssoEntries.remove(ssoId); + } + + private static class SimpleSingleSignOnEntry implements SingleSignOn { + private final String id; + private final Account account; + private final String mechanismName; + private final Map sessions = new CopyOnWriteMap<>(); + + SimpleSingleSignOnEntry(String id, Account account, String mechanismName) { + this.id = id; + this.account = account; + this.mechanismName = mechanismName; + } + + @Override + public String getId() { + return this.id; + } + + @Override + public Account getAccount() { + return this.account; + } + + @Override + public String getMechanismName() { + return this.mechanismName; + } + + @Override + public Iterator iterator() { + return Collections.unmodifiableCollection(this.sessions.values()).iterator(); + } + + @Override + public boolean contains(Session session) { + return this.sessions.containsKey(session.getSessionManager()); + } + + @Override + public Session getSession(SessionManager manager) { + return this.sessions.get(manager); + } + + @Override + public void add(Session session) { + this.sessions.put(session.getSessionManager(), session); + } + + @Override + public void remove(Session session) { + this.sessions.remove(session.getSessionManager()); + } + + @Override + public void close() { + // Do nothing + } + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SecurityActions.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SecurityActions.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SecurityActions.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.security.AccessController; +import java.security.PrivilegedAction; + +import io.undertow.security.api.AuthenticationMode; +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpServerExchange; + +class SecurityActions { + static SecurityContextImpl createSecurityContextImpl(final HttpServerExchange exchange, final AuthenticationMode authenticationMode, final IdentityManager identityManager) { + if (System.getSecurityManager() == null) { + return new SecurityContextImpl(exchange, authenticationMode, identityManager); + } else { + return AccessController.doPrivileged(new PrivilegedAction() { + @Override + public SecurityContextImpl run() { + return new SecurityContextImpl(exchange, authenticationMode, identityManager); + } + }); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SecurityContextFactoryImpl.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SecurityContextFactoryImpl.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SecurityContextFactoryImpl.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,51 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.api.AuthenticationMode; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.api.SecurityContextFactory; +import io.undertow.security.idm.IdentityManager; +import io.undertow.server.HttpServerExchange; + +/** + *

+ * Default {@link io.undertow.security.api.SecurityContextFactory} implementation. It creates + * {@link io.undertow.security.impl.SecurityContextImpl} instances with the specified parameters, setting the + * programmatic mechanism name if it is not null. + *

+ * + * @author Stefan Guilhen + */ +public class SecurityContextFactoryImpl implements SecurityContextFactory { + + public static final SecurityContextFactory INSTANCE = new SecurityContextFactoryImpl(); + + private SecurityContextFactoryImpl() { + + } + + @Override + public SecurityContext createSecurityContext(final HttpServerExchange exchange, final AuthenticationMode mode, + final IdentityManager identityManager, final String programmaticMechName) { + SecurityContextImpl securityContext = SecurityActions.createSecurityContextImpl(exchange, mode, identityManager); + if (programmaticMechName != null) + securityContext.setProgramaticMechName(programmaticMechName); + return securityContext; + } +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SecurityContextImpl.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SecurityContextImpl.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SecurityContextImpl.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,382 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; + +import io.undertow.UndertowMessages; +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.AuthenticationMechanism.AuthenticationMechanismOutcome; +import io.undertow.security.api.AuthenticationMechanism.ChallengeResult; +import io.undertow.security.api.AuthenticationMode; +import io.undertow.security.api.NotificationReceiver; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.api.SecurityNotification; +import io.undertow.security.api.SecurityNotification.EventType; +import io.undertow.security.idm.Account; +import io.undertow.security.idm.IdentityManager; +import io.undertow.security.idm.PasswordCredential; +import io.undertow.server.HttpServerExchange; + +import static io.undertow.UndertowMessages.MESSAGES; +import static io.undertow.util.StatusCodes.FORBIDDEN; +import static io.undertow.util.StatusCodes.OK; + +/** + * The internal SecurityContext used to hold the state of security for the current exchange. + * + * @author Darran Lofthouse + * @author Stuart Douglas + */ +public class SecurityContextImpl implements SecurityContext { + + private static final RuntimePermission PERMISSION = new RuntimePermission("MODIFY_UNDERTOW_SECURITY_CONTEXT"); + + private final AuthenticationMode authenticationMode; + private boolean authenticationRequired; + private String programaticMechName = "Programatic"; + private AuthenticationState authenticationState = AuthenticationState.NOT_ATTEMPTED; + private final HttpServerExchange exchange; + private final List authMechanisms = new ArrayList<>(); + private final IdentityManager identityManager; + private final List notificationReceivers = new ArrayList<>(); + + + // Maybe this will need to be a custom mechanism that doesn't exchange tokens with the client but will then + // be configured to either associate with the connection, the session or some other arbitrary whatever. + // + // Do we want multiple to be supported or just one? Maybe extend the AuthenticationMechanism to allow + // it to be identified and called. + + private String mechanismName; + private Account account; + + // TODO - Why two constructors? Maybe the first can do. + + public SecurityContextImpl(final HttpServerExchange exchange, final IdentityManager identityManager) { + this(exchange, AuthenticationMode.PRO_ACTIVE, identityManager); + } + + public SecurityContextImpl(final HttpServerExchange exchange, final AuthenticationMode authenticationMode, final IdentityManager identityManager) { + this.authenticationMode = authenticationMode; + this.identityManager = identityManager; + this.exchange = exchange; + if (System.getSecurityManager() != null) { + System.getSecurityManager().checkPermission(PERMISSION); + } + } + + /* + * Authentication can be represented as being at one of many states with different transitions depending on desired outcome. + * + * NOT_ATTEMPTED + * ATTEMPTED + * AUTHENTICATED + * CHALLENGED_SENT + */ + + public boolean authenticate() { + // TODO - I don't see a need to force single threaded - if this request is from the servlet APIs then the request will + // have already been dispatched. + return !authTransition(); + } + + private boolean authTransition() { + if (authTransitionRequired()) { + switch (authenticationState) { + case NOT_ATTEMPTED: + authenticationState = attemptAuthentication(); + break; + case ATTEMPTED: + authenticationState = sendChallenges(); + break; + default: + throw new IllegalStateException("It should not be possible to reach this."); + } + return authTransition(); + + } else { + // Keep in mind this switch statement is only called after a call to authTransitionRequired. + switch (authenticationState) { + case NOT_ATTEMPTED: // No constraint was set that mandated authentication so not reason to hold up the request. + case ATTEMPTED: // Attempted based on incoming request but no a failure so allow the request to proceed. + case AUTHENTICATED: // Authentication was a success - no responses sent. + return false; + default: + // Remaining option is CHALLENGE_SENT to request processing must end. + return true; + } + } + } + + private AuthenticationState attemptAuthentication() { + return new AuthAttempter(authMechanisms.iterator(), exchange).transition(); + } + + private AuthenticationState sendChallenges() { + return new ChallengeSender(authMechanisms.iterator(), exchange).transition(); + } + + private boolean authTransitionRequired() { + switch (authenticationState) { + case NOT_ATTEMPTED: + // There has been no attempt to authenticate the current request so do so either if required or if we are set to + // be pro-active. + return authenticationRequired || authenticationMode == AuthenticationMode.PRO_ACTIVE; + case ATTEMPTED: + // To be ATTEMPTED we know it was not AUTHENTICATED so if it is required we need to transition to send the + // challenges. + return authenticationRequired; + default: + // At this point the state would either be AUTHENTICATED or CHALLENGE_SENT - either of which mean no further + // transitions applicable for this request. + return false; + } + } + + @Override + public void setAuthenticationRequired() { + authenticationRequired = true; + } + + @Override + public boolean isAuthenticationRequired() { + return authenticationRequired; + } + + @Override + public boolean isAuthenticated() { + return authenticationState == AuthenticationState.AUTHENTICATED; + } + + /** + * Set the name of the mechanism used for authentication to be reported if authentication was handled programatically. + * + * @param programaticMechName + */ + public void setProgramaticMechName(final String programaticMechName) { + this.programaticMechName = programaticMechName; + } + + /** + * @return The name of the mechanism used to authenticate the request. + */ + @Override + public String getMechanismName() { + return mechanismName; + } + + @Override + public void addAuthenticationMechanism(final AuthenticationMechanism handler) { + // TODO - Do we want to change this so we can ensure the mechanisms are not modifiable mid request? + authMechanisms.add(handler); + } + + @Override + public List getAuthenticationMechanisms() { + return Collections.unmodifiableList(authMechanisms); + } + + @Override + public Account getAuthenticatedAccount() { + return account; + } + + @Override + public IdentityManager getIdentityManager() { + return identityManager; + } + + @Override + public boolean login(final String username, final String password) { + final Account account = identityManager.verify(username, new PasswordCredential(password.toCharArray())); + if (account == null) { + return false; + } + + authenticationComplete(account, programaticMechName, true); + this.authenticationState = AuthenticationState.AUTHENTICATED; + + return true; + } + + @Override + public void logout() { + if (!isAuthenticated()) { + return; + } + sendNoticiation(new SecurityNotification(exchange, SecurityNotification.EventType.LOGGED_OUT, account, mechanismName, true, + MESSAGES.userLoggedOut(account.getPrincipal().getName()), true)); + + this.account = null; + this.mechanismName = null; + this.authenticationState = AuthenticationState.NOT_ATTEMPTED; + } + + @Override + public void authenticationComplete(Account account, String mechanism, final boolean cachingRequired) { + authenticationComplete(account, mechanism, false, cachingRequired); + } + + protected void authenticationComplete(Account account, String mechanism, boolean programatic, final boolean cachingRequired) { + this.account = account; + this.mechanismName = mechanism; + + sendNoticiation(new SecurityNotification(exchange, EventType.AUTHENTICATED, account, mechanism, programatic, + MESSAGES.userAuthenticated(account.getPrincipal().getName()), cachingRequired)); + } + + @Override + public void authenticationFailed(String message, String mechanism) { + sendNoticiation(new SecurityNotification(exchange, EventType.FAILED_AUTHENTICATION, null, mechanism, false, message, true)); + } + + private void sendNoticiation(final SecurityNotification notification) { + for (NotificationReceiver current : notificationReceivers) { + current.handleNotification(notification); + } + } + + @Override + public void registerNotificationReceiver(NotificationReceiver receiver) { + notificationReceivers.add(receiver); + } + + @Override + public void removeNotificationReceiver(NotificationReceiver receiver) { + notificationReceivers.remove(receiver); + } + + private class AuthAttempter { + + private final Iterator mechanismIterator; + private final HttpServerExchange exchange; + + private AuthAttempter(final Iterator mechanismIterator, final HttpServerExchange exchange) { + this.mechanismIterator = mechanismIterator; + this.exchange = exchange; + } + + private AuthenticationState transition() { + if (mechanismIterator.hasNext()) { + final AuthenticationMechanism mechanism = mechanismIterator.next(); + AuthenticationMechanismOutcome outcome = mechanism.authenticate(exchange, SecurityContextImpl.this); + + if (outcome == null) { + throw UndertowMessages.MESSAGES.authMechanismOutcomeNull(); + } + + switch (outcome) { + case AUTHENTICATED: + // TODO - Should verify that the mechanism did register an authenticated Account. + return AuthenticationState.AUTHENTICATED; + case NOT_AUTHENTICATED: + // A mechanism attempted to authenticate but could not complete, this now means that + // authentication is required and challenges need to be sent. + setAuthenticationRequired(); + return AuthenticationState.ATTEMPTED; + case NOT_ATTEMPTED: + // Time to try the next mechanism. + return transition(); + default: + throw new IllegalStateException(); + } + + } else { + // Reached the end of the mechanisms and no mechanism authenticated for us to reach this point. + return AuthenticationState.ATTEMPTED; + } + } + + } + + /** + * Class responsible for sending the authentication challenges. + */ + private class ChallengeSender { + + private final Iterator mechanismIterator; + private final HttpServerExchange exchange; + + private boolean atLeastOneChallenge = false; + private Integer chosenStatusCode = null; + + private ChallengeSender(final Iterator mechanismIterator, final HttpServerExchange exchange) { + this.mechanismIterator = mechanismIterator; + this.exchange = exchange; + } + + private AuthenticationState transition() { + if (mechanismIterator.hasNext()) { + final AuthenticationMechanism mechanism = mechanismIterator.next(); + ChallengeResult result = mechanism.sendChallenge(exchange, SecurityContextImpl.this); + + if (result.isChallengeSent()) { + atLeastOneChallenge = true; + Integer desiredCode = result.getDesiredResponseCode(); + if (chosenStatusCode == null) { + chosenStatusCode = desiredCode; + } else if (desiredCode != null) { + if (chosenStatusCode.equals(OK)) { + // Allows a more specific code to be chosen. + // TODO - Still need a more complex code resolution strategy if many different codes are + // returned (Although those mechanisms may just never work together.) + chosenStatusCode = desiredCode; + } + } + } + + + // We always transition so we can reach the end of the list and hit the else. + return transition(); + + } else { + // Iterated all mechanisms, now need to select a suitable status code. + if (atLeastOneChallenge) { + if (chosenStatusCode != null) { + exchange.setResponseCode(chosenStatusCode); + } + } else { + // No mechanism generated a challenge so send a 403 as our challenge - i.e. just rejecting the request. + exchange.setResponseCode(FORBIDDEN); + } + + return AuthenticationState.CHALLENGE_SENT; + + } + } + + } + + /** + * Representation of the current authentication state of the SecurityContext. + */ + enum AuthenticationState { + NOT_ATTEMPTED, + + ATTEMPTED, + + AUTHENTICATED, + + CHALLENGE_SENT; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SimpleNonceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SimpleNonceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SimpleNonceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,545 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import static io.undertow.UndertowMessages.MESSAGES; + +import io.undertow.security.api.SessionNonceManager; +import io.undertow.server.HttpServerExchange; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Random; +import java.util.Set; +import java.util.WeakHashMap; +import java.util.concurrent.TimeUnit; + +import org.xnio.XnioExecutor; +import org.xnio.XnioExecutor.Key; + +import io.undertow.util.FlexBase64; + +/** + * A default {@link io.undertow.security.api.NonceManager} implementation to provide reasonable single host management of nonces. + * + * This {@link io.undertow.security.api.NonceManager} manages nonces in two groups, the first is the group that are allocated to new requests, this group + * is a problem as we want to be able to limit how many we distribute so we don't have a DOS storing too many but we also don't + * a high number of requests to to push the other valid nonces out faster than they can be used. + * + * The second group is the set of nonces actively in use - these should be maintained as we can also maintain the nonce count + * and even track the next nonce once invalid. + * + * Maybe group one should be a timestamp and private key hashed together, if used with a nonce count they move to be tracked to + * ensure the same count is not used again - if successfully used without a nonce count add to a blacklist until expiration? A + * nonce used without a nonce count will essentially be single use with each request getting a new nonce. + * + * @author Darran Lofthouse + */ +public class SimpleNonceManager implements SessionNonceManager { + + private static final String DEFAULT_HASH_ALG = "MD5"; + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + /** + * List of invalid nonces, this list contains the nonces that have been used without a nonce count. + * + * In that situation they are considered single use and must not be used again. + */ + private final Set invalidNonces = Collections.synchronizedSet(new HashSet()); + + /** + * Map of known currently valid nonces, a SortedMap is used to order the nonces by their creation time stamp allowing a + * simple iteration over the keys to identify expired nonces. + */ + private final Map knownNonces = Collections.synchronizedMap(new HashMap()); + + /** + * A WeakHashMap to map expired nonces to their replacement nonce. For an item to be added to this Collection the value will + * have been removed from the knownNonces map. + * + * A replacement nonce will have been added to knownNonces that references the key used here - once the replacement nonce is + * removed from knownNonces then the key will be eligible for garbage collection allowing it to be removed from this map as + * well. + * + * The value in this Map is a plain String, this is to avoid inadvertently creating a long term reference to the key we + * expect to be garbage collected at some point in the future. + */ + private final Map forwardMapping = Collections.synchronizedMap(new WeakHashMap()); + + /** + * A pseudo-random generator for creating the nonces, a secure random is not required here as this is used purely to + * minimise the chance of collisions should two nonces be generated at exactly the same time. + */ + private final Random random = new Random(); + + private final String secret; + private final String hashAlg; + private final int hashLength; + + /** + * After a nonce is issued the first authentication response MUST be received within 5 minutes. + */ + private final long firstUseTimeOut = 5 * 60 * 1000; + + /** + * Overall a nonce is valid from 15 minutes from first being issued, if used after this then a new nonce will be issued. + */ + private final long overallTimeOut = 15 * 60 * 1000; + + /** + * A previously used nonce will be allowed to remain in the knownNonces list for up to 5 minutes. + * + * The nonce will be accepted during this 5 minute window but will immediately be replaced causing any additional requests + * to be forced to use the new nonce. + * + * This is primarily for session based digests where loosing the cached session key would be bad. + */ + private final long cacheTimePostExpiry = 5 * 60 * 1000; + + public SimpleNonceManager() { + this(DEFAULT_HASH_ALG); + } + + public SimpleNonceManager(final String hashAlg) { + // Verify it is a valid algorithm (at least for now) + MessageDigest digest = getDigest(hashAlg); + + this.hashAlg = hashAlg; + this.hashLength = digest.getDigestLength(); + + // Create a new secret only valid within this NonceManager instance. + Random rand = new SecureRandom(); + byte[] secretBytes = new byte[32]; + rand.nextBytes(secretBytes); + secret = FlexBase64.encodeString(digest.digest(secretBytes), false); + } + + private MessageDigest getDigest(final String hashAlg) { + try { + return MessageDigest.getInstance(hashAlg); + } catch (NoSuchAlgorithmException e) { + throw MESSAGES.hashAlgorithmNotFound(hashAlg); + } + } + + /** + * + * @see io.undertow.security.api.NonceManager#nextNonce(java.lang.String) + */ + public String nextNonce(String lastNonce, HttpServerExchange exchange) { + if (lastNonce == null) { + return createNewNonceString(); + } + + if (invalidNonces.contains(lastNonce)) { + // The nonce supplied has already been used. + return createNewNonceString(); + } + + String nonce = lastNonce; + // Loop the forward mappings. + synchronized (forwardMapping) { + NonceHolder holder = new NonceHolder(lastNonce); + while (forwardMapping.containsKey(holder)) { + nonce = forwardMapping.get(holder); + // The final NonceHolder will then be used if a forwardMapping needs to be set. + holder = new NonceHolder(nonce); + } + + synchronized (knownNonces) { + Nonce value = knownNonces.get(nonce); + if (value == null) { + // Not a likely scenario but if this occurs then most likely the nonce mapped to has also expired so we will + // just send a new nonce. + nonce = createNewNonceString(); + } else { + long now = System.currentTimeMillis(); + // The cacheTimePostExpiry is not included here as this is our opportunity to inform the client to use a + // replacement nonce without a stale round trip. + long earliestAccepted = now - firstUseTimeOut; + if (value.timeStamp < earliestAccepted || value.timeStamp > now) { + XnioExecutor executor = exchange.getIoThread(); + Nonce replacement = createNewNonce(holder); + if (value.executorKey != null) { + // The outcome doesn't matter - if we have the value we have all we need. + value.executorKey.remove(); + } + + nonce = replacement.nonce; + // Create a record of the forward mapping so if any requests do need to be marked stale they can be + // pointed towards the correct nonce to use. + forwardMapping.put(holder, nonce); + // Bring over any existing session key. + replacement.setSessionKey(value.getSessionKey()); + // At this point we will not accept the nonce again so remove it from the list of known nonces but do + // register the replacement. + knownNonces.remove(holder.nonce); + // There are two reasons for registering the replacement 1 - to preserve any session key, 2 - To keep a + // reference to the now invalid key so it + // can be used as a key in a weak hash map. + knownNonces.put(nonce, replacement); + earliestAccepted = now - (overallTimeOut + cacheTimePostExpiry); + long timeTillExpiry = replacement.timeStamp - earliestAccepted; + replacement.executorKey = executor.executeAfter(new KnownNonceCleaner(nonce), timeTillExpiry, + TimeUnit.MILLISECONDS); + + } + } + } + } + + return nonce; + } + + private String createNewNonceString() { + return createNewNonce(null).nonce; + } + + private Nonce createNewNonce(NonceHolder previousNonce) { + byte[] prefix = new byte[8]; + random.nextBytes(prefix); + long timeStamp = System.currentTimeMillis(); + byte[] now = Long.toString(timeStamp).getBytes(UTF_8); + + String nonce = createNonce(prefix, now); + + return new Nonce(nonce, timeStamp, previousNonce); + } + + /** + * + * @see io.undertow.security.api.NonceManager#validateNonce(java.lang.String, int) + */ + @Override + public boolean validateNonce(String nonce, int nonceCount, HttpServerExchange exchange) { + XnioExecutor executor = exchange.getIoThread(); + if (nonceCount < 0) { + if (invalidNonces.contains(nonce)) { + // Without a nonce count the nonce is only usable once. + return false; + } + // Not already known so will drop into first use validation. + } else if (knownNonces.containsKey(nonce)) { + // At this point we need to validate that the nonce is still within it's time limits, + // If a new nonce had been selected then a known nonce would not have been found. + // The nonce will also have it's nonce count checked. + return validateNonceWithCount(new Nonce(nonce), nonceCount, executor); + + } else if (forwardMapping.containsKey(new NonceHolder(nonce))) { + // We could have let this drop through as the next validation would fail anyway but + // why waste the time if we already know a replacement nonce has been issued. + return false; + } + + // This is not a nonce currently known to us so start the validation process. + Nonce value = verifyUnknownNonce(nonce, nonceCount); + if (value == null) { + return false; + } + + long now = System.currentTimeMillis(); + // NOTE - This check is for the first use, overall validity is checked in validateNonceWithCount. + long earliestAccepted = now - firstUseTimeOut; + if (value.timeStamp < earliestAccepted || value.timeStamp > now) { + // The embedded timestamp is either expired or somehow is after now. + return false; + } + + if (nonceCount < 0) { + // Allow a single use but reject all further uses. + return addInvalidNonce(value, executor); + } else { + return validateNonceWithCount(value, nonceCount, executor); + } + } + + private boolean validateNonceWithCount(Nonce nonce, int nonceCount, final XnioExecutor executor) { + // This point could have been reached either because the knownNonces map contained the key or because + // it didn't and a count was supplied - either way need to double check the contents of knownNonces once + // the lock is in place. + synchronized (knownNonces) { + Nonce value = knownNonces.get(nonce.nonce); + long now = System.currentTimeMillis(); + // For the purpose of this validation we also add the cacheTimePostExpiry - when nextNonce is subsequently + // called it will decide if we are in the interval to replace the nonce. + long earliestAccepted = now - (overallTimeOut + cacheTimePostExpiry); + if (value == null) { + if (nonce.timeStamp < 0) { + // Means it was in there, now it isn't - most likely a timestamp expiration mid check - abandon validation. + return false; + } + + if (nonce.timeStamp > earliestAccepted && nonce.timeStamp < now) { + knownNonces.put(nonce.nonce, nonce); + long timeTillExpiry = nonce.timeStamp - earliestAccepted; + nonce.executorKey = executor.executeAfter(new KnownNonceCleaner(nonce.nonce), timeTillExpiry, + TimeUnit.MILLISECONDS); + return true; + } + + return false; + } else { + // We have it, just need to verify that it has not expired and that the nonce key is valid. + if (value.timeStamp < earliestAccepted || value.timeStamp > now) { + // The embedded timestamp is either expired or somehow is after now!! + return false; + } + + if (value.getMaxNonceCount() < nonceCount) { + value.setMaxNonceCount(nonceCount); + return true; + } + + return false; + } + + } + + } + + private boolean addInvalidNonce(final Nonce nonce, final XnioExecutor executor) { + long now = System.currentTimeMillis(); + long invalidBefore = now - firstUseTimeOut; + + long timeTillInvalid = nonce.timeStamp - invalidBefore; + if (timeTillInvalid > 0) { + if (invalidNonces.add(nonce.nonce)) { + executor.executeAfter(new InvalidNonceCleaner(nonce.nonce), timeTillInvalid, TimeUnit.MILLISECONDS); + return true; + } else { + return false; + } + } else { + // So close to expiring any record of this nonce being used could have been cleared so + // don't take a chance and just say no. + return false; + } + } + + /** + * Verify a previously unknown nonce and return the {@link NonceKey} representation for the nonce. + * + * Later when a nonce is re-used we can match based on the String alone - the information embedded within the nonce will be + * cached with it. + * + * This stage of the verification simply extracts the prefix and the embedded timestamp and recreates a new hashed and + * Base64 nonce based on the local secret - if the newly generated nonce matches the supplied one we accept it was created + * by this nonce manager. + * + * This verification does not validate that the timestamp is within a valid time period. + * + * @param nonce - + * @return + */ + private Nonce verifyUnknownNonce(final String nonce, final int nonceCount) { + byte[] complete; + int offset; + int length; + try { + ByteBuffer decode = FlexBase64.decode(nonce); + complete = decode.array(); + offset = decode.arrayOffset(); + length = decode.limit() - offset; + } catch (IOException e) { + throw MESSAGES.invalidBase64Token(e); + } + + int timeStampLength = complete[offset + 8]; + // A sanity check to try and verify the sizes we expect from the arrays are correct. + if (hashLength > 0) { + int expectedLength = 9 + timeStampLength + hashLength; + if (length != expectedLength) { + throw MESSAGES.invalidNonceReceived(); + } else if (timeStampLength + 1 >= length) + throw MESSAGES.invalidNonceReceived(); + } + + byte[] prefix = new byte[8]; + System.arraycopy(complete, offset, prefix, 0, 8); + byte[] timeStampBytes = new byte[timeStampLength]; + System.arraycopy(complete, offset + 9, timeStampBytes, 0, timeStampBytes.length); + + String expectedNonce = createNonce(prefix, timeStampBytes); + + if (expectedNonce.equals(nonce)) { + try { + long timeStamp = Long.parseLong(new String(timeStampBytes, UTF_8)); + + return new Nonce(expectedNonce, timeStamp, nonceCount); + } catch (NumberFormatException dropped) { + } + } + + return null; + } + + private String createNonce(final byte[] prefix, final byte[] timeStamp) { + byte[] hashedPart = generateHash(prefix, timeStamp); + byte[] complete = new byte[9 + timeStamp.length + hashedPart.length]; + System.arraycopy(prefix, 0, complete, 0, 8); + complete[8] = (byte) timeStamp.length; + System.arraycopy(timeStamp, 0, complete, 9, timeStamp.length); + System.arraycopy(hashedPart, 0, complete, 9 + timeStamp.length, hashedPart.length); + + return FlexBase64.encodeString(complete, false); + } + + private byte[] generateHash(final byte[] prefix, final byte[] timeStamp) { + MessageDigest digest = getDigest(hashAlg); + + digest.update(prefix); + digest.update(timeStamp); + + return digest.digest(secret.getBytes(UTF_8)); + } + + public void associateHash(String nonce, byte[] hash) { + // TODO Auto-generated method stub + + } + + public byte[] lookupHash(String nonce) { + // TODO Auto-generated method stub + return null; + } + + /** + * A simple wrapper around a nonce to allow it to be used as a key in a weak map. + */ + private class NonceHolder { + private final String nonce; + + private NonceHolder(final String nonce) { + if (nonce == null) { + throw new NullPointerException("nonce must not be null."); + } + this.nonce = nonce; + } + + @Override + public int hashCode() { + return nonce.hashCode(); + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof NonceHolder) ? nonce.equals(((NonceHolder) obj).nonce) : false; + } + } + + /** + * The state associated with a nonce. + * + * A NonceKey for a preciously valid nonce is also referenced, this is so that a WeakHashMap can be used to maintain a + * mapping from the original NonceKey to the new nonce value. + */ + private class Nonce { + + private final String nonce; + + private final long timeStamp; + // TODO we will also add a mechanism to track the gaps as the only restriction is that a NC can only be used one. + private int maxNonceCount; + // We keep this as it is used in the wek hash map as a forward mapping as long as the nonce to map to is still alive. + @SuppressWarnings("unused") + private final NonceHolder previousNonce; + private byte[] sessionKey; + private Key executorKey; + + private Nonce(final String nonce) { + this(nonce, -1, -1); + } + + private Nonce(final String nonce, final long timeStamp) { + this(nonce, timeStamp, -1); + } + + private Nonce(final String nonce, final long timeStamp, final int initialNC) { + this(nonce, timeStamp, initialNC, null); + } + + private Nonce(final String nonce, final long timeStamp, final NonceHolder previousNonce) { + this(nonce, timeStamp, -1, previousNonce); + } + + private Nonce(final String nonce, final long timeStamp, final int initialNC, final NonceHolder previousNonce) { + this.nonce = nonce; + this.timeStamp = timeStamp; + this.maxNonceCount = initialNC; + this.previousNonce = previousNonce; + } + + byte[] getSessionKey() { + return sessionKey; + } + + void setSessionKey(final byte[] sessionKey) { + this.sessionKey = sessionKey; + } + + int getMaxNonceCount() { + return maxNonceCount; + } + + void setMaxNonceCount(int maxNonceCount) { + this.maxNonceCount = maxNonceCount; + } + + } + + private class InvalidNonceCleaner implements Runnable { + + private final String nonce; + + private InvalidNonceCleaner(final String nonce) { + if (nonce == null) { + throw new NullPointerException("nonce must not be null."); + } + this.nonce = nonce; + } + + public void run() { + invalidNonces.remove(nonce); + } + + } + + private class KnownNonceCleaner implements Runnable { + private final String nonce; + + private KnownNonceCleaner(final String nonce) { + if (nonce == null) { + throw new NullPointerException("nonce must not be null."); + } + this.nonce = nonce; + } + + public void run() { + knownNonces.remove(nonce); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOn.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOn.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOn.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.idm.Account; +import io.undertow.server.session.Session; +import io.undertow.server.session.SessionManager; + +/** + * @author Stuart Douglas + * @author Paul Ferraro + */ +public interface SingleSignOn extends Iterable, AutoCloseable { + + /** + * Returns the unique identifier for this SSO. + * @return + */ + String getId(); + + /** + * Returns the account associated with this SSO. + * @return an account + */ + Account getAccount(); + + /** + * Returns the authentication mechanism used to create the account associated with this SSO. + * @return an authentication mechanism + */ + String getMechanismName(); + + /** + * Indicates whether or not the specified session is contained in the set of sessions to which the user is authenticated + * @param manager a session manager + * @return + */ + boolean contains(Session session); + + /** + * Adds the specified session to the set of sessions to which the user is authenticated + * @param manager a session manager + */ + void add(Session session); + + /** + * Removes the specified session from the set of sessions to which the user is authenticated + * @param manager a session manager + */ + void remove(Session session); + + /** + * Returns the session associated with the deployment of the specified session manager + * @param manager a session manager + * @return a session + */ + Session getSession(SessionManager manager); + + /** + * Releases any resources acquired by this object. + * Must be called after this object is no longer in use. + */ + @Override + void close(); +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOnAuthenticationMechanism.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOnAuthenticationMechanism.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOnAuthenticationMechanism.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,232 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.NotificationReceiver; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.api.SecurityNotification; +import io.undertow.security.idm.Account; +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.CookieImpl; +import io.undertow.server.session.Session; +import io.undertow.server.session.SessionListener; +import io.undertow.server.session.SessionManager; +import io.undertow.util.ConduitFactory; +import io.undertow.util.Sessions; +import org.xnio.conduits.StreamSinkConduit; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +/** + * Authenticator that can be used to configure single sign on. + * + * @author Stuart Douglas + * @author Paul Ferraro + */ +public class SingleSignOnAuthenticationMechanism implements AuthenticationMechanism { + + private static final String SSO_SESSION_ATTRIBUTE = SingleSignOnAuthenticationMechanism.class.getName() + ".SSOID"; + + // Use weak references to prevent memory leaks following undeployment + private final Set seenSessionManagers = Collections.synchronizedSet(Collections.newSetFromMap(new WeakHashMap())); + + private String cookieName = "JSESSIONIDSSO"; + private boolean httpOnly; + private boolean secure; + private String domain; + private String path; + private final SessionInvalidationListener listener = new SessionInvalidationListener(); + private final ResponseListener responseListener = new ResponseListener(); + private final SingleSignOnManager manager; + + public SingleSignOnAuthenticationMechanism(SingleSignOnManager storage) { + this.manager = storage; + } + + @Override + public AuthenticationMechanismOutcome authenticate(HttpServerExchange exchange, SecurityContext securityContext) { + Cookie cookie = exchange.getRequestCookies().get(cookieName); + if (cookie != null) { + final String ssoId = cookie.getValue(); + try (SingleSignOn sso = this.manager.findSingleSignOn(ssoId)) { + if (sso != null) { + Account verified = securityContext.getIdentityManager().verify(sso.getAccount()); + if (verified == null) { + //we return not attempted here to allow other mechanisms to proceed as normal + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + final Session session = getSession(exchange); + registerSessionIfRequired(sso, session); + securityContext.authenticationComplete(verified, sso.getMechanismName(), false); + securityContext.registerNotificationReceiver(new NotificationReceiver() { + @Override + public void handleNotification(SecurityNotification notification) { + if (notification.getEventType() == SecurityNotification.EventType.LOGGED_OUT) { + manager.removeSingleSignOn(ssoId); + } + } + }); + return AuthenticationMechanismOutcome.AUTHENTICATED; + } + } + clearSsoCookie(exchange); + } + exchange.addResponseWrapper(responseListener); + return AuthenticationMechanismOutcome.NOT_ATTEMPTED; + } + + private void registerSessionIfRequired(SingleSignOn sso, Session session) { + if (!sso.contains(session)) { + sso.add(session); + session.setAttribute(SSO_SESSION_ATTRIBUTE, sso.getId()); + SessionManager manager = session.getSessionManager(); + if (seenSessionManagers.add(manager)) { + manager.registerSessionListener(listener); + } + } + } + + private void clearSsoCookie(HttpServerExchange exchange) { + exchange.getResponseCookies().put(cookieName, new CookieImpl(cookieName).setMaxAge(0).setHttpOnly(httpOnly).setSecure(secure).setDomain(domain)); + } + + @Override + public ChallengeResult sendChallenge(HttpServerExchange exchange, SecurityContext securityContext) { + return new ChallengeResult(false); + } + + protected Session getSession(final HttpServerExchange exchange) { + return Sessions.getOrCreateSession(exchange); + } + + final class ResponseListener implements ConduitWrapper { + + @Override + public StreamSinkConduit wrap(ConduitFactory factory, HttpServerExchange exchange) { + SecurityContext sc = exchange.getSecurityContext(); + Account account = sc.getAuthenticatedAccount(); + if (account != null) { + try (SingleSignOn sso = manager.createSingleSignOn(account, sc.getMechanismName())) { + Session session = getSession(exchange); + registerSessionIfRequired(sso, session); + exchange.getResponseCookies().put(cookieName, new CookieImpl(cookieName, sso.getId()).setHttpOnly(httpOnly).setSecure(secure).setDomain(domain).setPath(path)); + } + } + return factory.create(); + } + } + + + final class SessionInvalidationListener implements SessionListener { + + @Override + public void sessionCreated(Session session, HttpServerExchange exchange) { + } + + @Override + public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { + String ssoId = (String) session.getAttribute(SSO_SESSION_ATTRIBUTE); + if (ssoId != null) { + try (SingleSignOn sso = manager.findSingleSignOn(ssoId)) { + if (sso != null) { + sso.remove(session); + if (reason == SessionDestroyedReason.INVALIDATED) { + for (Session associatedSession : sso) { + associatedSession.invalidate(null); + sso.remove(associatedSession); + } + } + // If there are no more associated sessions, remove the SSO altogether + if (!sso.iterator().hasNext()) { + manager.removeSingleSignOn(ssoId); + } + } + } + } + } + + @Override + public void attributeAdded(Session session, String name, Object value) { + } + + @Override + public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) { + } + + @Override + public void attributeRemoved(Session session, String name, Object oldValue) { + } + + @Override + public void sessionIdChanged(Session session, String oldSessionId) { + } + } + + + public String getCookieName() { + return cookieName; + } + + public SingleSignOnAuthenticationMechanism setCookieName(String cookieName) { + this.cookieName = cookieName; + return this; + } + + public boolean isHttpOnly() { + return httpOnly; + } + + public SingleSignOnAuthenticationMechanism setHttpOnly(boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + public boolean isSecure() { + return secure; + } + + public SingleSignOnAuthenticationMechanism setSecure(boolean secure) { + this.secure = secure; + return this; + } + + public String getDomain() { + return domain; + } + + public SingleSignOnAuthenticationMechanism setDomain(String domain) { + this.domain = domain; + return this; + } + + public String getPath() { + return path; + } + + public SingleSignOnAuthenticationMechanism setPath(String path) { + this.path = path; + return this; + } + +} Index: 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOnManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOnManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/security/impl/SingleSignOnManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,32 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.security.impl; + +import io.undertow.security.idm.Account; + +/** + * @author Paul Ferraro + */ +public interface SingleSignOnManager { + SingleSignOn createSingleSignOn(Account account, String mechanism); + + SingleSignOn findSingleSignOn(String ssoId); + + void removeSingleSignOn(String ssoId); +} Index: 3rdParty_sources/undertow/io/undertow/server/AbstractServerConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/AbstractServerConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/AbstractServerConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,303 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.LinkedList; +import java.util.List; + +public abstract class AbstractServerConnection extends ServerConnection { + protected final StreamConnection channel; + protected final CloseSetter closeSetter; + protected final Pool bufferPool; + protected final HttpHandler rootHandler; + protected final OptionMap undertowOptions; + protected final StreamSourceConduit originalSourceConduit; + protected final StreamSinkConduit originalSinkConduit; + protected final List closeListeners = new LinkedList<>(); + + protected HttpServerExchange current; + + private final int bufferSize; + /** + * Any extra bytes that were read from the channel. This could be data for this requests, or the next response. + */ + protected Pooled extraBytes; + + public AbstractServerConnection(StreamConnection channel, final Pool bufferPool, final HttpHandler rootHandler, final OptionMap undertowOptions, final int bufferSize) { + this.channel = channel; + this.bufferPool = bufferPool; + this.rootHandler = rootHandler; + this.undertowOptions = undertowOptions; + this.bufferSize = bufferSize; + closeSetter = new CloseSetter(); + if (channel != null) { + this.originalSinkConduit = channel.getSinkChannel().getConduit(); + this.originalSourceConduit = channel.getSourceChannel().getConduit(); + channel.setCloseListener(closeSetter); + } else { + this.originalSinkConduit = null; + this.originalSourceConduit = null; + } + } + + /** + * Get the root HTTP handler for this connection. + * + * @return the root HTTP handler for this connection + */ + public HttpHandler getRootHandler() { + return rootHandler; + } + + /** + * Get the buffer pool for this connection. + * + * @return the buffer pool for this connection + */ + @Override + public Pool getBufferPool() { + return bufferPool; + } + + /** + * Get the underlying channel. + * + * @return the underlying channel + */ + public StreamConnection getChannel() { + return channel; + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public XnioWorker getWorker() { + return channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + if(channel == null) { + return null; + } + return channel.getIoThread(); + } + + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public boolean supportsOption(final Option option) { + return channel.supportsOption(option); + } + + @Override + public T getOption(final Option option) throws IOException { + return channel.getOption(option); + } + + @Override + public T setOption(final Option option, final T value) throws IllegalArgumentException, IOException { + return channel.setOption(option, value); + } + + @Override + public void close() throws IOException { + channel.close(); + } + + @Override + public SocketAddress getPeerAddress() { + return channel.getPeerAddress(); + } + + @Override + public A getPeerAddress(final Class type) { + return channel.getPeerAddress(type); + } + + @Override + public SocketAddress getLocalAddress() { + return channel.getLocalAddress(); + } + + @Override + public A getLocalAddress(final Class type) { + return channel.getLocalAddress(type); + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + /** + * @return The size of the buffers allocated by the buffer pool + */ + @Override + public int getBufferSize() { + return bufferSize; + } + + public Pooled getExtraBytes() { + return extraBytes; + } + + public void setExtraBytes(final Pooled extraBytes) { + this.extraBytes = extraBytes; + } + + /** + * @return The original source conduit + */ + public StreamSourceConduit getOriginalSourceConduit() { + return originalSourceConduit; + } + + /** + * @return The original underlying sink conduit + */ + public StreamSinkConduit getOriginalSinkConduit() { + return originalSinkConduit; + } + + /** + * Resets the channel to its original state, effectively disabling all current conduit + * wrappers. The current state is encapsulated inside a {@link ConduitState} object that + * can be used the restore the channel. + * + * @return An opaque representation of the previous channel state + */ + public ConduitState resetChannel() { + ConduitState ret = new ConduitState(channel.getSinkChannel().getConduit(), channel.getSourceChannel().getConduit()); + channel.getSinkChannel().setConduit(originalSinkConduit); + channel.getSourceChannel().setConduit(originalSourceConduit); + return ret; + } + + /** + * Resets the channel to its original state, effectively disabling all current conduit + * wrappers. The current state is lost. + */ + public void clearChannel() { + channel.getSinkChannel().setConduit(originalSinkConduit); + channel.getSourceChannel().setConduit(originalSourceConduit); + } + /** + * Restores the channel conduits to a previous state. + * + * @param state The original state + * @see #resetChannel() + */ + public void restoreChannel(final ConduitState state) { + channel.getSinkChannel().setConduit(state.sink); + channel.getSourceChannel().setConduit(state.source); + } + + public static class ConduitState { + final StreamSinkConduit sink; + final StreamSourceConduit source; + + private ConduitState(final StreamSinkConduit sink, final StreamSourceConduit source) { + this.sink = sink; + this.source = source; + } + } + + protected static StreamSinkConduit sink(ConduitState state) { + return state.sink; + } + + protected static StreamSourceConduit source(ConduitState state) { + return state.source; + } + + @Override + public void addCloseListener(CloseListener listener) { + this.closeListeners.add(listener); + } + + @Override + protected ConduitStreamSinkChannel getSinkChannel() { + return channel.getSinkChannel(); + } + + @Override + protected ConduitStreamSourceChannel getSourceChannel() { + return channel.getSourceChannel(); + } + + protected void setUpgradeListener(HttpUpgradeListener upgradeListener) { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + protected void maxEntitySizeUpdated(HttpServerExchange exchange) { + } + + private class CloseSetter implements ChannelListener.Setter, ChannelListener { + + private ChannelListener listener; + + @Override + public void set(ChannelListener listener) { + this.listener = listener; + } + + @Override + public void handleEvent(StreamConnection channel) { + for (CloseListener l : closeListeners) { + try { + l.closed(AbstractServerConnection.this); + } catch (Throwable e) { + UndertowLogger.REQUEST_LOGGER.exceptionInvokingCloseListener(l, e); + } + } + if(current != null) { + current.endExchange(); + } + ChannelListeners.invokeChannelListener(AbstractServerConnection.this, listener); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/BasicSSLSessionInfo.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/BasicSSLSessionInfo.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/BasicSSLSessionInfo.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,135 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.UndertowMessages; +import io.undertow.util.FlexBase64; +import org.xnio.SslClientAuthMode; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.CertificateException; +import javax.security.cert.X509Certificate; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.cert.Certificate; + +/** + * Basic SSL session information. This information is generally provided by a front end proxy. + * + * @author Stuart Douglas + */ +public class BasicSSLSessionInfo implements SSLSessionInfo { + + private static final Charset US_ASCII = Charset.forName("US-ASCII"); + + private final byte[] sessionId; + private final String cypherSuite; + private final java.security.cert.Certificate peerCertificate; + private final X509Certificate certificate; + + /** + * + * @param sessionId The SSL session ID + * @param cypherSuite The cypher suite name + * @param certificate A string representation of the client certificate + * @throws java.security.cert.CertificateException If the client cert could not be decoded + * @throws CertificateException If the client cert could not be decoded + */ + public BasicSSLSessionInfo(byte[] sessionId, String cypherSuite, String certificate) throws java.security.cert.CertificateException, CertificateException { + this.sessionId = sessionId; + this.cypherSuite = cypherSuite; + + if (certificate != null) { + java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509"); + byte[] certificateBytes = certificate.getBytes(US_ASCII); + ByteArrayInputStream stream = new ByteArrayInputStream(certificateBytes); + peerCertificate = cf.generateCertificate(stream); + this.certificate = X509Certificate.getInstance(certificateBytes); + } else { + this.peerCertificate = null; + this.certificate = null; + } + } + /** + * + * @param sessionId The Base64 encoded SSL session ID + * @param cypherSuite The cypher suite name + * @param certificate A string representation of the client certificate + * @throws java.security.cert.CertificateException If the client cert could not be decoded + * @throws CertificateException If the client cert could not be decoded + */ + public BasicSSLSessionInfo(String sessionId, String cypherSuite, String certificate) throws java.security.cert.CertificateException, CertificateException { + this(sessionId == null ? null : base64Decode(sessionId), cypherSuite, certificate); + } + + @Override + public byte[] getSessionId() { + if(sessionId == null) { + return null; + } + final byte[] copy = new byte[sessionId.length]; + System.arraycopy(sessionId, 0, copy, 0, copy.length); + return copy; + } + + @Override + public String getCipherSuite() { + return cypherSuite; + } + + @Override + public java.security.cert.Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException { + if (certificate == null) { + throw UndertowMessages.MESSAGES.peerUnverified(); + } + return new Certificate[]{peerCertificate}; + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException { + if (certificate == null) { + throw UndertowMessages.MESSAGES.peerUnverified(); + } + return new X509Certificate[]{certificate}; + } + + @Override + public void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClientAuthMode) throws IOException { + throw UndertowMessages.MESSAGES.renegotiationNotSupported(); + } + + + private static byte[] base64Decode(String sessionId) { + try { + ByteBuffer sessionIdBuffer = FlexBase64.decode(sessionId); + byte[] sessionIdData; + if (sessionIdBuffer.hasArray()) { + sessionIdData = sessionIdBuffer.array(); + } else { + sessionIdData = new byte[sessionIdBuffer.remaining()]; + sessionIdBuffer.get(sessionIdData); + } + return sessionIdData; + } catch (IOException e) { + throw new RuntimeException(e); //won't happen + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/BlockingHttpExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/BlockingHttpExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/BlockingHttpExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import io.undertow.io.Sender; + + +/** + * An interface that provides the input and output streams for blocking HTTP requests. + * + * @author Stuart Douglas + */ +public interface BlockingHttpExchange extends Closeable { + + /** + * Returns the input stream that is in use for this exchange. + * + * @return The input stream + */ + InputStream getInputStream(); + + /** + * Returns the output stream that is in use for this exchange. + * + * In some circumstances this may not be available, such as if a writer + * is being used for a servlet response + * + * @return The output stream + */ + OutputStream getOutputStream(); + + /** + * Returns a sender based on the provided output stream + * + * @return A sender that uses the output stream + */ + Sender getSender(); + + /** + * Closes both the input and output streams + */ + void close() throws IOException; +} Index: 3rdParty_sources/undertow/io/undertow/server/ConduitWrapper.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/ConduitWrapper.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/ConduitWrapper.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.Conduit; + +/** + * Interface that provides a means of wrapping a {@link Conduit}. Every conduit wrapper has a chance + * to replace the conduit with a conduit which either wraps or replaces the passed in conduit. However it is the responsibility + * of either the conduit wrapper instance or the conduit it creates to ensure that the original conduit is eventually + * cleaned up and shut down properly when the request is terminated. + * + * @author Stuart Douglas + */ +public interface ConduitWrapper { + + /** + * Wrap the conduit. The wrapper should not return {@code null}. If no wrapping is desired, the original + * conduit should be returned. + * + * @param factory the original conduit + * @param exchange the in-flight HTTP exchange + * @return the replacement conduit + */ + T wrap(final ConduitFactory factory, final HttpServerExchange exchange); +} Index: 3rdParty_sources/undertow/io/undertow/server/ConnectionSSLSessionInfo.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/ConnectionSSLSessionInfo.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/ConnectionSSLSessionInfo.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,206 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.UndertowOptions; +import io.undertow.server.protocol.http.HttpServerConnection; +import org.xnio.ChannelListener; +import org.xnio.Options; +import org.xnio.Pooled; +import org.xnio.SslClientAuthMode; +import org.xnio.channels.Channels; +import org.xnio.channels.SslChannel; +import org.xnio.channels.StreamSourceChannel; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.X509Certificate; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.cert.Certificate; + +/** + * SSL session information that is read directly from the SSL session of the + * XNIO connection + * + * @author Stuart Douglas + */ +public class ConnectionSSLSessionInfo implements SSLSessionInfo { + + private final SslChannel channel; + private final HttpServerConnection serverConnection; + + public ConnectionSSLSessionInfo(SslChannel channel, HttpServerConnection serverConnection) { + this.channel = channel; + this.serverConnection = serverConnection; + } + + @Override + public byte[] getSessionId() { + return channel.getSslSession().getId(); + } + + @Override + public String getCipherSuite() { + return channel.getSslSession().getCipherSuite(); + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException, RenegotiationRequiredException { + try { + return channel.getSslSession().getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + throw new RenegotiationRequiredException(); + } + } catch (IOException e1) { + //ignore, will not actually happen + } + throw e; + } + } + + + @Override + public void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClientAuthMode) throws IOException { + if (exchange.isRequestComplete()) { + renegotiateNoRequest(exchange, sslClientAuthMode); + } else { + renegotiateBufferRequest(exchange, sslClientAuthMode); + } + } + + public void renegotiateBufferRequest(HttpServerExchange exchange, SslClientAuthMode newAuthMode) throws IOException { + int maxSize = exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_BUFFERED_REQUEST_SIZE, 16384); + if (maxSize <= 0) { + throw new SSLPeerUnverifiedException(""); + } + + //first we need to read the request + boolean requestResetRequired = false; + StreamSourceChannel requestChannel = Connectors.getExistingRequestChannel(exchange); + if (requestChannel == null) { + requestChannel = exchange.getRequestChannel(); + requestResetRequired = true; + } + + Pooled pooled = exchange.getConnection().getBufferPool().allocate(); + boolean free = true; //if the pooled buffer should be freed + int usedBuffers = 0; + Pooled[] poolArray = null; + final int bufferSize = pooled.getResource().remaining(); + int allowedBuffers = ((maxSize + bufferSize - 1) / bufferSize); + poolArray = new Pooled[allowedBuffers]; + poolArray[usedBuffers++] = pooled; + try { + int res; + do { + final ByteBuffer buf = pooled.getResource(); + res = Channels.readBlocking(requestChannel, buf); + if (!buf.hasRemaining()) { + if (usedBuffers == allowedBuffers) { + throw new SSLPeerUnverifiedException(""); + } else { + buf.flip(); + pooled = exchange.getConnection().getBufferPool().allocate(); + poolArray[usedBuffers++] = pooled; + } + } + } while (res != -1); + free = false; + pooled.getResource().flip(); + Connectors.ungetRequestBytes(exchange, poolArray); + renegotiateNoRequest(exchange, newAuthMode); + } finally { + if (free) { + for(Pooled buf : poolArray) { + if(buf != null) { + buf.free(); + } + } + } + if(requestResetRequired) { + exchange.requestChannel = null; + } + } + } + + public void renegotiateNoRequest(HttpServerExchange exchange, SslClientAuthMode newAuthMode) throws IOException { + AbstractServerConnection.ConduitState oldState = serverConnection.resetChannel(); + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + SslHandshakeWaiter waiter = new SslHandshakeWaiter(); + channel.getHandshakeSetter().set(waiter); + //we use requested, to place nicely with other auth modes + channel.setOption(Options.SSL_CLIENT_AUTH_MODE, newAuthMode); + channel.getSslSession().invalidate(); + channel.startHandshake(); + ByteBuffer buff = ByteBuffer.wrap(new byte[1]); + while (!waiter.isDone() && serverConnection.isOpen()) { + int read = serverConnection.getSourceChannel().read(buff); + if (read != 0) { + throw new SSLPeerUnverifiedException(""); + } + if (!waiter.isDone()) { + serverConnection.getSourceChannel().awaitReadable(); + } + } + } + } finally { + if (oldState != null) { + serverConnection.restoreChannel(oldState); + } + } + } + + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException, RenegotiationRequiredException { + try { + return channel.getSslSession().getPeerCertificateChain(); + } catch (SSLPeerUnverifiedException e) { + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + throw new RenegotiationRequiredException(); + } + } catch (IOException e1) { + //ignore, will not actually happen + } + throw e; + } + } + + private static class SslHandshakeWaiter implements ChannelListener { + + private volatile boolean done = false; + + boolean isDone() { + return done; + } + + @Override + public void handleEvent(SslChannel channel) { + done = true; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/Connectors.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/Connectors.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/Connectors.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,233 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.UndertowLogger; +import io.undertow.server.handlers.Cookie; +import io.undertow.util.DateUtils; +import io.undertow.util.Headers; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; + +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.Map; +import java.util.concurrent.Executor; + +/** + * This class provides the connector part of the {@link HttpServerExchange} API. + *

+ * It contains methods that logically belong on the exchange, however should only be used + * by connector implementations. + * + * @author Stuart Douglas + */ +public class Connectors { + + + /** + * Flattens the exchange cookie map into the response header map. This should be called by a + * connector just before the response is started. + * + * @param exchange The server exchange + */ + public static void flattenCookies(final HttpServerExchange exchange) { + Map cookies = exchange.getResponseCookiesInternal(); + if (cookies != null) { + for (Map.Entry entry : cookies.entrySet()) { + exchange.getResponseHeaders().add(Headers.SET_COOKIE, getCookieString(entry.getValue())); + } + } + } + + /** + * Attached buffered data to the exchange. The will generally be used to allow data to be re-read. + * + * @param exchange The HTTP server exchange + * @param buffers The buffers to attach + */ + public static void ungetRequestBytes(final HttpServerExchange exchange, Pooled... buffers) { + Pooled[] existing = exchange.getAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA); + Pooled[] newArray; + if (existing == null) { + newArray = new Pooled[buffers.length]; + System.arraycopy(buffers, 0, newArray, 0, buffers.length); + } else { + newArray = new Pooled[existing.length + buffers.length]; + System.arraycopy(existing, 0, newArray, 0, existing.length); + System.arraycopy(buffers, 0, newArray, existing.length, buffers.length); + } + exchange.putAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA, newArray); //todo: force some kind of wakeup? + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + Pooled[] bufs = exchange.getAttachment(HttpServerExchange.BUFFERED_REQUEST_DATA); + if (bufs != null) { + for (Pooled i : bufs) { + if(i != null) { + i.free(); + } + } + } + nextListener.proceed(); + } + }); + } + + public static void terminateRequest(final HttpServerExchange exchange) { + exchange.terminateRequest(); + } + + public static void terminateResponse(final HttpServerExchange exchange) { + exchange.terminateResponse(); + } + + private static String getCookieString(final Cookie cookie) { + switch (cookie.getVersion()) { + case 0: + return addVersion0ResponseCookieToExchange(cookie); + case 1: + default: + return addVersion1ResponseCookieToExchange(cookie); + } + } + + public static void setRequestStartTime(HttpServerExchange exchange) { + exchange.setRequestStartTime(System.nanoTime()); + } + + private static String addVersion0ResponseCookieToExchange(final Cookie cookie) { + final StringBuilder header = new StringBuilder(cookie.getName()); + header.append("="); + header.append(cookie.getValue()); + + if (cookie.getPath() != null) { + header.append("; path="); + header.append(cookie.getPath()); + } + if (cookie.getDomain() != null) { + header.append("; domain="); + header.append(cookie.getDomain()); + } + if (cookie.isSecure()) { + header.append("; secure"); + } + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + if (cookie.getExpires() != null) { + header.append("; Expires="); + header.append(DateUtils.toOldCookieDateString(cookie.getExpires())); + } else if (cookie.getMaxAge() != null) { + if (cookie.getMaxAge() >= 0) { + header.append("; Max-Age="); + header.append(cookie.getMaxAge()); + } + if (cookie.getMaxAge() == 0) { + Date expires = new Date(); + expires.setTime(0); + header.append("; Expires="); + header.append(DateUtils.toOldCookieDateString(expires)); + } else if (cookie.getMaxAge() > 0) { + Date expires = new Date(); + expires.setTime(expires.getTime() + cookie.getMaxAge() * 1000L); + header.append("; Expires="); + header.append(DateUtils.toOldCookieDateString(expires)); + } + } + return header.toString(); + + } + + private static String addVersion1ResponseCookieToExchange(final Cookie cookie) { + + final StringBuilder header = new StringBuilder(cookie.getName()); + header.append("="); + header.append(cookie.getValue()); + header.append("; Version=1"); + if (cookie.getPath() != null) { + header.append("; Path="); + header.append(cookie.getPath()); + } + if (cookie.getDomain() != null) { + header.append("; Domain="); + header.append(cookie.getDomain()); + } + if (cookie.isDiscard()) { + header.append("; Discard"); + } + if (cookie.isSecure()) { + header.append("; Secure"); + } + if (cookie.isHttpOnly()) { + header.append("; HttpOnly"); + } + if (cookie.getMaxAge() != null) { + if (cookie.getMaxAge() >= 0) { + header.append("; Max-Age="); + header.append(cookie.getMaxAge()); + } + } + if (cookie.getExpires() != null) { + header.append("; Expires="); + header.append(DateUtils.toDateString(cookie.getExpires())); + } + return header.toString(); + } + + public static void executeRootHandler(final HttpHandler handler, final HttpServerExchange exchange) { + try { + exchange.setInCall(true); + handler.handleRequest(exchange); + exchange.setInCall(false); + boolean resumed = exchange.runResumeReadWrite(); + if (exchange.isDispatched()) { + if (resumed) { + throw new RuntimeException("resumed and dispatched"); + } + final Runnable dispatchTask = exchange.getDispatchTask(); + Executor executor = exchange.getDispatchExecutor(); + exchange.setDispatchExecutor(null); + exchange.unDispatch(); + if (dispatchTask != null) { + executor = executor == null ? exchange.getConnection().getWorker() : executor; + executor.execute(dispatchTask); + } + } else if (!resumed) { + exchange.endExchange(); + } + } catch (Throwable t) { + exchange.setInCall(false); + if (!exchange.isResponseStarted()) { + exchange.setResponseCode(500); + } + UndertowLogger.REQUEST_LOGGER.errorf(t, "Undertow request failed %s", exchange); + exchange.endExchange(); + } + } + + /** + * Returns the existing request channel, if it exists. Otherwise returns null + * + * @param exchange The http server exchange + */ + public static StreamSourceChannel getExistingRequestChannel(final HttpServerExchange exchange) { + return exchange.requestChannel; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/DefaultResponseListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/DefaultResponseListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/DefaultResponseListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,35 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +/** + * Listener interface for default response handlers. These are handlers that generate default content + * such as error pages. + * + * @author Stuart Douglas + */ +public interface DefaultResponseListener { + + /** + * + * @param exchange The exchange + * @return true if this listener is generating a default response. + */ + boolean handleDefaultResponse(final HttpServerExchange exchange); +} Index: 3rdParty_sources/undertow/io/undertow/server/ExchangeCompletionListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/ExchangeCompletionListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/ExchangeCompletionListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +/** + * Listener interface for events that are run at the completion of a request/response + * cycle (i.e. when the request has been completely read, and the response has been fully written). + * + * At this point it is to late to modify the exchange further. + * + * Completion listeners are invoked in reverse order, + * + * @author Stuart Douglas + */ +public interface ExchangeCompletionListener { + + void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener); + + interface NextListener { + + void proceed(); + + } +} Index: 3rdParty_sources/undertow/io/undertow/server/HandlerWrapper.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/HandlerWrapper.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/HandlerWrapper.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,30 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +/** + * Interface that can be used to wrap the handler chains, adding additional handlers. + * + * @author Stuart Douglas + */ +public interface HandlerWrapper { + + HttpHandler wrap(HttpHandler handler); + +} Index: 3rdParty_sources/undertow/io/undertow/server/HttpHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/HttpHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/HttpHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +/** + * A handler for an HTTP request. The request handler must eventually either call another handler or end the exchange. + * + * + * @author David M. Lloyd + */ +public interface HttpHandler { + + /** + * Handle the request. + * + * @param exchange the HTTP request/response exchange + * + */ + void handleRequest(HttpServerExchange exchange) throws Exception; +} Index: 3rdParty_sources/undertow/io/undertow/server/HttpServerExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/HttpServerExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/HttpServerExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,2100 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.channels.DetachableStreamSinkChannel; +import io.undertow.channels.DetachableStreamSourceChannel; +import io.undertow.conduits.EmptyStreamSourceConduit; +import io.undertow.io.AsyncSenderImpl; +import io.undertow.io.BlockingSenderImpl; +import io.undertow.io.Sender; +import io.undertow.io.UndertowInputStream; +import io.undertow.io.UndertowOutputStream; +import io.undertow.security.api.SecurityContext; +import io.undertow.server.handlers.Cookie; +import io.undertow.util.AbstractAttachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.ConduitFactory; +import io.undertow.util.Cookies; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.NetworkUtils; +import io.undertow.util.Protocols; +import org.jboss.logging.Logger; +import org.xnio.Buffers; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.XnioIoThread; +import org.xnio.channels.Channels; +import org.xnio.channels.Configurable; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.Conduit; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.FileChannel; +import java.security.AccessController; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; + +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreClear; +import static org.xnio.Bits.anyAreSet; +import static org.xnio.Bits.intBitMask; + +/** + * An HTTP server request/response exchange. An instance of this class is constructed as soon as the request headers are + * fully parsed. + * + * @author David M. Lloyd + */ +public final class HttpServerExchange extends AbstractAttachable { + + // immutable state + + private static final Logger log = Logger.getLogger(HttpServerExchange.class); + + private static final RuntimePermission SET_SECURITY_CONTEXT = new RuntimePermission("io.undertow.SET_SECURITY_CONTEXT"); + private static final String ISO_8859_1 = "ISO-8859-1"; + + /** + * The attachment key that buffered request data is attached under. + */ + static final AttachmentKey[]> BUFFERED_REQUEST_DATA = AttachmentKey.create(Pooled[].class); + + private final ServerConnection connection; + private final HeaderMap requestHeaders; + private final HeaderMap responseHeaders; + + private int exchangeCompletionListenersCount = 0; + private ExchangeCompletionListener[] exchangeCompleteListeners; + private DefaultResponseListener[] defaultResponseListeners; + + private Map> queryParameters; + private Map> pathParameters; + + private Map requestCookies; + private Map responseCookies; + + /** + * The actual response channel. May be null if it has not been created yet. + */ + private WriteDispatchChannel responseChannel; + /** + * The actual request channel. May be null if it has not been created yet. + */ + protected ReadDispatchChannel requestChannel; + + private BlockingHttpExchange blockingHttpExchange; + + private HttpString protocol; + + /** + * The security context + */ + private SecurityContext securityContext; + + // mutable state + + private int state = 200; + private HttpString requestMethod; + private String requestScheme; + + /** + * The original request URI. This will include the host name if it was specified by the client. + *

+ * This is not decoded in any way, and does not include the query string. + *

+ * Examples: + * GET http://localhost:8080/myFile.jsf?foo=bar HTTP/1.1 -> 'http://localhost:8080/myFile.jsf' + * POST /my+File.jsf?foo=bar HTTP/1.1 -> '/my+File.jsf' + */ + private String requestURI; + + /** + * The request path. This will be decoded by the server, and does not include the query string. + *

+ * This path is not canonicalised, so care must be taken to ensure that escape attacks are not possible. + *

+ * Examples: + * GET http://localhost:8080/b/../my+File.jsf?foo=bar HTTP/1.1 -> '/b/../my+File.jsf' + * POST /my+File.jsf?foo=bar HTTP/1.1 -> '/my File.jsf' + */ + private String requestPath; + + /** + * The remaining unresolved portion of request path. If a {@link io.undertow.server.handlers.CanonicalPathHandler} is + * installed this will be canonicalised. + *

+ * Initially this will be equal to {@link #requestPath}, however it will be modified as handlers resolve the path. + */ + private String relativePath; + + /** + * The resolved part of the canonical path. + */ + private String resolvedPath = ""; + + /** + * the query string + */ + private String queryString = ""; + + private int requestWrapperCount = 0; + private ConduitWrapper[] requestWrappers; //we don't allocate these by default, as for get requests they are not used + + private int responseWrapperCount = 0; + private ConduitWrapper[] responseWrappers; + + private Sender sender; + + private long requestStartTime = -1; + + + /** + * The maximum entity size. This can be modified before the request stream is obtained, however once the request + * stream is obtained this cannot be modified further. + *

+ * The default value for this is determined by the {@link io.undertow.UndertowOptions#MAX_ENTITY_SIZE} option. A value + * of 0 indicates that this is unbounded. + *

+ * If this entity size is exceeded the request channel will be forcibly closed. + *

+ * TODO: integrate this with HTTP 100-continue responses, to make it possible to send a 417 rather than just forcibly + * closing the channel. + * + * @see io.undertow.UndertowOptions#MAX_ENTITY_SIZE + */ + private long maxEntitySize; + + /** + * When the call stack return this task will be executed by the executor specified in {@link #dispatchExecutor}. + * If the executor is null then it will be executed by the XNIO worker. + */ + private Runnable dispatchTask; + + /** + * The executor that is to be used to dispatch the {@link #dispatchTask}. Note that this is not cleared + * between dispatches, so once a request has been dispatched once then all subsequent dispatches will use + * the same executor. + */ + private Executor dispatchExecutor; + + + private static final int MASK_RESPONSE_CODE = intBitMask(0, 9); + + /** + * Flag that is set when the response sending begins + */ + private static final int FLAG_RESPONSE_SENT = 1 << 10; + + /** + * Flag that is sent when the response has been fully written and flushed. + */ + private static final int FLAG_RESPONSE_TERMINATED = 1 << 11; + + /** + * Flag that is set once the request has been fully read. For zero + * length requests this is set immediately. + */ + private static final int FLAG_REQUEST_TERMINATED = 1 << 12; + + /** + * Flag that is set if this is a persistent connection, and the + * connection should be re-used. + */ + private static final int FLAG_PERSISTENT = 1 << 14; + + /** + * If this flag is set it means that the request has been dispatched, + * and will not be ending when the call stack returns. + *

+ * This could be because it is being dispatched to a worker thread from + * an IO thread, or because resume(Reads/Writes) has been called. + */ + private static final int FLAG_DISPATCHED = 1 << 15; + + /** + * Flag that is set if the {@link #requestURI} field contains the hostname. + */ + private static final int FLAG_URI_CONTAINS_HOST = 1 << 16; + + /** + * If this flag is set then the request is current running through a + * handler chain. + *

+ * This will be true most of the time, this only time this will return + * false is when performing async operations outside the scope of a call to + * {@link Connectors#executeRootHandler(HttpHandler, HttpServerExchange)}, + * such as when performing async IO. + *

+ * If this is true then when the call stack returns the exchange will either be dispatched, + * or the exchange will be ended. + */ + private static final int FLAG_IN_CALL = 1 << 17; + private static final int FLAG_SHOULD_RESUME_READS = 1 << 18; + private static final int FLAG_SHOLD_RESUME_WRITES = 1 << 19; + + /** + * The source address for the request. If this is null then the actual source address from the channel is used + */ + private InetSocketAddress sourceAddress; + + /** + * The destination address for the request. If this is null then the actual source address from the channel is used + */ + private InetSocketAddress destinationAddress; + + public HttpServerExchange(final ServerConnection connection, long maxEntitySize) { + this(connection, new HeaderMap(), new HeaderMap(), maxEntitySize); + } + + public HttpServerExchange(final ServerConnection connection) { + this(connection, 0); + } + + public HttpServerExchange(final ServerConnection connection, final HeaderMap requestHeaders, final HeaderMap responseHeaders, long maxEntitySize) { + this.connection = connection; + this.maxEntitySize = maxEntitySize; + this.requestHeaders = requestHeaders; + this.responseHeaders = responseHeaders; + } + + /** + * Get the request protocol string. Normally this is one of the strings listed in {@link Protocols}. + * + * @return the request protocol string + */ + public HttpString getProtocol() { + return protocol; + } + + /** + * Sets the http protocol + * + * @param protocol + */ + public HttpServerExchange setProtocol(final HttpString protocol) { + this.protocol = protocol; + return this; + } + + /** + * Determine whether this request conforms to HTTP 0.9. + * + * @return {@code true} if the request protocol is equal to {@link Protocols#HTTP_0_9}, {@code false} otherwise + */ + public boolean isHttp09() { + return protocol.equals(Protocols.HTTP_0_9); + } + + /** + * Determine whether this request conforms to HTTP 1.0. + * + * @return {@code true} if the request protocol is equal to {@link Protocols#HTTP_1_0}, {@code false} otherwise + */ + public boolean isHttp10() { + return protocol.equals(Protocols.HTTP_1_0); + } + + /** + * Determine whether this request conforms to HTTP 1.1. + * + * @return {@code true} if the request protocol is equal to {@link Protocols#HTTP_1_1}, {@code false} otherwise + */ + public boolean isHttp11() { + return protocol.equals(Protocols.HTTP_1_1); + } + + /** + * Get the HTTP request method. Normally this is one of the strings listed in {@link io.undertow.util.Methods}. + * + * @return the HTTP request method + */ + public HttpString getRequestMethod() { + return requestMethod; + } + + /** + * Set the HTTP request method. + * + * @param requestMethod the HTTP request method + */ + public HttpServerExchange setRequestMethod(final HttpString requestMethod) { + this.requestMethod = requestMethod; + return this; + } + + /** + * Get the request URI scheme. Normally this is one of {@code http} or {@code https}. + * + * @return the request URI scheme + */ + public String getRequestScheme() { + return requestScheme; + } + + /** + * Set the request URI scheme. + * + * @param requestScheme the request URI scheme + */ + public HttpServerExchange setRequestScheme(final String requestScheme) { + this.requestScheme = requestScheme; + return this; + } + + /** + * The original request URI. This will include the host name, protocol etc + * if it was specified by the client. + *

+ * This is not decoded in any way, and does not include the query string. + *

+ * Examples: + * GET http://localhost:8080/myFile.jsf?foo=bar HTTP/1.1 -> 'http://localhost:8080/myFile.jsf' + * POST /my+File.jsf?foo=bar HTTP/1.1 -> '/my+File.jsf' + */ + public String getRequestURI() { + return requestURI; + } + + /** + * Sets the request URI + * + * @param requestURI The new request URI + */ + public HttpServerExchange setRequestURI(final String requestURI) { + this.requestURI = requestURI; + return this; + } + + /** + * Sets the request URI + * + * @param requestURI The new request URI + * @param containsHost If this is true the request URI contains the host part + */ + public HttpServerExchange setRequestURI(final String requestURI, boolean containsHost) { + this.requestURI = requestURI; + if (containsHost) { + this.state |= FLAG_URI_CONTAINS_HOST; + } else { + this.state &= ~FLAG_URI_CONTAINS_HOST; + } + return this; + } + + /** + * If a request was submitted to the server with a full URI instead of just a path this + * will return true. For example: + *

+ * GET http://localhost:8080/b/../my+File.jsf?foo=bar HTTP/1.1 -> true + * POST /my+File.jsf?foo=bar HTTP/1.1 -> false + * + * @return true If the request URI contains the host part of the URI + */ + public boolean isHostIncludedInRequestURI() { + return anyAreSet(state, FLAG_URI_CONTAINS_HOST); + } + + + /** + * The request path. This will be decoded by the server, and does not include the query string. + *

+ * This path is not canonicalised, so care must be taken to ensure that escape attacks are not possible. + *

+ * Examples: + * GET http://localhost:8080/b/../my+File.jsf?foo=bar HTTP/1.1 -> '/b/../my+File.jsf' + * POST /my+File.jsf?foo=bar HTTP/1.1 -> '/my File.jsf' + */ + public String getRequestPath() { + return requestPath; + } + + /** + * Set the request URI path. + * + * @param requestPath the request URI path + */ + public HttpServerExchange setRequestPath(final String requestPath) { + this.requestPath = requestPath; + return this; + } + + /** + * Get the request relative path. This is the path which should be evaluated by the current handler. + *

+ * If the {@link io.undertow.server.handlers.CanonicalPathHandler} is installed in the current chain + * then this path with be canonicalized + * + * @return the request relative path + */ + public String getRelativePath() { + return relativePath; + } + + /** + * Set the request relative path. + * + * @param relativePath the request relative path + */ + public HttpServerExchange setRelativePath(final String relativePath) { + this.relativePath = relativePath; + return this; + } + + /** + * Get the resolved path. + * + * @return the resolved path + */ + public String getResolvedPath() { + return resolvedPath; + } + + /** + * Set the resolved path. + * + * @param resolvedPath the resolved path + */ + public HttpServerExchange setResolvedPath(final String resolvedPath) { + this.resolvedPath = resolvedPath; + return this; + } + + /** + * + * @return The query string, without the leading ? + */ + public String getQueryString() { + return queryString; + } + + public HttpServerExchange setQueryString(final String queryString) { + this.queryString = queryString; + return this; + } + + /** + * Reconstructs the complete URL as seen by the user. This includes scheme, host name etc, + * but does not include query string. + *

+ * This is not decoded. + */ + public String getRequestURL() { + if (isHostIncludedInRequestURI()) { + return getRequestURI(); + } else { + return getRequestScheme() + "://" + getHostAndPort() + getRequestURI(); + } + } + + /** + * Returns the request charset. If none was explicitly specified it will return + * "ISO-8859-1", which is the default charset for HTTP requests. + * + * @return The character encoding + */ + public String getRequestCharset() { + return extractCharset(requestHeaders); + } + + /** + * Returns the response charset. If none was explicitly specified it will return + * "ISO-8859-1", which is the default charset for HTTP requests. + * + * @return The character encoding + */ + public String getResponseCharset() { + HeaderMap headers = responseHeaders; + return extractCharset(headers); + } + + private String extractCharset(HeaderMap headers) { + String contentType = headers.getFirst(Headers.CONTENT_TYPE); + if (contentType == null) { + return null; + } + String value = Headers.extractQuotedValueFromHeader(contentType, "charset"); + if(value != null) { + return value; + } + return ISO_8859_1; + } + + /** + * Return the host that this request was sent to, in general this will be the + * value of the Host header, minus the port specifier. + *

+ * If this resolves to an IPv6 address it will not be enclosed by square brackets. + * Care must be taken when constructing URLs based on this method to ensure IPv6 URLs + * are handled correctly. + * + * @return The host part of the destination address + */ + public String getHostName() { + String host = requestHeaders.getFirst(Headers.HOST); + if (host == null) { + host = getDestinationAddress().getAddress().getHostAddress(); + } else { + if (host.startsWith("[")) { + host = host.substring(1, host.indexOf(']')); + } else if (host.indexOf(':') != -1) { + host = host.substring(0, host.indexOf(':')); + } + } + return host; + } + + /** + * Return the host, and also the port if this request was sent to a non-standard port. In general + * this will just be the value of the Host header. + *

+ * If this resolves to an IPv6 address it *will* be enclosed by square brackets. The return + * value of this method is suitable for inclusion in a URL. + * + * @return The host and port part of the destination address + */ + public String getHostAndPort() { + String host = requestHeaders.getFirst(Headers.HOST); + if (host == null) { + host = NetworkUtils.formatPossibleIpv6Address(getDestinationAddress().getAddress().getHostAddress()); + int port = getDestinationAddress().getPort(); + if (!((getRequestScheme().equals("http") && port == 80) + || (getRequestScheme().equals("https") && port == 8080))) { + host = host + ":" + port; + } + } + return host; + } + + /** + * Return the port that this request was sent to. In general this will be the value of the Host + * header, minus the host name. + * + * @return The port part of the destination address + */ + public int getHostPort() { + String host = requestHeaders.getFirst(Headers.HOST); + if (host != null) { + //for ipv6 addresses we make sure we take out the first part, which can have multiple occurrences of : + final int colonIndex; + if (host.startsWith("[")) { + colonIndex = host.indexOf(':', host.indexOf(']')); + } else { + colonIndex = host.indexOf(':'); + } + if (colonIndex != -1) { + return Integer.parseInt(host.substring(colonIndex + 1)); + } else { + if (getRequestScheme().equals("https")) { + return 443; + } else if (getRequestScheme().equals("http")) { + return 80; + } + } + } + return getDestinationAddress().getPort(); + } + + /** + * Get the underlying HTTP connection. + * + * @return the underlying HTTP connection + */ + public ServerConnection getConnection() { + return connection; + } + + public boolean isPersistent() { + return anyAreSet(state, FLAG_PERSISTENT); + } + + public boolean isInIoThread() { + return getIoThread() == Thread.currentThread(); + } + + public boolean isUpgrade() { + return getResponseCode() == 101; + } + + public HttpServerExchange setPersistent(final boolean persistent) { + if (persistent) { + this.state = this.state | FLAG_PERSISTENT; + } else { + this.state = this.state & ~FLAG_PERSISTENT; + } + return this; + } + + public boolean isDispatched() { + return anyAreSet(state, FLAG_DISPATCHED); + } + + public HttpServerExchange unDispatch() { + state &= ~FLAG_DISPATCHED; + dispatchTask = null; + return this; + } + + /** + * + */ + public HttpServerExchange dispatch() { + state |= FLAG_DISPATCHED; + return this; + } + + /** + * Dispatches this request to the XNIO worker thread pool. Once the call stack returns + * the given runnable will be submitted to the executor. + *

+ * In general handlers should first check the value of {@link #isInIoThread()} before + * calling this method, and only dispatch if the request is actually running in the IO + * thread. + * + * @param runnable The task to run + * @throws IllegalStateException If this exchange has already been dispatched + */ + public HttpServerExchange dispatch(final Runnable runnable) { + dispatch(null, runnable); + return this; + } + + /** + * Dispatches this request to the given executor. Once the call stack returns + * the given runnable will be submitted to the executor. + *

+ * In general handlers should first check the value of {@link #isInIoThread()} before + * calling this method, and only dispatch if the request is actually running in the IO + * thread. + * + * @param runnable The task to run + * @throws IllegalStateException If this exchange has already been dispatched + */ + public HttpServerExchange dispatch(final Executor executor, final Runnable runnable) { + if (executor != null) { + this.dispatchExecutor = executor; + } + if (isInCall()) { + state |= FLAG_DISPATCHED; + this.dispatchTask = runnable; + } else { + if (executor == null) { + getConnection().getWorker().execute(runnable); + } else { + executor.execute(runnable); + } + } + return this; + } + + public HttpServerExchange dispatch(final HttpHandler handler) { + dispatch(null, handler); + return this; + } + + public HttpServerExchange dispatch(final Executor executor, final HttpHandler handler) { + final Runnable runnable = new Runnable() { + @Override + public void run() { + Connectors.executeRootHandler(handler, HttpServerExchange.this); + } + }; + dispatch(executor, runnable); + return this; + } + + /** + * Sets the executor that is used for dispatch operations where no executor is specified. + * + * @param executor The executor to use + */ + public HttpServerExchange setDispatchExecutor(final Executor executor) { + if (executor == null) { + dispatchExecutor = null; + } else { + dispatchExecutor = executor; + } + return this; + } + + /** + * Gets the current executor that is used for dispatch operations. This may be null + * + * @return The current dispatch executor + */ + public Executor getDispatchExecutor() { + return dispatchExecutor; + } + + /** + * @return The current dispatch task + */ + Runnable getDispatchTask() { + return dispatchTask; + } + + boolean isInCall() { + return anyAreSet(state, FLAG_IN_CALL); + } + + HttpServerExchange setInCall(boolean value) { + if (value) { + state |= FLAG_IN_CALL; + } else { + state &= ~FLAG_IN_CALL; + } + return this; + } + + + /** + * Upgrade the channel to a raw socket. This method set the response code to 101, and then marks both the + * request and response as terminated, which means that once the current request is completed the raw channel + * can be obtained from {@link io.undertow.server.protocol.http.HttpServerConnection#getChannel()} + * + * @throws IllegalStateException if a response or upgrade was already sent, or if the request body is already being + * read + */ + public HttpServerExchange upgradeChannel(final HttpUpgradeListener listener) { + if (!connection.isUpgradeSupported()) { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + connection.setUpgradeListener(listener); + setResponseCode(101); + getResponseHeaders().put(Headers.CONNECTION, Headers.UPGRADE_STRING); + return this; + } + + /** + * Upgrade the channel to a raw socket. This method set the response code to 101, and then marks both the + * request and response as terminated, which means that once the current request is completed the raw channel + * can be obtained from {@link io.undertow.server.protocol.http.HttpServerConnection#getChannel()} + * + * @param productName the product name to report to the client + * @throws IllegalStateException if a response or upgrade was already sent, or if the request body is already being + * read + */ + public HttpServerExchange upgradeChannel(String productName, final HttpUpgradeListener listener) { + if (!connection.isUpgradeSupported()) { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + connection.setUpgradeListener(listener); + setResponseCode(101); + final HeaderMap headers = getResponseHeaders(); + headers.put(Headers.UPGRADE, productName); + headers.put(Headers.CONNECTION, Headers.UPGRADE_STRING); + return this; + } + + public HttpServerExchange addExchangeCompleteListener(final ExchangeCompletionListener listener) { + final int exchangeCompletionListenersCount = this.exchangeCompletionListenersCount++; + ExchangeCompletionListener[] exchangeCompleteListeners = this.exchangeCompleteListeners; + if (exchangeCompleteListeners == null || exchangeCompleteListeners.length == exchangeCompletionListenersCount) { + ExchangeCompletionListener[] old = exchangeCompleteListeners; + this.exchangeCompleteListeners = exchangeCompleteListeners = new ExchangeCompletionListener[exchangeCompletionListenersCount + 2]; + if(old != null) { + System.arraycopy(old, 0, exchangeCompleteListeners, 0, exchangeCompletionListenersCount); + } + } + exchangeCompleteListeners[exchangeCompletionListenersCount] = listener; + return this; + } + + public HttpServerExchange addDefaultResponseListener(final DefaultResponseListener listener) { + int i = 0; + if(defaultResponseListeners == null) { + defaultResponseListeners = new DefaultResponseListener[2]; + } else { + while (i != defaultResponseListeners.length && defaultResponseListeners[i] != null) { + ++i; + } + if (i == defaultResponseListeners.length) { + DefaultResponseListener[] old = defaultResponseListeners; + defaultResponseListeners = new DefaultResponseListener[defaultResponseListeners.length + 2]; + System.arraycopy(old, 0, defaultResponseListeners, 0, old.length); + } + } + defaultResponseListeners[i] = listener; + return this; + } + + /** + * Get the source address of the HTTP request. + * + * @return the source address of the HTTP request + */ + public InetSocketAddress getSourceAddress() { + if (sourceAddress != null) { + return sourceAddress; + } + return connection.getPeerAddress(InetSocketAddress.class); + } + + /** + * Sets the source address of the HTTP request. If this is not explicitly set + * the actual source address of the channel is used. + * + * @param sourceAddress The address + */ + public HttpServerExchange setSourceAddress(InetSocketAddress sourceAddress) { + this.sourceAddress = sourceAddress; + return this; + } + + /** + * Get the source address of the HTTP request. + * + * @return the source address of the HTTP request + */ + public InetSocketAddress getDestinationAddress() { + if (destinationAddress != null) { + return destinationAddress; + } + return connection.getLocalAddress(InetSocketAddress.class); + } + + /** + * Sets the destination address of the HTTP request. If this is not explicitly set + * the actual destination address of the channel is used. + * + * @param destinationAddress The address + */ + public HttpServerExchange setDestinationAddress(InetSocketAddress destinationAddress) { + this.destinationAddress = destinationAddress; + return this; + } + + /** + * Get the request headers. + * + * @return the request headers + */ + public HeaderMap getRequestHeaders() { + return requestHeaders; + } + + /** + * @return The content length of the request, or -1 if it has not been set + */ + public long getRequestContentLength() { + String contentLengthString = requestHeaders.getFirst(Headers.CONTENT_LENGTH); + if (contentLengthString == null) { + return -1; + } + return Long.parseLong(contentLengthString); + } + + /** + * Get the response headers. + * + * @return the response headers + */ + public HeaderMap getResponseHeaders() { + return responseHeaders; + } + + /** + * @return The content length of the response, or -1 if it has not been set + */ + public long getResponseContentLength() { + String contentLengthString = responseHeaders.getFirst(Headers.CONTENT_LENGTH); + if (contentLengthString == null) { + return -1; + } + return Long.parseLong(contentLengthString); + } + + /** + * Sets the response content length + * + * @param length The content length + */ + public HttpServerExchange setResponseContentLength(long length) { + if (length == -1) { + responseHeaders.remove(Headers.CONTENT_LENGTH); + } else { + responseHeaders.put(Headers.CONTENT_LENGTH, Long.toString(length)); + } + return this; + } + + /** + * Returns a mutable map of query parameters. + * + * @return The query parameters + */ + public Map> getQueryParameters() { + if (queryParameters == null) { + queryParameters = new TreeMap<>(); + } + return queryParameters; + } + + public HttpServerExchange addQueryParam(final String name, final String param) { + if (queryParameters == null) { + queryParameters = new TreeMap<>(); + } + Deque list = queryParameters.get(name); + if (list == null) { + queryParameters.put(name, list = new ArrayDeque<>(2)); + } + list.add(param); + return this; + } + + + /** + * Returns a mutable map of path parameters + * + * @return The path parameters + */ + public Map> getPathParameters() { + if (pathParameters == null) { + pathParameters = new TreeMap<>(); + } + return pathParameters; + } + + public HttpServerExchange addPathParam(final String name, final String param) { + if (pathParameters == null) { + pathParameters = new TreeMap<>(); + } + Deque list = pathParameters.get(name); + if (list == null) { + pathParameters.put(name, list = new ArrayDeque<>(2)); + } + list.add(param); + return this; + } + + /** + * @return A mutable map of request cookies + */ + public Map getRequestCookies() { + if (requestCookies == null) { + requestCookies = Cookies.parseRequestCookies( + getConnection().getUndertowOptions().get(UndertowOptions.MAX_COOKIES, 200), + getConnection().getUndertowOptions().get(UndertowOptions.ALLOW_EQUALS_IN_COOKIE_VALUE, false), + requestHeaders.get(Headers.COOKIE)); + } + return requestCookies; + } + + /** + * Sets a response cookie + * + * @param cookie The cookie + */ + public HttpServerExchange setResponseCookie(final Cookie cookie) { + if (responseCookies == null) { + responseCookies = new TreeMap<>(); //hashmap is slow to allocate in JDK7 + } + responseCookies.put(cookie.getName(), cookie); + return this; + } + + /** + * @return A mutable map of response cookies + */ + public Map getResponseCookies() { + if (responseCookies == null) { + responseCookies = new TreeMap<>(); + } + return responseCookies; + } + + /** + * For internal use only + * + * @return The response cookies, or null if they have not been set yet + */ + Map getResponseCookiesInternal() { + return responseCookies; + } + + /** + * @return true If the response has already been started + */ + public boolean isResponseStarted() { + return allAreSet(state, FLAG_RESPONSE_SENT); + } + + /** + * Get the inbound request. If there is no request body, calling this method + * may cause the next request to immediately be processed. The {@link StreamSourceChannel#close()} or {@link StreamSourceChannel#shutdownReads()} + * method must be called at some point after the request is processed to prevent resource leakage and to allow + * the next request to proceed. Any unread content will be discarded. + * + * @return the channel for the inbound request, or {@code null} if another party already acquired the channel + */ + public StreamSourceChannel getRequestChannel() { + if (requestChannel != null) { + return null; + } + if (anyAreSet(state, FLAG_REQUEST_TERMINATED)) { + return requestChannel = new ReadDispatchChannel(new ConduitStreamSourceChannel(Configurable.EMPTY, new EmptyStreamSourceConduit(getIoThread()))); + } + final ConduitWrapper[] wrappers = this.requestWrappers; + final ConduitStreamSourceChannel sourceChannel = connection.getSourceChannel(); + if (wrappers != null) { + this.requestWrappers = null; + final WrapperConduitFactory factory = new WrapperConduitFactory<>(wrappers, requestWrapperCount, sourceChannel.getConduit(), this); + sourceChannel.setConduit(factory.create()); + } + return requestChannel = new ReadDispatchChannel(sourceChannel); + } + + public boolean isRequestChannelAvailable() { + return requestChannel == null; + } + + /** + * Returns true if the completion handler for this exchange has been invoked, and the request is considered + * finished. + */ + public boolean isComplete() { + return allAreSet(state, FLAG_REQUEST_TERMINATED | FLAG_RESPONSE_TERMINATED); + } + + /** + * Returns true if all data has been read from the request, or if there + * was not data. + * + * @return true if the request is complete + */ + public boolean isRequestComplete() { + return allAreSet(state, FLAG_REQUEST_TERMINATED); + } + + /** + * @return true if the responses is complete + */ + public boolean isResponseComplete() { + return allAreSet(state, FLAG_RESPONSE_TERMINATED); + } + + /** + * Force the codec to treat the request as fully read. Should only be invoked by handlers which downgrade + * the socket or implement a transfer coding. + */ + void terminateRequest() { + int oldVal = state; + if (allAreSet(oldVal, FLAG_REQUEST_TERMINATED)) { + // idempotent + return; + } + if (requestChannel != null) { + requestChannel.requestDone(); + } + this.state = oldVal | FLAG_REQUEST_TERMINATED; + if (anyAreSet(oldVal, FLAG_RESPONSE_TERMINATED)) { + invokeExchangeCompleteListeners(); + } + } + + private void invokeExchangeCompleteListeners() { + if (exchangeCompletionListenersCount > 0) { + int i = exchangeCompletionListenersCount - 1; + ExchangeCompletionListener next = exchangeCompleteListeners[i]; + exchangeCompletionListenersCount = -1; + next.exchangeEvent(this, new ExchangeCompleteNextListener(exchangeCompleteListeners, this, i)); + } else if (exchangeCompletionListenersCount == 0) { + exchangeCompletionListenersCount = -1; + connection.exchangeComplete(this); + } + } + + /** + * Get the response channel. The channel must be closed and fully flushed before the next response can be started. + * In order to close the channel you must first call {@link org.xnio.channels.StreamSinkChannel#shutdownWrites()}, + * and then call {@link org.xnio.channels.StreamSinkChannel#flush()} until it returns true. Alternatively you can + * call {@link #endExchange()}, which will close the channel as part of its cleanup. + *

+ * Closing a fixed-length response before the corresponding number of bytes has been written will cause the connection + * to be reset and subsequent requests to fail; thus it is important to ensure that the proper content length is + * delivered when one is specified. The response channel may not be writable until after the response headers have + * been sent. + *

+ * If this method is not called then an empty or default response body will be used, depending on the response code set. + *

+ * The returned channel will begin to write out headers when the first write request is initiated, or when + * {@link org.xnio.channels.StreamSinkChannel#shutdownWrites()} is called on the channel with no content being written. + * Once the channel is acquired, however, the response code and headers may not be modified. + *

+ * + * @return the response channel, or {@code null} if another party already acquired the channel + */ + public StreamSinkChannel getResponseChannel() { + if (responseChannel != null) { + return null; + } + final ConduitWrapper[] wrappers = responseWrappers; + this.responseWrappers = null; + final ConduitStreamSinkChannel sinkChannel = connection.getSinkChannel(); + if (sinkChannel == null) { + return null; + } + if(wrappers != null) { + final WrapperStreamSinkConduitFactory factory = new WrapperStreamSinkConduitFactory(wrappers, responseWrapperCount, this, sinkChannel.getConduit()); + sinkChannel.setConduit(factory.create()); + } else { + sinkChannel.setConduit(connection.getSinkConduit(this, sinkChannel.getConduit())); + } + this.responseChannel = new WriteDispatchChannel(sinkChannel); + this.startResponse(); + return responseChannel; + } + + /** + * Get the response sender. + *

+ * For blocking exchanges this will return a sender that uses the underlying output stream. + * + * @return the response sender, or {@code null} if another party already acquired the channel or the sender + * @see #getResponseChannel() + */ + public Sender getResponseSender() { + if (blockingHttpExchange != null) { + return blockingHttpExchange.getSender(); + } + if (sender != null) { + return sender; + } + return sender = new AsyncSenderImpl(this); + } + + /** + * @return true if {@link #getResponseChannel()} has not been called + */ + public boolean isResponseChannelAvailable() { + return responseChannel == null; + } + + /** + * Change the response code for this response. If not specified, the code will be a {@code 200}. Setting + * the response code after the response headers have been transmitted has no effect. + * + * @param responseCode the new code + * @throws IllegalStateException if a response or upgrade was already sent + */ + public HttpServerExchange setResponseCode(final int responseCode) { + if (responseCode < 0 || responseCode > 999) { + throw new IllegalArgumentException("Invalid response code"); + } + int oldVal = state; + if (allAreSet(oldVal, FLAG_RESPONSE_SENT)) { + throw UndertowMessages.MESSAGES.responseAlreadyStarted(); + } + this.state = oldVal & ~MASK_RESPONSE_CODE | responseCode & MASK_RESPONSE_CODE; + return this; + } + + /** + * Adds a {@link ConduitWrapper} to the request wrapper chain. + * + * @param wrapper the wrapper + */ + public HttpServerExchange addRequestWrapper(final ConduitWrapper wrapper) { + ConduitWrapper[] wrappers = requestWrappers; + if (requestChannel != null) { + throw UndertowMessages.MESSAGES.requestChannelAlreadyProvided(); + } + if (wrappers == null) { + wrappers = requestWrappers = new ConduitWrapper[2]; + } else if (wrappers.length == requestWrapperCount) { + requestWrappers = new ConduitWrapper[wrappers.length + 2]; + System.arraycopy(wrappers, 0, requestWrappers, 0, wrappers.length); + wrappers = requestWrappers; + } + wrappers[requestWrapperCount++] = wrapper; + return this; + } + + /** + * Adds a {@link ConduitWrapper} to the response wrapper chain. + * + * @param wrapper the wrapper + */ + public HttpServerExchange addResponseWrapper(final ConduitWrapper wrapper) { + ConduitWrapper[] wrappers = responseWrappers; + if (responseChannel != null) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + if(wrappers == null) { + this.responseWrappers = wrappers = new ConduitWrapper[2]; + } else if (wrappers.length == responseWrapperCount) { + responseWrappers = new ConduitWrapper[wrappers.length + 2]; + System.arraycopy(wrappers, 0, responseWrappers, 0, wrappers.length); + wrappers = responseWrappers; + } + wrappers[responseWrapperCount++] = wrapper; + return this; + } + + /** + * Calling this method puts the exchange in blocking mode, and creates a + * {@link BlockingHttpExchange} object to store the streams. + *

+ * When an exchange is in blocking mode the input stream methods become + * available, other than that there is presently no major difference + * between blocking an non-blocking modes. + * + * @return The existing blocking exchange, if any + */ + public BlockingHttpExchange startBlocking() { + final BlockingHttpExchange old = this.blockingHttpExchange; + blockingHttpExchange = new DefaultBlockingHttpExchange(this); + return old; + } + + /** + * Calling this method puts the exchange in blocking mode, using the given + * blocking exchange as the source of the streams. + *

+ * When an exchange is in blocking mode the input stream methods become + * available, other than that there is presently no major difference + * between blocking an non-blocking modes. + *

+ * Note that this method may be called multiple times with different + * exchange objects, to allow handlers to modify the streams + * that are being used. + * + * @return The existing blocking exchange, if any + */ + public BlockingHttpExchange startBlocking(final BlockingHttpExchange httpExchange) { + final BlockingHttpExchange old = this.blockingHttpExchange; + blockingHttpExchange = httpExchange; + return old; + } + + /** + * Returns true if {@link #startBlocking()} or {@link #startBlocking(BlockingHttpExchange)} has been called. + * + * @return true If this is a blocking HTTP server exchange + */ + public boolean isBlocking() { + return blockingHttpExchange != null; + } + + /** + * @return The input stream + * @throws IllegalStateException if {@link #startBlocking()} has not been called + */ + public InputStream getInputStream() { + if (blockingHttpExchange == null) { + throw UndertowMessages.MESSAGES.startBlockingHasNotBeenCalled(); + } + return blockingHttpExchange.getInputStream(); + } + + /** + * @return The output stream + * @throws IllegalStateException if {@link #startBlocking()} has not been called + */ + public OutputStream getOutputStream() { + if (blockingHttpExchange == null) { + throw UndertowMessages.MESSAGES.startBlockingHasNotBeenCalled(); + } + return blockingHttpExchange.getOutputStream(); + } + + /** + * Get the response code. + * + * @return the response code + */ + public int getResponseCode() { + return state & MASK_RESPONSE_CODE; + } + + /** + * Force the codec to treat the response as fully written. Should only be invoked by handlers which downgrade + * the socket or implement a transfer coding. + */ + HttpServerExchange terminateResponse() { + int oldVal = state; + if (allAreSet(oldVal, FLAG_RESPONSE_TERMINATED)) { + // idempotent + return this; + } + responseChannel.responseDone(); + this.state = oldVal | FLAG_RESPONSE_TERMINATED; + if (anyAreSet(oldVal, FLAG_REQUEST_TERMINATED)) { + invokeExchangeCompleteListeners(); + } + return this; + } + + /** + * + * @return The request start time, or -1 if this was not recorded + */ + public long getRequestStartTime() { + return requestStartTime; + } + + + HttpServerExchange setRequestStartTime(long requestStartTime) { + this.requestStartTime = requestStartTime; + return this; + } + + /** + * Ends the exchange by fully draining the request channel, and flushing the response channel. + *

+ * This can result in handoff to an XNIO worker, so after this method is called the exchange should + * not be modified by the caller. + *

+ * If the exchange is already complete this method is a noop + */ + public HttpServerExchange endExchange() { + final int state = this.state; + if (allAreSet(state, FLAG_REQUEST_TERMINATED | FLAG_RESPONSE_TERMINATED)) { + if(blockingHttpExchange != null) { + //we still have to close the blocking exchange in this case, + IoUtils.safeClose(blockingHttpExchange); + } + return this; + } + if(defaultResponseListeners != null) { + int i = defaultResponseListeners.length - 1; + while (i >= 0) { + DefaultResponseListener listener = defaultResponseListeners[i]; + if (listener != null) { + defaultResponseListeners[i] = null; + try { + if (listener.handleDefaultResponse(this)) { + return this; + } + } catch (Exception e) { + UndertowLogger.REQUEST_LOGGER.debug("Exception running default response listener", e); + } + } + i--; + } + } + + if (anyAreClear(state, FLAG_REQUEST_TERMINATED)) { + connection.terminateRequestChannel(this); + } + + if (blockingHttpExchange != null) { + try { + //TODO: can we end up in this situation in a IO thread? + blockingHttpExchange.close(); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(connection); + } + } + + //417 means that we are rejecting the request + //so the client should not actually send any data + if (anyAreClear(state, FLAG_REQUEST_TERMINATED)) { + + //not really sure what the best thing to do here is + //for now we are just going to drain the channel + if (requestChannel == null) { + getRequestChannel(); + } + int totalRead = 0; + for (; ; ) { + try { + long read = Channels.drain(requestChannel, Long.MAX_VALUE); + totalRead += read; + if (read == 0) { + //if the response code is 417 this is a rejected continuation request. + //however there is a chance the client could have sent the data anyway + //so we attempt to drain, and if we have not drained anything then we + //assume the server has not sent any data + + if (getResponseCode() != 417 || totalRead > 0) { + requestChannel.getReadSetter().set(ChannelListeners.drainListener(Long.MAX_VALUE, + new ChannelListener() { + @Override + public void handleEvent(final StreamSourceChannel channel) { + if (anyAreClear(state, FLAG_RESPONSE_TERMINATED)) { + closeAndFlushResponse(); + } + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(final StreamSourceChannel channel, final IOException e) { + + //make sure the listeners have been invoked + //unless the connection has been killed this is a no-op + invokeExchangeCompleteListeners(); + UndertowLogger.REQUEST_LOGGER.debug("Exception draining request stream", e); + IoUtils.safeClose(connection); + } + } + )); + requestChannel.resumeReads(); + return this; + } else { + break; + } + } else if (read == -1) { + break; + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(connection); + break; + } + + } + } + if (anyAreClear(state, FLAG_RESPONSE_TERMINATED)) { + closeAndFlushResponse(); + } + return this; + } + + private void closeAndFlushResponse() { + if(!connection.isOpen()) { + //not much point trying to flush + + //make sure the listeners have been invoked + invokeExchangeCompleteListeners(); + return; + } + try { + if (isResponseChannelAvailable()) { + getResponseHeaders().put(Headers.CONTENT_LENGTH, "0"); + getResponseChannel(); + } + responseChannel.shutdownWrites(); + if (!responseChannel.flush()) { + responseChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener( + new ChannelListener() { + @Override + public void handleEvent(final StreamSinkChannel channel) { + channel.suspendWrites(); + channel.getWriteSetter().set(null); + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(final Channel channel, final IOException exception) { + + //make sure the listeners have been invoked + invokeExchangeCompleteListeners(); + UndertowLogger.REQUEST_LOGGER.debug("Exception ending request", exception); + IoUtils.safeClose(connection); + } + } + )); + responseChannel.resumeWrites(); + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + + IoUtils.safeClose(connection); + } + } + + /** + * Transmit the response headers. After this method successfully returns, + * the response channel may become writable. + *

+ * If this method fails the request and response channels will be closed. + *

+ * This method runs asynchronously. If the channel is writable it will + * attempt to write as much of the response header as possible, and then + * queue the rest in a listener and return. + *

+ * If future handlers in the chain attempt to write before this is finished + * XNIO will just magically sort it out so it works. This is not actually + * implemented yet, so we just terminate the connection straight away at + * the moment. + *

+ * TODO: make this work properly + * + * @throws IllegalStateException if the response headers were already sent + */ + HttpServerExchange startResponse() throws IllegalStateException { + int oldVal = state; + if (allAreSet(oldVal, FLAG_RESPONSE_SENT)) { + throw UndertowMessages.MESSAGES.responseAlreadyStarted(); + } + this.state = oldVal | FLAG_RESPONSE_SENT; + + log.tracef("Starting to write response for %s", this); + return this; + } + + public XnioIoThread getIoThread() { + return connection.getIoThread(); + } + + /** + * @return The maximum entity size for this exchange + */ + public long getMaxEntitySize() { + return maxEntitySize; + } + + /** + * Sets the max entity size for this exchange. This cannot be modified after the request channel has been obtained. + * + * @param maxEntitySize The max entity size + */ + public HttpServerExchange setMaxEntitySize(final long maxEntitySize) { + if (!isRequestChannelAvailable()) { + throw UndertowMessages.MESSAGES.requestChannelAlreadyProvided(); + } + this.maxEntitySize = maxEntitySize; + connection.maxEntitySizeUpdated(this); + return this; + } + + public SecurityContext getSecurityContext() { + return securityContext; + } + + public void setSecurityContext(SecurityContext securityContext) { + if(System.getSecurityManager() != null) { + AccessController.checkPermission(SET_SECURITY_CONTEXT); + } + this.securityContext = securityContext; + } + + /** + * Actually resumes reads or writes, if the relevant method has been called. + * + * @return true if reads or writes were resumed + */ + boolean runResumeReadWrite() { + boolean ret = false; + if(anyAreSet(state, FLAG_SHOLD_RESUME_WRITES)) { + responseChannel.runResume(); + ret = true; + } + if(anyAreSet(state, FLAG_SHOULD_RESUME_READS)) { + requestChannel.runResume(); + ret = true; + } + state &= ~(FLAG_SHOULD_RESUME_READS | FLAG_SHOLD_RESUME_WRITES); + return ret; + } + + private static class ExchangeCompleteNextListener implements ExchangeCompletionListener.NextListener { + private final ExchangeCompletionListener[] list; + private final HttpServerExchange exchange; + private int i; + + public ExchangeCompleteNextListener(final ExchangeCompletionListener[] list, final HttpServerExchange exchange, int i) { + this.list = list; + this.exchange = exchange; + this.i = i; + } + + @Override + public void proceed() { + if (--i >= 0) { + final ExchangeCompletionListener next = list[i]; + next.exchangeEvent(exchange, this); + } else if(i == -1) { + exchange.connection.exchangeComplete(exchange); + } + } + } + + private static class DefaultBlockingHttpExchange implements BlockingHttpExchange { + + private InputStream inputStream; + private OutputStream outputStream; + private Sender sender; + private final HttpServerExchange exchange; + + DefaultBlockingHttpExchange(final HttpServerExchange exchange) { + this.exchange = exchange; + } + + public InputStream getInputStream() { + if (inputStream == null) { + inputStream = new UndertowInputStream(exchange); + } + return inputStream; + } + + public OutputStream getOutputStream() { + if (outputStream == null) { + outputStream = new UndertowOutputStream(exchange); + } + return outputStream; + } + + @Override + public Sender getSender() { + if (sender == null) { + sender = new BlockingSenderImpl(exchange, getOutputStream()); + } + return sender; + } + + @Override + public void close() throws IOException { + try { + getInputStream().close(); + } finally { + getOutputStream().close(); + } + } + } + + /** + * Channel implementation that is actually provided to clients of the exchange. + *

+ * We do not provide the underlying conduit channel, as this is shared between requests, so we need to make sure that after this request + * is done the the channel cannot affect the next request. + *

+ * It also delays a wakeup/resumesWrites calls until the current call stack has returned, thus ensuring that only 1 thread is + * active in the exchange at any one time. + */ + private class WriteDispatchChannel extends DetachableStreamSinkChannel implements StreamSinkChannel { + + private boolean wakeup; + + public WriteDispatchChannel(final ConduitStreamSinkChannel delegate) { + super(delegate); + } + + @Override + protected boolean isFinished() { + return allAreSet(state, FLAG_RESPONSE_TERMINATED); + } + + @Override + public void resumeWrites() { + if (isFinished()) { + return; + } + if (isInCall()) { + state |= FLAG_SHOLD_RESUME_WRITES; + } else { + delegate.resumeWrites(); + } + } + + @Override + public void wakeupWrites() { + if (isFinished()) { + return; + } + if (isInCall()) { + wakeup = true; + state |= FLAG_SHOLD_RESUME_WRITES; + } else { + delegate.wakeupWrites(); + } + } + + @Override + public boolean isWriteResumed() { + return anyAreSet(state, FLAG_SHOLD_RESUME_WRITES) || super.isWriteResumed(); + } + + public void runResume() { + if (!isFinished() && isWriteResumed()) { + if (wakeup) { + wakeup = false; + delegate.wakeupWrites(); + } else { + delegate.resumeWrites(); + } + } else if(wakeup) { + wakeup = false; + invokeListener(); + } + } + + private void invokeListener() { + getIoThread().execute(new Runnable() { + @Override + public void run() { + ChannelListeners.invokeChannelListener(WriteDispatchChannel.this, writeSetter.get()); + } + }); + } + + @Override + public void awaitWritable() throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + super.awaitWritable(); + } + + @Override + public void awaitWritable(long time, TimeUnit timeUnit) throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + super.awaitWritable(time, timeUnit); + } + } + + /** + * Channel implementation that is actually provided to clients of the exchange. We do not provide the underlying + * conduit channel, as this will become the next requests conduit channel, so if a thread is still hanging onto this + * exchange it can result in problems. + *

+ * It also delays a readResume call until the current call stack has returned, thus ensuring that only 1 thread is + * active in the exchange at any one time. + *

+ * It also handles buffered request data. + */ + private final class ReadDispatchChannel extends DetachableStreamSourceChannel implements StreamSourceChannel { + + private boolean wakeup = true; + private boolean readsResumed = false; + + + public ReadDispatchChannel(final ConduitStreamSourceChannel delegate) { + super(delegate); + } + + @Override + protected boolean isFinished() { + return allAreSet(state, FLAG_REQUEST_TERMINATED); + } + + @Override + public void resumeReads() { + readsResumed = true; + if (isFinished()) { + return; + } + if (isInCall()) { + state |= FLAG_SHOULD_RESUME_READS; + } else { + delegate.resumeReads(); + } + } + + public void wakeupReads() { + if (isInCall()) { + wakeup = true; + state |= FLAG_SHOULD_RESUME_READS; + } else { + if(isFinished()) { + invokeListener(); + } else { + delegate.wakeupReads(); + } + } + } + + private void invokeListener() { + getIoThread().execute(new Runnable() { + @Override + public void run() { + ChannelListeners.invokeChannelListener(ReadDispatchChannel.this, readSetter.get()); + } + }); + } + + public void requestDone() { + if(delegate instanceof ConduitStreamSourceChannel) { + ((ConduitStreamSourceChannel)delegate).setReadListener(null); + ((ConduitStreamSourceChannel)delegate).setCloseListener(null); + } else { + delegate.getReadSetter().set(null); + delegate.getCloseSetter().set(null); + } + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered == null) { + return super.transferTo(position, count, target); + } + return target.transferFrom(this, position, count); + } + + @Override + public void awaitReadable() throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered == null) { + super.awaitReadable(); + } + } + + @Override + public void suspendReads() { + readsResumed = false; + super.suspendReads(); + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered == null) { + return super.transferTo(count, throughBuffer, target); + } + //make sure there is no garbage in throughBuffer + throughBuffer.position(0); + throughBuffer.limit(0); + long copied = 0; + for (int i = 0; i < buffered.length; ++i) { + Pooled pooled = buffered[i]; + if (pooled != null) { + final ByteBuffer buf = pooled.getResource(); + if (buf.hasRemaining()) { + int res = target.write(buf); + + if (!buf.hasRemaining()) { + pooled.free(); + buffered[i] = null; + } + if (res == 0) { + return copied; + } else { + copied += res; + } + } else { + pooled.free(); + buffered[i] = null; + } + } + } + removeAttachment(BUFFERED_REQUEST_DATA); + if (copied == 0) { + return super.transferTo(count, throughBuffer, target); + } else { + return copied; + } + } + + @Override + public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered == null) { + super.awaitReadable(time, timeUnit); + } + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered == null) { + return super.read(dsts, offset, length); + } + long copied = 0; + for (int i = 0; i < buffered.length; ++i) { + Pooled pooled = buffered[i]; + if (pooled != null) { + final ByteBuffer buf = pooled.getResource(); + if (buf.hasRemaining()) { + copied += Buffers.copy(dsts, offset, length, buf); + if (!buf.hasRemaining()) { + pooled.free(); + buffered[i] = null; + } + if (!Buffers.hasRemaining(dsts, offset, length)) { + return copied; + } + } else { + pooled.free(); + buffered[i] = null; + } + } + } + removeAttachment(BUFFERED_REQUEST_DATA); + if (copied == 0) { + return super.read(dsts, offset, length); + } else { + return copied; + } + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + return read(dsts, 0, dsts.length); + } + + @Override + public boolean isOpen() { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered != null) { + return true; + } + return super.isOpen(); + } + + @Override + public void close() throws IOException { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered != null) { + for (Pooled pooled : buffered) { + if (pooled != null) { + pooled.free(); + } + } + } + removeAttachment(BUFFERED_REQUEST_DATA); + super.close(); + } + + @Override + public boolean isReadResumed() { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered != null) { + return readsResumed; + } + if(isFinished()) { + return false; + } + return anyAreSet(state, FLAG_SHOULD_RESUME_READS) || super.isReadResumed(); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + Pooled[] buffered = getAttachment(BUFFERED_REQUEST_DATA); + if (buffered == null) { + return super.read(dst); + } + int copied = 0; + for (int i = 0; i < buffered.length; ++i) { + Pooled pooled = buffered[i]; + if (pooled != null) { + final ByteBuffer buf = pooled.getResource(); + if (buf.hasRemaining()) { + copied += Buffers.copy(dst, buf); + if (!buf.hasRemaining()) { + pooled.free(); + buffered[i] = null; + } + if (!dst.hasRemaining()) { + return copied; + } + } else { + pooled.free(); + buffered[i] = null; + } + } + } + removeAttachment(BUFFERED_REQUEST_DATA); + if (copied == 0) { + return super.read(dst); + } else { + return copied; + } + } + + public void runResume() { + if (isReadResumed()) { + if (wakeup) { + wakeup = false; + delegate.wakeupReads(); + } else { + delegate.resumeReads(); + } + } else if(wakeup) { + wakeup = false; + invokeListener(); + } + } + } + + public static class WrapperStreamSinkConduitFactory implements ConduitFactory { + + private final HttpServerExchange exchange; + private final ConduitWrapper[] wrappers; + private int position; + private final StreamSinkConduit first; + + + public WrapperStreamSinkConduitFactory(ConduitWrapper[] wrappers, int wrapperCount, HttpServerExchange exchange, StreamSinkConduit first) { + this.wrappers = wrappers; + this.exchange = exchange; + this.first = first; + this.position = wrapperCount - 1; + } + + @Override + public StreamSinkConduit create() { + if (position == -1) { + return exchange.getConnection().getSinkConduit(exchange, first); + } else { + return wrappers[position--].wrap(this, exchange); + } + } + } + + public static class WrapperConduitFactory implements ConduitFactory { + + private final HttpServerExchange exchange; + private final ConduitWrapper[] wrappers; + private int position; + private T first; + + + public WrapperConduitFactory(ConduitWrapper[] wrappers, int wrapperCount, T first, HttpServerExchange exchange) { + this.wrappers = wrappers; + this.exchange = exchange; + this.position = wrapperCount - 1; + this.first = first; + } + + @Override + public T create() { + if (position == -1) { + return first; + } else { + return wrappers[position--].wrap(this, exchange); + } + } + } + + @Override + public String toString() { + return "HttpServerExchange{ " + getRequestMethod().toString() + " " + getRequestURI() + '}'; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/HttpUpgradeListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/HttpUpgradeListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/HttpUpgradeListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import org.xnio.StreamConnection; + +/** + * Listener that is used to perform a HTTP upgrade. + * + * @author Stuart Douglas + */ +public interface HttpUpgradeListener { + + /** + * Method that is called once the upgrade is complete. + * + * @param streamConnection The connection that can be used to send or receive data + * @param exchange + */ + void handleUpgrade(final StreamConnection streamConnection, HttpServerExchange exchange); + +} Index: 3rdParty_sources/undertow/io/undertow/server/JvmRouteHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/JvmRouteHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/JvmRouteHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,131 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.StreamSinkConduit; + +/** + * + * Handler that appends the JVM route to the session id. + * + * @author Stuart Douglas + */ +public class JvmRouteHandler implements HttpHandler { + + private final HttpHandler next; + private final String sessionCookieName; + private final String jvmRoute; + private final JvmRouteWrapper wrapper = new JvmRouteWrapper(); + + + public JvmRouteHandler(HttpHandler next, String sessionCookieName, String jvmRoute) { + this.next = next; + this.sessionCookieName = sessionCookieName; + this.jvmRoute = jvmRoute; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + + Cookie sessionId = exchange.getRequestCookies().get(sessionCookieName); + if (sessionId != null) { + int part = sessionId.getValue().indexOf('.'); + if (part != -1) { + sessionId.setValue(sessionId.getValue().substring(0, part)); + } + } + exchange.addResponseWrapper(wrapper); + next.handleRequest(exchange); + } + + private class JvmRouteWrapper implements ConduitWrapper { + + @Override + public StreamSinkConduit wrap(ConduitFactory factory, HttpServerExchange exchange) { + + Cookie sessionId = exchange.getResponseCookies().get(sessionCookieName); + if (sessionId != null) { + StringBuilder sb = new StringBuilder(sessionId.getValue()); + sb.append('.'); + sb.append(jvmRoute); + sessionId.setValue(sb.toString()); + } + return factory.create(); + } + } + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "jvm-route"; + } + + @Override + public Map> parameters() { + Map> params = new HashMap<>(); + params.put("value", String.class); + params.put("session-cookie-name", String.class); + + return params; + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public HandlerWrapper build(Map config) { + String sessionCookieName = (String) config.get("session-cookie-name"); + + return new Wrapper((String)config.get("value"), sessionCookieName == null ? "JSESSIONID" : sessionCookieName); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final String value; + private final String sessionCookieName; + + private Wrapper(String value, String sessionCookieName) { + this.value = value; + this.sessionCookieName = sessionCookieName; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new JvmRouteHandler(handler, sessionCookieName, value); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/ListenerRegistry.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/ListenerRegistry.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/ListenerRegistry.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,176 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CopyOnWriteArraySet; + +import io.undertow.UndertowMessages; +import io.undertow.util.CopyOnWriteMap; + +/** + * A registry of listeners, and the services that are exposed via these listeners. + * + * This is not used directly by Undertow, but can be used by embedding applications to + * track listener metadata. + * + * @author Stuart Douglas + */ +public class ListenerRegistry { + + private final ConcurrentMap listeners = new CopyOnWriteMap<>(); + + public Listener getListener(final String name) { + return listeners.get(name); + } + + public void addListener(final Listener listener) { + if(listeners.putIfAbsent(listener.getName(), listener) != null) { + throw UndertowMessages.MESSAGES.listenerAlreadyRegistered(listener.getName()); + } + } + + public void removeListener(final String name) { + listeners.remove(name); + } + + public static final class Listener { + + private final String protocol; + private final String name; + private final String serverName; + private final InetSocketAddress bindAddress; + + /** + * Map that can be used to store additional listener metadata + */ + private final Map contextInformation = new CopyOnWriteMap<>(); + + /** + * Information about any HTTP upgrade handlers that are registered on this handler. + */ + private final Set httpUpgradeMetadata = new CopyOnWriteArraySet<>(); + + public Listener(final String protocol, final String name, final String serverName, final InetSocketAddress bindAddress) { + this.protocol = protocol; + this.name = name; + this.serverName = serverName; + this.bindAddress = bindAddress; + } + + /** + * The protocol that this listener is using + */ + public String getProtocol() { + return protocol; + } + + /** + * The optional listener name; + */ + public String getName() { + return name; + } + + /** + * The server name + */ + public String getServerName() { + return serverName; + } + + /** + * The address that this listener is bound to + */ + public InetSocketAddress getBindAddress() { + return bindAddress; + } + + public Collection getContextKeys() { + return contextInformation.keySet(); + } + + public Object removeContextInformation(final String key) { + return contextInformation.remove(key); + } + + public Object setContextInformation(final String key, final Object value) { + return contextInformation.put(key, value); + } + + public Object getContextInformation(final String key) { + return contextInformation.get(key); + } + + public void addHttpUpgradeMetadata(final HttpUpgradeMetadata upgradeMetadata) { + httpUpgradeMetadata.add(upgradeMetadata); + } + + public void removeHttpUpgradeMetadata(final HttpUpgradeMetadata upgradeMetadata) { + httpUpgradeMetadata.remove(upgradeMetadata); + } + + public Set getHttpUpgradeMetadata() { + return Collections.unmodifiableSet(httpUpgradeMetadata); + } + } + + public static final class HttpUpgradeMetadata { + + private final String protocol; + private final String subProtocol; + private final Map contextInformation = new CopyOnWriteMap<>(); + + + public HttpUpgradeMetadata(final String protocol, final String subProtocol) { + this.protocol = protocol; + this.subProtocol = subProtocol; + } + + public String getProtocol() { + return protocol; + } + + public String getSubProtocol() { + return subProtocol; + } + + public Collection getContextKeys() { + return contextInformation.keySet(); + } + + public Object removeContextInformation(final String key) { + return contextInformation.remove(key); + } + + public Object setContextInformation(final String key, final Object value) { + return contextInformation.put(key, value); + } + + public Object getContextInformation(final String key) { + return contextInformation.get(key); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/OpenListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/OpenListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/OpenListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import org.xnio.ChannelListener; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +public interface OpenListener extends ChannelListener { + + HttpHandler getRootHandler(); + + void setRootHandler(HttpHandler rootHandler); + + OptionMap getUndertowOptions(); + + void setUndertowOptions(OptionMap undertowOptions); + + Pool getBufferPool(); +} Index: 3rdParty_sources/undertow/io/undertow/server/RenegotiationRequiredException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/RenegotiationRequiredException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/RenegotiationRequiredException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +/** + * Exception that is thrown that indicates that SSL renegotiation is required + * in order to get a client cert. + * + * This will be thrown if a user attempts to retrieve a client cert and the SSL mode + * is {@link org.xnio.SslClientAuthMode#NOT_REQUESTED}. + * + * @author Stuart Douglas + */ +public class RenegotiationRequiredException extends Exception { + + public RenegotiationRequiredException() { + } + + public RenegotiationRequiredException(String message) { + super(message); + } + + public RenegotiationRequiredException(String message, Throwable cause) { + super(message, cause); + } + + public RenegotiationRequiredException(Throwable cause) { + super(cause); + } + + public RenegotiationRequiredException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { + super(message, cause, enableSuppression, writableStackTrace); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/RoutingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/RoutingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/RoutingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,230 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.predicate.Predicate; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import io.undertow.util.PathTemplate; +import io.undertow.util.PathTemplateMatch; +import io.undertow.util.PathTemplateMatcher; + +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * A Handler that handles the common case of routing via path template and method name. + * + * @author Stuart Douglas + */ +public class RoutingHandler implements HttpHandler { + + private final Map> matches = new CopyOnWriteMap<>(); + private final PathTemplateMatcher allMethodsMatcher = new PathTemplateMatcher<>(); + + private volatile HttpHandler fallbackHandler = ResponseCodeHandler.HANDLE_404; + private volatile HttpHandler invalidMethodHandler = ResponseCodeHandler.HANDLE_405; + + /** + * If this is true then path matches will be added to the query parameters for easy access by + * later handlers. + */ + private final boolean rewriteQueryParameters; + + public RoutingHandler(boolean rewriteQueryParameters) { + this.rewriteQueryParameters = rewriteQueryParameters; + } + + public RoutingHandler() { + this.rewriteQueryParameters = true; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + + PathTemplateMatcher matcher = matches.get(exchange.getRequestMethod()); + if (matcher == null) { + invalidMethodHandler.handleRequest(exchange); + return; + } + PathTemplateMatcher.PathMatchResult match = matcher.match(exchange.getRelativePath()); + if (match == null) { + if (allMethodsMatcher.match(exchange.getRelativePath()) != null) { + invalidMethodHandler.handleRequest(exchange); + return; + } + fallbackHandler.handleRequest(exchange); + return; + } + exchange.putAttachment(PathTemplateMatch.ATTACHMENT_KEY, match); + if (rewriteQueryParameters) { + for (Map.Entry entry : match.getParameters().entrySet()) { + exchange.addQueryParam(entry.getKey(), entry.getValue()); + } + } + for (HandlerHolder handler : match.getValue().predicatedHandlers) { + if (handler.predicate.resolve(exchange)) { + handler.handler.handleRequest(exchange); + return; + } + } + if (match.getValue().defaultHandler != null) { + match.getValue().defaultHandler.handleRequest(exchange); + } else { + fallbackHandler.handleRequest(exchange); + } + } + + public synchronized RoutingHandler add(final String method, final String template, HttpHandler handler) { + return add(new HttpString(method), template, handler); + } + + public synchronized RoutingHandler add(HttpString method, String template, HttpHandler handler) { + PathTemplateMatcher matcher = matches.get(method); + if (matcher == null) { + matches.put(method, matcher = new PathTemplateMatcher<>()); + } + RoutingMatch res = matcher.get(template); + if (res == null) { + matcher.add(template, res = new RoutingMatch()); + } + if (allMethodsMatcher.get(template) == null) { + allMethodsMatcher.add(template, res); + } + res.defaultHandler = handler; + return this; + } + + + + public synchronized RoutingHandler get(final String template, HttpHandler handler) { + return add(Methods.GET, template, handler); + } + + public synchronized RoutingHandler post(final String template, HttpHandler handler) { + return add(Methods.POST, template, handler); + } + + public synchronized RoutingHandler put(final String template, HttpHandler handler) { + return add(Methods.PUT, template, handler); + } + + public synchronized RoutingHandler delete(final String template, HttpHandler handler) { + return add(Methods.DELETE, template, handler); + } + + public synchronized RoutingHandler add(final String method, final String template, Predicate predicate, HttpHandler handler) { + return add(new HttpString(method), template, predicate, handler); + } + + public synchronized RoutingHandler add(HttpString method, String template, Predicate predicate, HttpHandler handler) { + PathTemplateMatcher matcher = matches.get(method); + if (matcher == null) { + matches.put(method, matcher = new PathTemplateMatcher<>()); + } + RoutingMatch res = matcher.get(template); + if (res == null) { + matcher.add(template, res = new RoutingMatch()); + } + if (allMethodsMatcher.get(template) == null) { + allMethodsMatcher.add(template, res); + } + res.predicatedHandlers.add(new HandlerHolder(predicate, handler)); + return this; + } + + public synchronized RoutingHandler get(final String template, Predicate predicate, HttpHandler handler) { + return add(Methods.GET, template, predicate, handler); + } + + public synchronized RoutingHandler post(final String template, Predicate predicate, HttpHandler handler) { + return add(Methods.POST, template, predicate, handler); + } + + public synchronized RoutingHandler put(final String template, Predicate predicate, HttpHandler handler) { + return add(Methods.PUT, template, predicate, handler); + } + + public synchronized RoutingHandler delete(final String template, Predicate predicate, HttpHandler handler) { + return add(Methods.DELETE, template, predicate, handler); + } + + public synchronized RoutingHandler addAll(RoutingHandler routingHandler) { + for (Entry> entry : routingHandler.getMatches().entrySet()) { + HttpString method = entry.getKey(); + PathTemplateMatcher matcher = matches.get(method); + if (matcher == null) { + matches.put(method, matcher = new PathTemplateMatcher<>()); + } + matcher.addAll(entry.getValue()); + // If we use allMethodsMatcher.addAll() we can have duplicate + // PathTemplates which we want to ignore here so it does not crash. + for (PathTemplate template : entry.getValue().getPathTemplates()) { + if (allMethodsMatcher.get(template.getTemplateString()) == null) { + allMethodsMatcher.add(template, new RoutingMatch()); + } + } + } + return this; + } + + Map> getMatches() { + return matches; + } + + public HttpHandler getFallbackHandler() { + return fallbackHandler; + } + + public RoutingHandler setFallbackHandler(HttpHandler fallbackHandler) { + this.fallbackHandler = fallbackHandler; + return this; + } + + public HttpHandler getInvalidMethodHandler() { + return invalidMethodHandler; + } + + public RoutingHandler setInvalidMethodHandler(HttpHandler invalidMethodHandler) { + this.invalidMethodHandler = invalidMethodHandler; + return this; + } + + private static class RoutingMatch { + + final List predicatedHandlers = new CopyOnWriteArrayList<>(); + volatile HttpHandler defaultHandler; + + } + + private static class HandlerHolder { + final Predicate predicate; + final HttpHandler handler; + + private HandlerHolder(Predicate predicate, HttpHandler handler) { + this.predicate = predicate; + this.handler = handler; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/SSLSessionInfo.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/SSLSessionInfo.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/SSLSessionInfo.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import org.xnio.SslClientAuthMode; + +import java.io.IOException; + +/** + * SSL session information. + * + * @author Stuart Douglas + */ +public interface SSLSessionInfo { + + /** + * + * @return The SSL session ID, or null if this could not be determined. + */ + byte[] getSessionId(); + + java.lang.String getCipherSuite(); + + /** + * Gets the peer certificates. This may force SSL renegotiation. + * + * @return The peer certificates + * @throws javax.net.ssl.SSLPeerUnverifiedException + * @throws RenegotiationRequiredException If the session + */ + java.security.cert.Certificate[] getPeerCertificates() throws javax.net.ssl.SSLPeerUnverifiedException, RenegotiationRequiredException; + + javax.security.cert.X509Certificate[] getPeerCertificateChain() throws javax.net.ssl.SSLPeerUnverifiedException, RenegotiationRequiredException; + + /** + * Renegotiate in a blocking manner. This will set the client aut + * + * TODO: we also need a non-blocking version + * + * @throws IOException + * @param exchange The exchange + * @param sslClientAuthMode The client cert mode to use when renegotiating + */ + void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClientAuthMode) throws IOException; + +} Index: 3rdParty_sources/undertow/io/undertow/server/ServerConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/ServerConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/ServerConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,204 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import io.undertow.util.AbstractAttachable; + +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.ConnectedChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSinkConduit; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; + +/** + * A server connection. + * + * @author Stuart Douglas + */ +public abstract class ServerConnection extends AbstractAttachable implements ConnectedChannel { + + /** + * + * @return The connections buffer pool + */ + public abstract Pool getBufferPool(); + + /** + * + * @return The connections worker + */ + public abstract XnioWorker getWorker(); + + /** + * + * @return The IO thread associated with the connection + */ + @Override + public abstract XnioIoThread getIoThread(); + + /** + * Sends an out of band response, such as a HTTP 100-continue response. + * + * WARNING: do not attempt to write to the current exchange until the out of band + * exchange has been fully written. Doing so may have unexpected results. + * + * TODO: this needs more thought. + * + * @return The out of band exchange. + * @param exchange The current exchange + */ + public abstract HttpServerExchange sendOutOfBandResponse(HttpServerExchange exchange); + + /** + * Invoked when the exchange is complete, and there is still data in the request channel. Some implementations + * (such as SPDY and HTTP2) have more efficient ways to drain the request than simply reading all data + * (e.g. RST_STREAM). + * + * After this method is invoked the stream will be drained normally. + * + * @param exchange The current exchange. + */ + public abstract void terminateRequestChannel(HttpServerExchange exchange); + + /** + * + * @return true if the connection is open + */ + public abstract boolean isOpen(); + + public abstract boolean supportsOption(Option option); + + public abstract T getOption(Option option) throws IOException; + + public abstract T setOption(Option option, T value) throws IllegalArgumentException, IOException; + + public abstract void close() throws IOException; + + /** + * Returns the actual address of the remote connection. This will not take things like X-Forwarded-for + * into account. + * @return The address of the remote peer + */ + public abstract SocketAddress getPeerAddress(); + + /** + * Returns the actual address of the remote connection. This will not take things like X-Forwarded-for + * into account. + * + * @param type The type of address to return + * @param The address type + * @return The remote endpoint address + */ + public abstract A getPeerAddress(Class type); + + public abstract SocketAddress getLocalAddress(); + + public abstract A getLocalAddress(Class type); + + public abstract OptionMap getUndertowOptions(); + + public abstract int getBufferSize(); + + /** + * Gets SSL information about the connection. This could represent the actual + * client connection, or could be providing SSL information that was provided + * by a front end proxy. + * + * @return SSL information about the connection + */ + public abstract SSLSessionInfo getSslSessionInfo(); + + /** + * Sets the current SSL information. This can be used by handlers to setup SSL + * information that was provided by a front end proxy. + * + * If this is being set of a per request basis then you must ensure that it is either + * cleared by an exchange completion listener at the end of the request, or is always + * set for every request. Otherwise it is possible to SSL information to 'leak' between + * requests. + * + * @param sessionInfo The ssl session information + */ + public abstract void setSslSessionInfo(SSLSessionInfo sessionInfo); + + /** + * Adds a close listener, than will be invoked with the connection is closed + * + * @param listener The close listener + */ + public abstract void addCloseListener(CloseListener listener); + + /** + * Upgrade the connection, if allowed + * @return The StreamConnection that should be passed to the upgrade handler + */ + protected abstract StreamConnection upgradeChannel(); + + protected abstract ConduitStreamSinkChannel getSinkChannel(); + + protected abstract ConduitStreamSourceChannel getSourceChannel(); + + /** + * Gets the sink conduit that should be used for this request. + * + * This allows the connection to apply any per-request conduit wrapping + * that is required, without adding to the response wrappers array. + * + * There is no corresponding method for source conduits, as in general + * conduits can be directly inserted into the connection after the + * request has been read. + * + * @return The source conduit + */ + protected abstract StreamSinkConduit getSinkConduit(HttpServerExchange exchange, final StreamSinkConduit conduit); + + /** + * + * @return true if this connection supports HTTP upgrade + */ + protected abstract boolean isUpgradeSupported(); + + /** + * Invoked when the exchange is complete. + */ + protected abstract void exchangeComplete(HttpServerExchange exchange); + + protected abstract void setUpgradeListener(HttpUpgradeListener upgradeListener); + + /** + * Callback that is invoked if the max entity size is updated. + * + * @param exchange The current exchange + */ + protected abstract void maxEntitySizeUpdated(HttpServerExchange exchange); + + public interface CloseListener { + + void closed(final ServerConnection connection); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/TruncatedResponseException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/TruncatedResponseException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/TruncatedResponseException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server; + +import java.io.IOException; + +/** + * An exception indicating that the response channel was prematurely closed. The response channel must be shut + * down and flushed successfully after all requests, even those which do not send a response body. + * + * @author David M. Lloyd + */ +public class TruncatedResponseException extends IOException { + + /** + * Constructs a {@code TruncatedResponseException} with no detail message. The cause is not initialized, and may + * subsequently be initialized by a call to {@link #initCause(Throwable) initCause}. + */ + public TruncatedResponseException() { + } + + /** + * Constructs a {@code TruncatedResponseException} with the specified detail message. The cause is not initialized, + * and may subsequently be initialized by a call to {@link #initCause(Throwable) initCause}. + * + * @param msg the detail message + */ + public TruncatedResponseException(final String msg) { + super(msg); + } + + /** + * Constructs a {@code TruncatedResponseException} with the specified cause. The detail message is set to: + *

(cause == null ? null : cause.toString())
+ * (which typically contains the class and detail message of {@code cause}). + * + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method) + */ + public TruncatedResponseException(final Throwable cause) { + super(cause); + } + + /** + * Constructs a {@code TruncatedResponseException} with the specified detail message and cause. + * + * @param msg the detail message + * @param cause the cause (which is saved for later retrieval by the {@link #getCause()} method) + */ + public TruncatedResponseException(final String msg, final Throwable cause) { + super(msg, cause); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/AccessControlListHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/AccessControlListHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/AccessControlListHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,164 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.UndertowMessages; +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.StatusCodes; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.regex.Pattern; +import java.util.regex.PatternSyntaxException; + +/** + * Handler that can accept or reject a request based on an attribute of the remote peer + * + * todo: should we support non-regex values for performance reasons? + * @author Stuart Douglas + * @author Andre Dietisheim + */ +public class AccessControlListHandler implements HttpHandler { + + private volatile HttpHandler next; + private volatile boolean defaultAllow = false; + private final ExchangeAttribute attribute; + private final List acl = new CopyOnWriteArrayList<>(); + + public AccessControlListHandler(final HttpHandler next, ExchangeAttribute attribute) { + this.next = next; + this.attribute = attribute; + } + + public AccessControlListHandler(ExchangeAttribute attribute) { + this.attribute = attribute; + this.next = ResponseCodeHandler.HANDLE_404; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + String attribute = this.attribute.readAttribute(exchange); + if (isAllowed(attribute)) { + next.handleRequest(exchange); + } else { + exchange.setResponseCode(StatusCodes.FORBIDDEN); + exchange.endExchange(); + } + } + + //package private for unit tests + boolean isAllowed(String attribute) { + if (attribute != null) { + for (AclMatch rule : acl) { + if (rule.matches(attribute)) { + return !rule.isDeny(); + } + } + } + return defaultAllow; + } + + public boolean isDefaultAllow() { + return defaultAllow; + } + + public AccessControlListHandler setDefaultAllow(final boolean defaultAllow) { + this.defaultAllow = defaultAllow; + return this; + } + + public HttpHandler getNext() { + return next; + } + + public AccessControlListHandler setNext(final HttpHandler next) { + this.next = next; + return this; + } + + /** + * Adds an allowed user agent peer to the ACL list + *

+ * User agent may be given as regex + * + * @param pattern The pattern to add to the ACL + */ + public AccessControlListHandler addAllow(final String pattern) { + return addRule(pattern, false); + } + + /** + * Adds an denied user agent to the ACL list + *

+ * User agent may be given as regex + * + * @param pattern The user agent to add to the ACL + */ + public AccessControlListHandler addDeny(final String pattern) { + return addRule(pattern, true); + } + + public AccessControlListHandler clearRules() { + this.acl.clear(); + return this; + } + + private AccessControlListHandler addRule(final String userAgent, final boolean deny) { + this.acl.add(new AclMatch(deny, userAgent)); + return this; + } + + static class AclMatch { + + private final boolean deny; + private final Pattern pattern; + + protected AclMatch(final boolean deny, final String pattern) { + this.deny = deny; + this.pattern = createPattern(pattern); + } + + private Pattern createPattern(final String pattern) { + try { + return Pattern.compile(pattern); + } catch (PatternSyntaxException e) { + throw UndertowMessages.MESSAGES.notAValidRegularExpressionPattern(pattern); + } + } + + boolean matches(final String attribute) { + return pattern.matcher(attribute).matches(); + } + + boolean isDeny() { + return deny; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{" + + "deny=" + deny + + ", pattern='" + pattern + '\'' + + '}'; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/AllowedMethodsHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/AllowedMethodsHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/AllowedMethodsHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,112 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; + +/** + * Handler that whitelists certain HTTP methods. Only requests with a method in + * the allowed methods set will be allowed to continue. + * + * @author Stuart Douglas + */ +public class AllowedMethodsHandler implements HttpHandler { + + private final Set allowedMethods; + private final HttpHandler next; + + public AllowedMethodsHandler(final HttpHandler next, final Set allowedMethods) { + this.allowedMethods = new HashSet<>(allowedMethods); + this.next = next; + } + + public AllowedMethodsHandler(final HttpHandler next, final HttpString... allowedMethods) { + this.allowedMethods = new HashSet<>(Arrays.asList(allowedMethods)); + this.next = next; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if (allowedMethods.contains(exchange.getRequestMethod())) { + next.handleRequest(exchange); + } else { + exchange.setResponseCode(StatusCodes.METHOD_NOT_ALLOWED); + exchange.endExchange(); + } + } + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "allowed-methods"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("methods", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("methods"); + } + + @Override + public String defaultParameter() { + return "methods"; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper((String[]) config.get("methods")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final String[] methods; + + private Wrapper(String[] methods) { + this.methods = methods; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + HttpString[] strings = new HttpString[methods.length]; + for(int i = 0; i < methods.length; ++i) { + strings[i] = new HttpString(methods[i]); + } + + return new AllowedMethodsHandler(handler, strings); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/AttachmentHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/AttachmentHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/AttachmentHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,69 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +/** + * Handler that adds an attachment to the request + * + * @author Stuart Douglas + */ +public class AttachmentHandler implements HttpHandler { + + private final AttachmentKey key; + private volatile T instance; + private volatile HttpHandler next; + + public AttachmentHandler(final AttachmentKey key, final HttpHandler next, final T instance) { + this.next = next; + this.key = key; + this.instance = instance; + } + + public AttachmentHandler(final AttachmentKey key, final HttpHandler next) { + this(key, next, null); + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.putAttachment(key, instance); + next.handleRequest(exchange); + } + + public T getInstance() { + return instance; + } + + public void setInstance(final T instance) { + this.instance = instance; + } + + public HttpHandler getNext() { + return next; + } + + public void setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/BlockingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/BlockingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/BlockingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,105 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; + +/** + * A {@link HttpHandler} that initiates a blocking request. If the thread is currently running + * in the io thread it will be dispatched. + * + * @author Stuart Douglas + * @author David M. Lloyd + */ +public final class BlockingHandler implements HttpHandler { + + private volatile HttpHandler handler; + + public BlockingHandler(final HttpHandler handler) { + this.handler = handler; + } + + public BlockingHandler() { + this(null); + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + + exchange.startBlocking(); + if (exchange.isInIoThread()) { + exchange.dispatch(handler); + } else { + handler.handleRequest(exchange); + } + } + + public HttpHandler getHandler() { + return handler; + } + + public BlockingHandler setRootHandler(final HttpHandler rootHandler) { + this.handler = rootHandler; + return this; + } + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "blocking"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new BlockingHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/CanonicalPathHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/CanonicalPathHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/CanonicalPathHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,97 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.Handlers; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.CanonicalPathUtils; + +/** + * @author Stuart Douglas + */ +public class CanonicalPathHandler implements HttpHandler { + + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + + public CanonicalPathHandler() { + } + + public CanonicalPathHandler(final HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.setRelativePath(CanonicalPathUtils.canonicalize(exchange.getRelativePath())); + next.handleRequest(exchange); + } + + public HttpHandler getNext() { + return next; + } + + public CanonicalPathHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "canonical-path"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new CanonicalPathHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/ChannelUpgradeHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/ChannelUpgradeHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/ChannelUpgradeHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,171 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.StreamConnection; + +import java.util.Iterator; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * An HTTP request handler which upgrades the HTTP request and hands it off as a socket to any XNIO consumer. + * + * @author David M. Lloyd + * @author Stuart Douglas + */ +public final class ChannelUpgradeHandler implements HttpHandler { + private final CopyOnWriteMap> handlers = new CopyOnWriteMap<>(); + private volatile HttpHandler nonUpgradeHandler = ResponseCodeHandler.HANDLE_404; + + /** + * Add a protocol to this handler. + * + * @param productString the product string to match + * @param openListener the open listener to call + * @param handshake a handshake implementation that can be used to verify the client request and modify the response + */ + public synchronized void addProtocol(String productString, ChannelListener openListener, final HttpUpgradeHandshake handshake) { + if (productString == null) { + throw new IllegalArgumentException("productString is null"); + } + if (openListener == null) { + throw new IllegalArgumentException("openListener is null"); + } + List list = handlers.get(productString); + if (list == null) { + handlers.put(productString, list = new CopyOnWriteArrayList<>()); + } + list.add(new Holder(openListener, handshake)); + } + + /** + * Add a protocol to this handler. + * + * @param productString the product string to match + * @param openListener the open listener to call + */ + public void addProtocol(String productString, ChannelListener openListener) { + addProtocol(productString, openListener, null); + } + + /** + * Remove a protocol from this handler. This will remove all upgrade handlers that match the product string + * + * @param productString the product string to match + */ + public synchronized void removeProtocol(String productString) { + handlers.remove(productString); + } + + /** + * Remove a protocol from this handler. + * + * @param productString the product string to match + * @param openListener The open listener + */ + public synchronized void removeProtocol(String productString, ChannelListener openListener) { + List holders = handlers.get(productString); + if (holders == null) { + return; + } + Iterator it = holders.iterator(); + while (it.hasNext()) { + Holder holder = it.next(); + if (holder.listener == openListener) { + it.remove(); + break; + } + } + if (holders.isEmpty()) { + handlers.remove(productString); + } + } + + /** + * Get the non-upgrade delegate handler. + * + * @return the non-upgrade delegate handler + */ + public HttpHandler getNonUpgradeHandler() { + return nonUpgradeHandler; + } + + /** + * Set the non-upgrade delegate handler. + * + * @param nonUpgradeHandler the non-upgrade delegate handler + */ + public ChannelUpgradeHandler setNonUpgradeHandler(final HttpHandler nonUpgradeHandler) { + Handlers.handlerNotNull(nonUpgradeHandler); + this.nonUpgradeHandler = nonUpgradeHandler; + return this; + } + + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final List upgradeStrings = exchange.getRequestHeaders().get(Headers.UPGRADE); + if (upgradeStrings != null && exchange.getRequestMethod().equals(Methods.GET)) { + for (String string : upgradeStrings) { + final List holders = handlers.get(string); + if (holders != null) { + for (Holder holder : holders) { + final ChannelListener listener = holder.listener; + if (holder.handshake != null) { + if (!holder.handshake.handleUpgrade(exchange)) { + //handshake did not match, try again + continue; + } + } + + exchange.upgradeChannel(string, new HttpUpgradeListener() { + + @Override + public void handleUpgrade(StreamConnection streamConnection, HttpServerExchange exchange) { + ChannelListeners.invokeChannelListener(streamConnection, listener); + } + }); + exchange.endExchange(); + return; + } + } + } + } + nonUpgradeHandler.handleRequest(exchange); + } + + + private static final class Holder { + final ChannelListener listener; + final HttpUpgradeHandshake handshake; + + private Holder(final ChannelListener listener, final HttpUpgradeHandshake handshake) { + this.listener = listener; + this.handshake = handshake; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/Cookie.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/Cookie.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/Cookie.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,72 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Date; + +/** + * A HTTP cookie. + * + * @see io.undertow.server.ExchangeCookieUtils + * @author Stuart Douglas + */ +public interface Cookie { + + String getName(); + + String getValue(); + + Cookie setValue(final String value); + + String getPath(); + + Cookie setPath(final String path); + + String getDomain(); + + Cookie setDomain(final String domain); + + Integer getMaxAge(); + + Cookie setMaxAge(final Integer maxAge); + + boolean isDiscard(); + + Cookie setDiscard(final boolean discard); + + boolean isSecure(); + + Cookie setSecure(final boolean secure); + + int getVersion(); + + Cookie setVersion(final int version); + + boolean isHttpOnly(); + + Cookie setHttpOnly(final boolean httpOnly); + + Date getExpires(); + + Cookie setExpires(final Date expires); + + String getComment(); + + Cookie setComment(final String comment); +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/CookieImpl.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/CookieImpl.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/CookieImpl.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,143 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Date; + +/** + * @author Stuart Douglas + */ +public class CookieImpl implements Cookie { + + private final String name; + private String value; + private String path; + private String domain; + private Integer maxAge; + private Date expires; + private boolean discard; + private boolean secure; + private boolean httpOnly; + private int version = 0; + private String comment; + + + public CookieImpl(final String name, final String value) { + this.name = name; + this.value = value; + } + + public CookieImpl(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + public CookieImpl setValue(final String value) { + this.value = value; + return this; + } + + public String getPath() { + return path; + } + + public CookieImpl setPath(final String path) { + this.path = path; + return this; + } + + public String getDomain() { + return domain; + } + + public CookieImpl setDomain(final String domain) { + this.domain = domain; + return this; + } + + public Integer getMaxAge() { + return maxAge; + } + + public CookieImpl setMaxAge(final Integer maxAge) { + this.maxAge = maxAge; + return this; + } + + public boolean isDiscard() { + return discard; + } + + public CookieImpl setDiscard(final boolean discard) { + this.discard = discard; + return this; + } + + public boolean isSecure() { + return secure; + } + + public CookieImpl setSecure(final boolean secure) { + this.secure = secure; + return this; + } + + public int getVersion() { + return version; + } + + public CookieImpl setVersion(final int version) { + this.version = version; + return this; + } + + public boolean isHttpOnly() { + return httpOnly; + } + + public CookieImpl setHttpOnly(final boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + public Date getExpires() { + return expires; + } + + public CookieImpl setExpires(final Date expires) { + this.expires = expires; + return this; + } + + public String getComment() { + return comment; + } + + public Cookie setComment(final String comment) { + this.comment = comment; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/DateHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/DateHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/DateHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,69 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Date; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.DateUtils; +import io.undertow.util.Headers; + +/** + * Class that adds the Date: header to a HTTP response. + * + * The current date string is cached, and is updated every second in a racey + * manner (i.e. it is possible for two thread to update it at once). + *

+ * This handler is deprecated, the same functionality is achieved by using the + * server option {@link io.undertow.UndertowOptions#ALWAYS_SET_DATE ALWAYS_SET_DATE}. + * It is enabled by default. + * + * @author Stuart Douglas + */ +@Deprecated() +public class DateHandler implements HttpHandler { + + private final HttpHandler next; + private volatile String cachedDateString; + private volatile long nextUpdateTime = -1; + + + public DateHandler(final HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + // better method is used in DateUtils#addDateHeaderIfRequired + long time = System.nanoTime(); + if(time < nextUpdateTime) { + exchange.getResponseHeaders().put(Headers.DATE, cachedDateString); + } else { + long realTime = System.currentTimeMillis(); + String dateString = DateUtils.toDateString(new Date(realTime)); + cachedDateString = dateString; + nextUpdateTime = time + 1000000000; + exchange.getResponseHeaders().put(Headers.DATE, dateString); + } + next.handleRequest(exchange); + } + + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/DisableCacheHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/DisableCacheHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/DisableCacheHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,71 @@ +package io.undertow.server.handlers; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.Headers; + +/** + * + * Handler that disables response caching by browsers and proxies. + * + * + * @author Stuart Douglas + */ +public class DisableCacheHandler implements HttpHandler { + + private final HttpHandler next; + + public DisableCacheHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.getResponseHeaders().add(Headers.CACHE_CONTROL, "no-cache, no-store, must-revalidate"); + exchange.getResponseHeaders().add(Headers.PRAGMA, "no-cache"); + exchange.getResponseHeaders().add(Headers.EXPIRES, "0"); + next.handleRequest(exchange); + } + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "disable-cache"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new DisableCacheHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/DisallowedMethodsHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/DisallowedMethodsHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/DisallowedMethodsHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,113 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; + +/** + * Handler that blacklists certain HTTP methods. + * + * @author Stuart Douglas + */ +public class DisallowedMethodsHandler implements HttpHandler { + + private final Set disallowedMethods; + private final HttpHandler next; + + public DisallowedMethodsHandler(final HttpHandler next, final Set disallowedMethods) { + this.disallowedMethods = new HashSet<>(disallowedMethods); + this.next = next; + } + + + public DisallowedMethodsHandler(final HttpHandler next, final HttpString... disallowedMethods) { + this.disallowedMethods = new HashSet<>(Arrays.asList(disallowedMethods)); + this.next = next; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if (disallowedMethods.contains(exchange.getRequestMethod())) { + exchange.setResponseCode(StatusCodes.METHOD_NOT_ALLOWED); + exchange.endExchange(); + } else { + next.handleRequest(exchange); + } + } + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "disallowed-methods"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("methods", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("methods"); + } + + @Override + public String defaultParameter() { + return "methods"; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper((String[]) config.get("methods")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final String[] methods; + + private Wrapper(String[] methods) { + this.methods = methods; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + HttpString[] strings = new HttpString[methods.length]; + for(int i = 0; i < methods.length; ++i) { + strings[i] = new HttpString(methods[i]); + } + + return new DisallowedMethodsHandler(handler, strings); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/ExceptionHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/ExceptionHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/ExceptionHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +package io.undertow.server.handlers; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * Handler that dispatches to a given handler and allows mapping exceptions + * to be handled by additional handlers. The order the exception handlers are + * added is important because of inheritance. Add all child classes before their + * parents in order to use different handlers. + */ +public class ExceptionHandler implements HttpHandler { + public static final AttachmentKey THROWABLE = AttachmentKey.create(Throwable.class); + + private final HttpHandler handler; + private final List> exceptionHandlers = new CopyOnWriteArrayList<>(); + + public ExceptionHandler(HttpHandler handler) { + this.handler = handler; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + try { + handler.handleRequest(exchange); + } catch (Throwable throwable) { + for (ExceptionHandlerHolder holder : exceptionHandlers) { + if (holder.getClazz().isInstance(throwable)) { + exchange.putAttachment(THROWABLE, throwable); + holder.getHandler().handleRequest(exchange); + return; + } + } + throw throwable; + } + } + + public ExceptionHandler addExceptionHandler(Class clazz, HttpHandler handler) { + exceptionHandlers.add(new ExceptionHandlerHolder<>(clazz, handler)); + return this; + } + + private static class ExceptionHandlerHolder { + private final Class clazz; + private final HttpHandler handler; + public ExceptionHandlerHolder(Class clazz, HttpHandler handler) { + super(); + this.clazz = clazz; + this.handler = handler; + } + public Class getClazz() { + return clazz; + } + public HttpHandler getHandler() { + return handler; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/GracefulShutdownHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/GracefulShutdownHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/GracefulShutdownHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,190 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.UndertowMessages; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.StatusCodes; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +/** + * Handler that allows for graceful server shutdown. Basically it provides a way to prevent the server from + * accepting new requests, and wait for existing requests to complete. + *

+ * The handler itself does not shut anything down. + *

+ * Import: The thread safety semantics of the handler are very important. Don't touch anything unless you know + * what you are doing. + * + * @author Stuart Douglas + */ +public class GracefulShutdownHandler implements HttpHandler { + + private volatile boolean shutdown = false; + private final GracefulShutdownListener listener = new GracefulShutdownListener(); + private final List shutdownListeners = new ArrayList<>(); + + private final Object lock = new Object(); + + private volatile long activeRequests = 0; + private static final AtomicLongFieldUpdater activeRequestsUpdater = AtomicLongFieldUpdater.newUpdater(GracefulShutdownHandler.class, "activeRequests"); + + private final HttpHandler next; + + public GracefulShutdownHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + activeRequestsUpdater.incrementAndGet(this); + if (shutdown) { + decrementRequests(); + exchange.setResponseCode(StatusCodes.SERVICE_UNAVAILABLE); + exchange.endExchange(); + return; + } + exchange.addExchangeCompleteListener(listener); + next.handleRequest(exchange); + } + + + public void shutdown() { + shutdown = true; + } + + public void start() { + synchronized (lock) { + shutdown = false; + for (ShutdownListener listener : shutdownListeners) { + listener.shutdown(false); + } + shutdownListeners.clear(); + } + } + + private void shutdownComplete() { + assert Thread.holdsLock(lock); + lock.notifyAll(); + for (ShutdownListener listener : shutdownListeners) { + listener.shutdown(true); + } + shutdownListeners.clear(); + } + + /** + * Waits for the handler to shutdown. + */ + public void awaitShutdown() throws InterruptedException { + synchronized (lock) { + if (!shutdown) { + throw UndertowMessages.MESSAGES.handlerNotShutdown(); + } + while (activeRequestsUpdater.get(this) > 0) { + lock.wait(); + } + } + } + + /** + * Waits a set length of time for the handler to shut down + * + * @param millis The length of time + * @return true If the handler successfully shut down + */ + public boolean awaitShutdown(long millis) throws InterruptedException { + synchronized (lock) { + if (!shutdown) { + throw UndertowMessages.MESSAGES.handlerNotShutdown(); + } + long end = System.currentTimeMillis() + millis; + int count = (int) activeRequestsUpdater.get(this); + while (count != 0) { + long left = end - System.currentTimeMillis(); + if (left <= 0) { + return false; + } + lock.wait(left); + count = (int) activeRequestsUpdater.get(this); + } + return true; + } + } + + /** + * Adds a shutdown listener that will be invoked when all requests have finished. If all requests have already been finished + * the listener will be invoked immediately. + * + * @param shutdownListener The shutdown listener + */ + public void addShutdownListener(final ShutdownListener shutdownListener) { + synchronized (lock) { + if (!shutdown) { + throw UndertowMessages.MESSAGES.handlerNotShutdown(); + } + long count = activeRequestsUpdater.get(this); + if (count == 0) { + shutdownListener.shutdown(true); + } else { + shutdownListeners.add(shutdownListener); + } + } + } + + private void decrementRequests() { + long active = activeRequestsUpdater.decrementAndGet(this); + if (shutdown) { + synchronized (lock) { + if (active == 0) { + shutdownComplete(); + } + } + } + } + + private final class GracefulShutdownListener implements ExchangeCompletionListener { + + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + try { + decrementRequests(); + } finally { + nextListener.proceed(); + } + } + } + + /** + * A listener which can be registered with the handler to be notified when all pending requests have finished. + */ + public interface ShutdownListener { + + /** + * Notification that the container has shutdown. + * + * @param shutdownSuccessful If the shutdown succeeded or not + */ + void shutdown(boolean shutdownSuccessful); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/HttpContinueAcceptingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/HttpContinueAcceptingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/HttpContinueAcceptingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.io.IOException; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.predicate.Predicate; +import io.undertow.predicate.Predicates; +import io.undertow.server.protocol.http.HttpContinue; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * Handler that provides support for HTTP/1.1 continue responses. + *

+ * If the provided predicate returns true then the request will be + * accepted, otherwise it will be rejected. + * + * If no predicate is supplied then all requests will be accepted. + * + * @see io.undertow.server.protocol.http.HttpContinue + * @author Stuart Douglas + */ +public class HttpContinueAcceptingHandler implements HttpHandler { + + private final HttpHandler next; + private final Predicate accept; + + public HttpContinueAcceptingHandler(HttpHandler next, Predicate accept) { + this.next = next; + this.accept = accept; + } + + public HttpContinueAcceptingHandler(HttpHandler next) { + this(next, Predicates.truePredicate()); + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if(HttpContinue.requiresContinueResponse(exchange)) { + if(accept.resolve(exchange)) { + HttpContinue.sendContinueResponse(exchange, new IoCallback() { + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + exchange.dispatch(next); + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + exchange.endExchange(); + } + }); + + } else { + HttpContinue.rejectExchange(exchange); + } + } else { + next.handleRequest(exchange); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/HttpContinueReadHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/HttpContinueReadHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/HttpContinueReadHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,197 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import io.undertow.server.ConduitWrapper; +import io.undertow.server.protocol.http.HttpContinue; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConduitFactory; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.StreamSourceConduit; + +/** + * Handler for requests that require 100-continue responses. If an attempt is made to read from the source + * channel then a 100 continue response is sent. + * + * @author Stuart Douglas + */ +public class HttpContinueReadHandler implements HttpHandler { + + private static final ConduitWrapper WRAPPER = new ConduitWrapper() { + @Override + public StreamSourceConduit wrap(final ConduitFactory factory, final HttpServerExchange exchange) { + return new ContinueConduit(factory.create(), exchange); + } + }; + + private final HttpHandler handler; + + public HttpContinueReadHandler(final HttpHandler handler) { + this.handler = handler; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if (HttpContinue.requiresContinueResponse(exchange)) { + exchange.addRequestWrapper(WRAPPER); + } + handler.handleRequest(exchange); + } + + private static final class ContinueConduit extends AbstractStreamSourceConduit implements StreamSourceConduit { + + private boolean sent = false; + private HttpContinue.ContinueResponseSender response = null; + private final HttpServerExchange exchange; + + + protected ContinueConduit(final StreamSourceConduit next, final HttpServerExchange exchange) { + super(next); + this.exchange = exchange; + } + + @Override + public long transferTo(final long position, final long count, final FileChannel target) throws IOException { + if (exchange.getResponseCode() == 417) { + //rejected + return -1; + } + if (!sent) { + sent = true; + response = HttpContinue.createResponseSender(exchange); + } + if (response != null) { + if (!response.send()) { + return 0; + } + response = null; + } + return super.transferTo(position, count, target); + } + + @Override + public long transferTo(final long count, final ByteBuffer throughBuffer, final StreamSinkChannel target) throws IOException { + if (exchange.getResponseCode() == 417) { + //rejected + return -1; + } + if (!sent) { + sent = true; + response = HttpContinue.createResponseSender(exchange); + } + if (response != null) { + if (!response.send()) { + return 0; + } + response = null; + } + return super.transferTo(count, throughBuffer, target); + } + + @Override + public int read(final ByteBuffer dst) throws IOException { + if (exchange.getResponseCode() == 417) { + //rejected + return -1; + } + if (!sent) { + sent = true; + response = HttpContinue.createResponseSender(exchange); + } + if (response != null) { + if (!response.send()) { + return 0; + } + response = null; + } + return super.read(dst); + } + + @Override + public long read(final ByteBuffer[] dsts, final int offs, final int len) throws IOException { + if (exchange.getResponseCode() == 417) { + //rejected + return -1; + } + if (!sent) { + sent = true; + response = HttpContinue.createResponseSender(exchange); + } + if (response != null) { + if (!response.send()) { + return 0; + } + response = null; + } + return super.read(dsts, offs, len); + } + + @Override + public void awaitReadable(final long time, final TimeUnit timeUnit) throws IOException { + if (exchange.getResponseCode() == 417) { + //rejected + return; + } + if (!sent) { + sent = true; + response = HttpContinue.createResponseSender(exchange); + } + long exitTime = System.currentTimeMillis() + timeUnit.toMillis(time); + if (response != null) { + while (!response.send()) { + long currentTime = System.currentTimeMillis(); + if (currentTime > exitTime) { + return; + } + response.awaitWritable(exitTime - currentTime, TimeUnit.MILLISECONDS); + } + response = null; + } + + long currentTime = System.currentTimeMillis(); + super.awaitReadable(exitTime - currentTime, TimeUnit.MILLISECONDS); + } + + @Override + public void awaitReadable() throws IOException { + if (exchange.getResponseCode() == 417) { + //rejected + return; + } + if (!sent) { + sent = true; + response = HttpContinue.createResponseSender(exchange); + } + if (response != null) { + while (!response.send()) { + response.awaitWritable(); + } + response = null; + } + super.awaitReadable(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/HttpTraceHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/HttpTraceHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/HttpTraceHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,110 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.Methods; + +/** + * A handler that handles HTTP trace requests + * + * @author Stuart Douglas + */ +public class HttpTraceHandler implements HttpHandler { + + private final HttpHandler handler; + + public HttpTraceHandler(final HttpHandler handler) { + this.handler = handler; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if(exchange.getRequestMethod().equals(Methods.TRACE)) { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "message/http"); + StringBuilder body = new StringBuilder("TRACE "); + body.append(exchange.getRequestURI()); + if(!exchange.getQueryString().isEmpty()) { + body.append('?'); + body.append(exchange.getQueryString()); + } + body.append(exchange.getProtocol().toString()); + body.append("\r\n"); + for(HeaderValues header : exchange.getRequestHeaders()) { + for(String value : header) { + body.append(header.getHeaderName()); + body.append(": "); + body.append(value); + body.append("\r\n"); + } + } + body.append("\r\n"); + exchange.getResponseSender().send(body.toString()); + } else { + handler.handleRequest(exchange); + } + } + + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "trace"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new HttpTraceHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/HttpUpgradeHandshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/HttpUpgradeHandshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/HttpUpgradeHandshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.io.IOException; + +import io.undertow.server.HttpServerExchange; + +/** + * Server side upgrade handler. This handler can inspect the request and modify the response. + *

+ * If the request does not meet this handlers requirements it should return false to allow + * other upgrade handlers to inspect the request. + *

+ * If the request is invalid (e.g. security information is invalid) this should thrown an IoException. + * if this occurs no further handlers will be tried. + * + * @author Stuart Douglas + */ +public interface HttpUpgradeHandshake { + + /** + * Validates an upgrade request and returns any extra headers that should be added to the response. + * + * @param exchange the server exchange + * @return true if the handshake is valid and should be upgraded. False if it is invalid + * @throws IOException If the handshake is invalid + */ + boolean handleUpgrade(final HttpServerExchange exchange) throws IOException; + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/IPAddressAccessControlHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/IPAddressAccessControlHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/IPAddressAccessControlHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,409 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.regex.Pattern; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.StatusCodes; +import org.xnio.Bits; + +/** + * Handler that can accept or reject a request based on the IP address of the remote peer. + * + * @author Stuart Douglas + */ +public class IPAddressAccessControlHandler implements HttpHandler { + + /** + * Standard IP address + */ + private static final Pattern IP4_EXACT = Pattern.compile("(?:\\d{1,3}\\.){3}\\d{1,3}"); + + /** + * Standard IP address, with some octets replaced by a '*' + */ + private static final Pattern IP4_WILDCARD = Pattern.compile("(?:(?:\\d{1,3}|\\*)\\.){3}(?:\\d{1,3}|\\*)"); + + /** + * IPv4 address with subnet specified via slash notation + */ + private static final Pattern IP4_SLASH = Pattern.compile("(?:\\d{1,3}\\.){3}\\d{1,3}\\/\\d\\d?"); + + /** + * Standard full IPv6 address + */ + private static final Pattern IP6_EXACT = Pattern.compile("(?:[a-zA-Z0-9]{1,4}:){7}[a-zA-Z0-9]{1,4}"); + + /** + * Standard full IPv6 address, with some parts replaced by a '*' + */ + private static final Pattern IP6_WILDCARD = Pattern.compile("(?:(?:[a-zA-Z0-9]{1,4}|\\*):){7}(?:[a-zA-Z0-9]{1,4}|\\*)"); + + /** + * Standard full IPv6 address with subnet specified via slash notation + */ + private static final Pattern IP6_SLASH = Pattern.compile("(?:[a-zA-Z0-9]{1,4}:){7}[a-zA-Z0-9]{1,4}\\/\\d{1,3}"); + + private volatile HttpHandler next; + private volatile boolean defaultAllow = false; + private final int denyResponseCode; + private final List ipv6acl = new CopyOnWriteArrayList<>(); + private final List ipv4acl = new CopyOnWriteArrayList<>(); + + public IPAddressAccessControlHandler(final HttpHandler next) { + this(next, StatusCodes.FORBIDDEN); + } + + public IPAddressAccessControlHandler(final HttpHandler next, final int denyResponseCode) { + this.next = next; + this.denyResponseCode = denyResponseCode; + } + + public IPAddressAccessControlHandler() { + this.next = ResponseCodeHandler.HANDLE_404; + this.denyResponseCode = StatusCodes.FORBIDDEN; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + InetSocketAddress peer = exchange.getSourceAddress(); + if (isAllowed(peer.getAddress())) { + next.handleRequest(exchange); + } else { + exchange.setResponseCode(denyResponseCode); + exchange.endExchange(); + } + } + + boolean isAllowed(InetAddress address) { + if(address instanceof Inet4Address) { + for (PeerMatch rule : ipv4acl) { + if (rule.matches(address)) { + return !rule.isDeny(); + } + } + } else if(address instanceof Inet6Address) { + for (PeerMatch rule : ipv6acl) { + if (rule.matches(address)) { + return !rule.isDeny(); + } + } + } + return defaultAllow; + } + + public int getDenyResponseCode() { + return denyResponseCode; + } + + public boolean isDefaultAllow() { + return defaultAllow; + } + + public IPAddressAccessControlHandler setDefaultAllow(final boolean defaultAllow) { + this.defaultAllow = defaultAllow; + return this; + } + + public HttpHandler getNext() { + return next; + } + + public IPAddressAccessControlHandler setNext(final HttpHandler next) { + this.next = next; + return this; + } + + + /** + * Adds an allowed peer to the ACL list + *

+ * Peer can take several forms: + *

+ * a.b.c.d = Literal IPv4 Address + * a:b:c:d:e:f:g:h = Literal IPv6 Address + * a.b.* = Wildcard IPv4 Address + * a:b:* = Wildcard IPv6 Address + * a.b.c.0/24 = Classless wildcard IPv4 address + * a:b:c:d:e:f:g:0/120 = Classless wildcard IPv4 address + * + * @param peer The peer to add to the ACL + */ + public IPAddressAccessControlHandler addAllow(final String peer) { + return addRule(peer, false); + } + + /** + * Adds an denied peer to the ACL list + *

+ * Peer can take several forms: + *

+ * a.b.c.d = Literal IPv4 Address + * a:b:c:d:e:f:g:h = Literal IPv6 Address + * a.b.* = Wildcard IPv4 Address + * a:b:* = Wildcard IPv6 Address + * a.b.c.0/24 = Classless wildcard IPv4 address + * a:b:c:d:e:f:g:0/120 = Classless wildcard IPv4 address + * + * @param peer The peer to add to the ACL + */ + public IPAddressAccessControlHandler addDeny(final String peer) { + return addRule(peer, true); + } + + public IPAddressAccessControlHandler clearRules() { + this.ipv4acl.clear(); + this.ipv6acl.clear(); + return this; + } + + private IPAddressAccessControlHandler addRule(final String peer, final boolean deny) { + if (IP4_EXACT.matcher(peer).matches()) { + addIpV4ExactMatch(peer, deny); + } else if (IP4_WILDCARD.matcher(peer).matches()) { + addIpV4WildcardMatch(peer, deny); + } else if (IP4_SLASH.matcher(peer).matches()) { + addIpV4SlashPrefix(peer, deny); + } else if (IP6_EXACT.matcher(peer).matches()) { + addIpV6ExactMatch(peer, deny); + } else if (IP6_WILDCARD.matcher(peer).matches()) { + addIpV6WildcardMatch(peer, deny); + } else if (IP6_SLASH.matcher(peer).matches()) { + addIpV6SlashPrefix(peer, deny); + } else { + throw UndertowMessages.MESSAGES.notAValidIpPattern(peer); + } + return this; + } + + private void addIpV6SlashPrefix(final String peer, final boolean deny) { + String[] components = peer.split("\\/"); + String[] parts = components[0].split("\\:"); + int maskLen = Integer.parseInt(components[1]); + assert parts.length == 8; + + + byte[] pattern = new byte[16]; + byte[] mask = new byte[16]; + + for (int i = 0; i < 8; ++i) { + int val = Integer.parseInt(parts[i], 16); + pattern[i * 2] = (byte) (val >> 8); + pattern[i * 2 + 1] = (byte) (val & 0xFF); + } + for (int i = 0; i < 16; ++i) { + if (maskLen > 8) { + mask[i] = (byte) (0xFF); + maskLen -= 8; + } else if (maskLen != 0) { + mask[i] = (byte) (Bits.intBitMask(8 - maskLen, 7) & 0xFF); + maskLen = 0; + } else { + break; + } + } + ipv6acl.add(new PrefixIpV6PeerMatch(deny, peer, mask, pattern)); + } + + private void addIpV4SlashPrefix(final String peer, final boolean deny) { + String[] components = peer.split("\\/"); + String[] parts = components[0].split("\\."); + int maskLen = Integer.parseInt(components[1]); + final int mask = Bits.intBitMask(32 - maskLen, 31); + int prefix = 0; + for (int i = 0; i < 4; ++i) { + prefix <<= 8; + String part = parts[i]; + int no = Integer.parseInt(part); + prefix |= no; + } + ipv4acl.add(new PrefixIpV4PeerMatch(deny, peer, mask, prefix)); + } + + private void addIpV6WildcardMatch(final String peer, final boolean deny) { + byte[] pattern = new byte[16]; + byte[] mask = new byte[16]; + String[] parts = peer.split("\\:"); + assert parts.length == 8; + for (int i = 0; i < 8; ++i) { + if (!parts[i].equals("*")) { + int val = Integer.parseInt(parts[i], 16); + pattern[i * 2] = (byte) (val >> 8); + pattern[i * 2 + 1] = (byte) (val & 0xFF); + mask[i * 2] = (byte) (0xFF); + mask[i * 2 + 1] = (byte) (0xFF); + } + } + ipv6acl.add(new PrefixIpV6PeerMatch(deny, peer, mask, pattern)); + } + + private void addIpV4WildcardMatch(final String peer, final boolean deny) { + String[] parts = peer.split("\\."); + int mask = 0; + int prefix = 0; + for (int i = 0; i < 4; ++i) { + mask <<= 8; + prefix <<= 8; + String part = parts[i]; + if (!part.equals("*")) { + int no = Integer.parseInt(part); + mask |= 0xFF; + prefix |= no; + } + } + ipv4acl.add(new PrefixIpV4PeerMatch(deny, peer, mask, prefix)); + } + + private void addIpV6ExactMatch(final String peer, final boolean deny) { + byte[] bytes = new byte[16]; + String[] parts = peer.split("\\:"); + assert parts.length == 8; + for (int i = 0; i < 8; ++i) { + int val = Integer.parseInt(parts[i], 16); + bytes[i * 2] = (byte) (val >> 8); + bytes[i * 2 + 1] = (byte) (val & 0xFF); + } + ipv6acl.add(new ExactIpV6PeerMatch(deny, peer, bytes)); + } + + private void addIpV4ExactMatch(final String peer, final boolean deny) { + String[] parts = peer.split("\\."); + byte[] bytes = {(byte) Integer.parseInt(parts[0]), (byte) Integer.parseInt(parts[1]), (byte) Integer.parseInt(parts[2]), (byte) Integer.parseInt(parts[3])}; + ipv4acl.add(new ExactIpV4PeerMatch(deny, peer, bytes)); + } + + + abstract static class PeerMatch { + + private final boolean deny; + private final String pattern; + + protected PeerMatch(final boolean deny, final String pattern) { + this.deny = deny; + this.pattern = pattern; + } + + abstract boolean matches(final InetAddress address); + + boolean isDeny() { + return deny; + } + + @Override + public String toString() { + return getClass().getSimpleName() + "{" + + "deny=" + deny + + ", pattern='" + pattern + '\'' + + '}'; + } + } + + static class ExactIpV4PeerMatch extends PeerMatch { + + private final byte[] address; + + protected ExactIpV4PeerMatch(final boolean deny, final String pattern, final byte[] address) { + super(deny, pattern); + this.address = address; + } + + @Override + boolean matches(final InetAddress address) { + return Arrays.equals(address.getAddress(), this.address); + } + } + + static class ExactIpV6PeerMatch extends PeerMatch { + + private final byte[] address; + + protected ExactIpV6PeerMatch(final boolean deny, final String pattern, final byte[] address) { + super(deny, pattern); + this.address = address; + } + + @Override + boolean matches(final InetAddress address) { + return Arrays.equals(address.getAddress(), this.address); + } + } + + private static class PrefixIpV4PeerMatch extends PeerMatch { + + private final int mask; + private final int prefix; + + protected PrefixIpV4PeerMatch(final boolean deny, final String pattern, final int mask, final int prefix) { + super(deny, pattern); + this.mask = mask; + this.prefix = prefix; + } + + @Override + boolean matches(final InetAddress address) { + byte[] bytes = address.getAddress(); + if (bytes == null) { + return false; + } + int addressInt = ((bytes[0] & 0xFF) << 24) | ((bytes[1] & 0xFF) << 16) | ((bytes[2] & 0xFF) << 8) | (bytes[3] & 0xFF); + return (addressInt & mask) == prefix; + } + } + + static class PrefixIpV6PeerMatch extends PeerMatch { + + private final byte[] mask; + private final byte[] prefix; + + protected PrefixIpV6PeerMatch(final boolean deny, final String pattern, final byte[] mask, final byte[] prefix) { + super(deny, pattern); + this.mask = mask; + this.prefix = prefix; + assert mask.length == prefix.length; + } + + @Override + boolean matches(final InetAddress address) { + byte[] bytes = address.getAddress(); + if (bytes == null) { + return false; + } + if (bytes.length != mask.length) { + return false; + } + for (int i = 0; i < mask.length; ++i) { + if ((bytes[i] & mask[i]) != prefix[i]) { + return false; + } + } + return true; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/JDBCLogHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/JDBCLogHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/JDBCLogHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,387 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.UndertowLogger; +import io.undertow.security.api.SecurityContext; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; + +import javax.sql.DataSource; +import java.net.InetSocketAddress; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.ArrayList; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +public class JDBCLogHandler implements HttpHandler, Runnable { + + private final HttpHandler next; + private final String formatString; + private final ExchangeCompletionListener exchangeCompletionListener = new JDBCLogCompletionListener(); + + + private final Executor logWriteExecutor; + + private final Deque pendingMessages; + + //0 = not running + //1 = queued + //2 = running + @SuppressWarnings("unused") + private volatile int state = 0; + + private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater.newUpdater(JDBCLogHandler.class, "state"); + + protected boolean useLongContentLength = false; + + private final DataSource dataSource; + + private String tableName; + private String remoteHostField; + private String userField; + private String timestampField; + private String virtualHostField; + private String methodField; + private String queryField; + private String statusField; + private String bytesField; + private String refererField; + private String userAgentField; + + public JDBCLogHandler(final HttpHandler next, final Executor logWriteExecutor, final String formatString, DataSource dataSource) { + this.next = next; + this.formatString = formatString; + this.dataSource = dataSource; + + tableName = "access"; + remoteHostField = "remoteHost"; + userField = "userName"; + timestampField = "timestamp"; + virtualHostField = "virtualHost"; + methodField = "method"; + queryField = "query"; + statusField = "status"; + bytesField = "bytes"; + refererField = "referer"; + userAgentField = "userAgent"; + this.logWriteExecutor = logWriteExecutor; + this.pendingMessages = new ConcurrentLinkedDeque<>(); + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.addExchangeCompleteListener(exchangeCompletionListener); + next.handleRequest(exchange); + } + + private class JDBCLogCompletionListener implements ExchangeCompletionListener { + @Override + public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) { + try { + logMessage(formatString, exchange); + } finally { + nextListener.proceed(); + } + } + } + + public void logMessage(String pattern, HttpServerExchange exchange) { + JDBCLogAttribute jdbcLogAttribute = new JDBCLogAttribute(); + + if (pattern.equals("combined")) { + jdbcLogAttribute.pattern = pattern; + } + jdbcLogAttribute.remoteHost = ((InetSocketAddress) exchange.getConnection().getPeerAddress()).getAddress().getHostAddress(); + SecurityContext sc = exchange.getSecurityContext(); + if (sc == null || !sc.isAuthenticated()) { + jdbcLogAttribute.user = null; + } else { + jdbcLogAttribute.user = sc.getAuthenticatedAccount().getPrincipal().getName(); + } + jdbcLogAttribute.query = exchange.getQueryString(); + + jdbcLogAttribute.bytes = exchange.getResponseContentLength(); + if (jdbcLogAttribute.bytes < 0) + jdbcLogAttribute.bytes = 0; + + jdbcLogAttribute.status = exchange.getResponseCode(); + + if (jdbcLogAttribute.pattern.equals("combined")) { + jdbcLogAttribute.virtualHost = exchange.getRequestHeaders().getFirst(Headers.HOST); + jdbcLogAttribute.method = exchange.getRequestMethod().toString(); + jdbcLogAttribute.referer = exchange.getRequestHeaders().getFirst(Headers.REFERER); + jdbcLogAttribute.userAgent = exchange.getRequestHeaders().getFirst(Headers.USER_AGENT); + } + + this.pendingMessages.add(jdbcLogAttribute); + int state = stateUpdater.get(this); + if (state == 0) { + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + } + + /** + * insert the log record to database + */ + @Override + public void run() { + if (!stateUpdater.compareAndSet(this, 1, 2)) { + return; + } + + List messages = new ArrayList<>(); + JDBCLogAttribute msg = null; + + //only grab at most 1000 messages at a time + for (int i = 0; i < 1000; ++i) { + msg = pendingMessages.poll(); + if (msg == null) { + break; + } + messages.add(msg); + } + try { + if (!messages.isEmpty()) { + writeMessage(messages); + } + } finally { + stateUpdater.set(this, 0); + //check to see if there is still more messages + //if so then run this again + if (!pendingMessages.isEmpty()) { + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + } + } + + private void writeMessage(List messages) { + PreparedStatement ps = null; + Connection conn = null; + try { + conn = dataSource.getConnection(); + conn.setAutoCommit(true); + ps = prepareStatement(conn); + for (JDBCLogAttribute jdbcLogAttribute : messages) { + int numberOfTries = 2; + while (numberOfTries > 0) { + try { + ps.clearParameters(); + ps.setString(1, jdbcLogAttribute.remoteHost); + ps.setString(2, jdbcLogAttribute.user); + ps.setTimestamp(3, jdbcLogAttribute.timestamp); + ps.setString(4, jdbcLogAttribute.query); + ps.setInt(5, jdbcLogAttribute.status); + if (useLongContentLength) { + ps.setLong(6, jdbcLogAttribute.bytes); + } else { + if (jdbcLogAttribute.bytes > Integer.MAX_VALUE) + jdbcLogAttribute.bytes = -1; + ps.setInt(6, (int) jdbcLogAttribute.bytes); + } + ps.setString(7, jdbcLogAttribute.virtualHost); + ps.setString(8, jdbcLogAttribute.method); + ps.setString(9, jdbcLogAttribute.referer); + ps.setString(10, jdbcLogAttribute.userAgent); + + ps.executeUpdate(); + numberOfTries = 0; + } catch (SQLException e) { + UndertowLogger.ROOT_LOGGER.error(e); + } + numberOfTries--; + } + } + ps.close(); + } catch (SQLException e) { + UndertowLogger.ROOT_LOGGER.errorWritingJDBCLog(e); + } finally { + if (ps != null) { + try { + ps.close(); + } catch (SQLException e) { + UndertowLogger.ROOT_LOGGER.debug("Exception closing prepared statement", e); + } + } + if (conn != null) { + try { + conn.close(); + } catch (SQLException e) { + UndertowLogger.ROOT_LOGGER.debug("Exception closing connection", e); + } + } + } + } + + /** + * For tests only. Blocks the current thread until all messages are written + * Just does a busy wait. + *

+ * DO NOT USE THIS OUTSIDE OF A TEST + */ + void awaitWrittenForTest() throws InterruptedException { + while (!pendingMessages.isEmpty()) { + Thread.sleep(10); + } + while (state != 0) { + Thread.sleep(10); + } + } + + private PreparedStatement prepareStatement(Connection conn) throws SQLException { + return conn.prepareStatement + ("INSERT INTO " + tableName + " (" + + remoteHostField + ", " + userField + ", " + + timestampField + ", " + queryField + ", " + + statusField + ", " + bytesField + ", " + + virtualHostField + ", " + methodField + ", " + + refererField + ", " + userAgentField + + ") VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"); + } + + private class JDBCLogAttribute { + protected String remoteHost = ""; + protected String user = ""; + protected String query = ""; + protected long bytes = 0; + protected int status = 0; + protected String virtualHost = ""; + protected String method = ""; + protected String referer = ""; + protected String userAgent = ""; + protected String pattern = "common"; + protected Timestamp timestamp = new Timestamp(System.currentTimeMillis()); + } + + public boolean isUseLongContentLength() { + return useLongContentLength; + } + + public void setUseLongContentLength(boolean useLongContentLength) { + this.useLongContentLength = useLongContentLength; + } + + public String getTableName() { + return tableName; + } + + public void setTableName(String tableName) { + this.tableName = tableName; + } + + public String getRemoteHostField() { + return remoteHostField; + } + + public void setRemoteHostField(String remoteHostField) { + this.remoteHostField = remoteHostField; + } + + public String getUserField() { + return userField; + } + + public void setUserField(String userField) { + this.userField = userField; + } + + public String getTimestampField() { + return timestampField; + } + + public void setTimestampField(String timestampField) { + this.timestampField = timestampField; + } + + public String getVirtualHostField() { + return virtualHostField; + } + + public void setVirtualHostField(String virtualHostField) { + this.virtualHostField = virtualHostField; + } + + public String getMethodField() { + return methodField; + } + + public void setMethodField(String methodField) { + this.methodField = methodField; + } + + public String getQueryField() { + return queryField; + } + + public void setQueryField(String queryField) { + this.queryField = queryField; + } + + public String getStatusField() { + return statusField; + } + + public void setStatusField(String statusField) { + this.statusField = statusField; + } + + public String getBytesField() { + return bytesField; + } + + public void setBytesField(String bytesField) { + this.bytesField = bytesField; + } + + public String getRefererField() { + return refererField; + } + + public void setRefererField(String refererField) { + this.refererField = refererField; + } + + public String getUserAgentField() { + return userAgentField; + } + + public void setUserAgentField(String userAgentField) { + this.userAgentField = userAgentField; + } + + @Override + public String toString() { + return "JDBCLogHandler{" + + "formatString='" + formatString + '\'' + + '}'; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/MetricsHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/MetricsHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/MetricsHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,141 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +import java.util.Date; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +/** + * Handler that records some metrics + * + * @author Stuart Douglas + */ +public class MetricsHandler implements HttpHandler { + + public static final HandlerWrapper WRAPPER = new HandlerWrapper() { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new MetricsHandler(handler); + } + }; + + private volatile MetricResult totalResult = new MetricResult(new Date()); + private final HttpHandler next; + + public MetricsHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + final long start = System.currentTimeMillis(); + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + long time = System.currentTimeMillis() - start; + totalResult.update((int)time); + nextListener.proceed(); + } + }); + next.handleRequest(exchange); + } + + public void reset() { + this.totalResult = new MetricResult(new Date()); + } + + public MetricResult getMetrics() { + return new MetricResult(this.totalResult); + } + + public static class MetricResult { + + private static final AtomicLongFieldUpdater totalRequestTimeUpdater = AtomicLongFieldUpdater.newUpdater(MetricResult.class, "totalRequestTime"); + private static final AtomicIntegerFieldUpdater maxRequestTimeUpdater = AtomicIntegerFieldUpdater.newUpdater(MetricResult.class, "maxRequestTime"); + private static final AtomicIntegerFieldUpdater minRequestTimeUpdater = AtomicIntegerFieldUpdater.newUpdater(MetricResult.class, "minRequestTime"); + private static final AtomicLongFieldUpdater invocationsUpdater = AtomicLongFieldUpdater.newUpdater(MetricResult.class, "totalRequests"); + + private final Date metricsStartDate; + + private volatile long totalRequestTime; + private volatile int maxRequestTime; + private volatile int minRequestTime = -1; + private volatile long totalRequests; + + public MetricResult(Date metricsStartDate) { + this.metricsStartDate = metricsStartDate; + } + + public MetricResult(MetricResult copy) { + this.metricsStartDate = copy.metricsStartDate; + this.totalRequestTime = copy.totalRequestTime; + this.maxRequestTime = copy.maxRequestTime; + this.minRequestTime = copy.minRequestTime; + this.totalRequests = copy.totalRequests; + } + + void update(final int requestTime) { + totalRequestTimeUpdater.addAndGet(this, requestTime); + int maxRequestTime; + do { + maxRequestTime = this.maxRequestTime; + if (requestTime < maxRequestTime) { + break; + } + } while (!maxRequestTimeUpdater.compareAndSet(this, maxRequestTime, requestTime)); + + int minRequestTime; + do { + minRequestTime = this.minRequestTime; + if (requestTime > minRequestTime && minRequestTime != -1) { + break; + } + } while (!minRequestTimeUpdater.compareAndSet(this, minRequestTime, requestTime)); + invocationsUpdater.incrementAndGet(this); + + + } + + public Date getMetricsStartDate() { + return metricsStartDate; + } + + public long getTotalRequestTime() { + return totalRequestTime; + } + + public int getMaxRequestTime() { + return maxRequestTime; + } + + public int getMinRequestTime() { + return minRequestTime; + } + + public long getTotalRequests() { + return totalRequests; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/NameVirtualHostHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/NameVirtualHostHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/NameVirtualHostHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,84 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Map; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.Headers; + +/** + * A {@link HttpHandler} that implements virtual hosts based on the Host: http header + * header. + * + * @author Stuart Douglas + */ +public class NameVirtualHostHandler implements HttpHandler { + + private volatile HttpHandler defaultHandler = ResponseCodeHandler.HANDLE_404; + private final Map hosts = new CopyOnWriteMap<>(); + + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final String hostHeader = exchange.getRequestHeaders().getFirst(Headers.HOST); + if (hostHeader != null) { + String host; + if (hostHeader.contains(":")) { //header can be in host:port format + host = hostHeader.substring(0, hostHeader.lastIndexOf(":")); + } else { + host = hostHeader; + } + final HttpHandler handler = hosts.get(host); + if (handler != null) { + handler.handleRequest(exchange); + return; + } + } + defaultHandler.handleRequest(exchange); + } + + public HttpHandler getDefaultHandler() { + return defaultHandler; + } + + public Map getHosts() { + return hosts; + } + + public NameVirtualHostHandler setDefaultHandler(final HttpHandler defaultHandler) { + Handlers.handlerNotNull(defaultHandler); + this.defaultHandler = defaultHandler; + return this; + } + + public synchronized NameVirtualHostHandler addHost(final String host, final HttpHandler handler) { + Handlers.handlerNotNull(handler); + hosts.put(host, handler); + return this; + } + + public synchronized NameVirtualHostHandler removeHost(final String host) { + hosts.remove(host); + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/OriginHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/OriginHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/OriginHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,155 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import io.undertow.Handlers; +import io.undertow.UndertowLogger; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; + +/** + * A handler for the HTTP Origin (RFC 6454) header. + * + * @author Stuart Douglas + */ +public class OriginHandler implements HttpHandler { + + private volatile HttpHandler originFailedHandler = ResponseCodeHandler.HANDLE_403; + private volatile Set allowedOrigins = new HashSet<>(); + private volatile boolean requireAllOrigins = true; + private volatile boolean requireOriginHeader = true; + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final List origin = exchange.getRequestHeaders().get(Headers.ORIGIN); + if (origin == null) { + if (requireOriginHeader) { + //TODO: Is 403 (Forbidden) the best response code + if (UndertowLogger.REQUEST_LOGGER.isDebugEnabled()) { + UndertowLogger.REQUEST_LOGGER.debugf("Refusing request for %s due to lack of Origin: header", exchange.getRequestPath()); + } + originFailedHandler.handleRequest(exchange); + return; + } + } else { + boolean found = false; + final boolean requireAllOrigins = this.requireAllOrigins; + for (final String header : origin) { + if (allowedOrigins.contains(header)) { + found = true; + if (!requireAllOrigins) { + break; + } + } else if (requireAllOrigins) { + if (UndertowLogger.REQUEST_LOGGER.isDebugEnabled()) { + UndertowLogger.REQUEST_LOGGER.debugf("Refusing request for %s due to Origin %s not being in the allowed origins list", exchange.getRequestPath(), header); + } + originFailedHandler.handleRequest(exchange); + return; + } + } + if (!found) { + if (UndertowLogger.REQUEST_LOGGER.isDebugEnabled()) { + UndertowLogger.REQUEST_LOGGER.debugf("Refusing request for %s as none of the specified origins %s were in the allowed origins list", exchange.getRequestPath(), origin); + } + originFailedHandler.handleRequest(exchange); + return; + } + } + next.handleRequest(exchange); + } + + public synchronized OriginHandler addAllowedOrigin(final String origin) { + final Set allowedOrigins = new HashSet<>(this.allowedOrigins); + allowedOrigins.add(origin); + this.allowedOrigins = Collections.unmodifiableSet(allowedOrigins); + return this; + } + + public synchronized OriginHandler addAllowedOrigins(final Collection origins) { + final Set allowedOrigins = new HashSet<>(this.allowedOrigins); + allowedOrigins.addAll(origins); + this.allowedOrigins = Collections.unmodifiableSet(allowedOrigins); + return this; + } + + public synchronized OriginHandler addAllowedOrigins(final String... origins) { + final Set allowedOrigins = new HashSet<>(this.allowedOrigins); + allowedOrigins.addAll(Arrays.asList(origins)); + this.allowedOrigins = Collections.unmodifiableSet(allowedOrigins); + return this; + } + + public synchronized Set getAllowedOrigins() { + return allowedOrigins; + } + + public synchronized OriginHandler clearAllowedOrigins() { + this.allowedOrigins = Collections.emptySet(); + return this; + } + + public boolean isRequireAllOrigins() { + return requireAllOrigins; + } + + public OriginHandler setRequireAllOrigins(final boolean requireAllOrigins) { + this.requireAllOrigins = requireAllOrigins; + return this; + } + + public boolean isRequireOriginHeader() { + return requireOriginHeader; + } + + public OriginHandler setRequireOriginHeader(final boolean requireOriginHeader) { + this.requireOriginHeader = requireOriginHeader; + return this; + } + + public HttpHandler getNext() { + return next; + } + + public OriginHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + public HttpHandler getOriginFailedHandler() { + return originFailedHandler; + } + + public OriginHandler setOriginFailedHandler(HttpHandler originFailedHandler) { + Handlers.handlerNotNull(originFailedHandler); + this.originFailedHandler = originFailedHandler; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/PathHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/PathHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/PathHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,132 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.PathMatcher; + +/** + * Handler that dispatches to a given handler based of a prefix match of the path. + *

+ * This only matches a single level of a request, e.g if you have a request that takes the form: + *

+ * /foo/bar + *

+ * + * @author Stuart Douglas + */ +public class PathHandler implements HttpHandler { + + private final PathMatcher pathMatcher = new PathMatcher<>(); + + public PathHandler(final HttpHandler defaultHandler) { + pathMatcher.addPrefixPath("/", defaultHandler); + } + + public PathHandler() { + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + final PathMatcher.PathMatch match = pathMatcher.match(exchange.getRelativePath()); + if (match.getValue() == null) { + ResponseCodeHandler.HANDLE_404.handleRequest(exchange); + return; + } + exchange.setRelativePath(match.getRemaining()); + exchange.setResolvedPath(exchange.getRequestPath().substring(0, exchange.getRequestPath().length() - match.getRemaining().length())); + match.getValue().handleRequest(exchange); + } + + /** + * Adds a path prefix and a handler for that path. If the path does not start + * with a / then one will be prepended. + *

+ * The match is done on a prefix bases, so registering /foo will also match /bar. Exact + * path matches are taken into account first. + *

+ * If / is specified as the path then it will replace the default handler. + * + * @param path The path + * @param handler The handler + * @see #addPrefixPath(String, io.undertow.server.HttpHandler) + * @deprecated Superseded by {@link #addPrefixPath()}. + */ + @Deprecated + public synchronized PathHandler addPath(final String path, final HttpHandler handler) { + return addPrefixPath(path, handler); + } + + /** + * Adds a path prefix and a handler for that path. If the path does not start + * with a / then one will be prepended. + *

+ * The match is done on a prefix bases, so registering /foo will also match /foo/bar. + * Though exact path matches are taken into account before prefix path matches. So + * if an exact path match exists it's handler will be triggered. + *

+ * If / is specified as the path then it will replace the default handler. + * + * @param path If the request contains this prefix, run handler. + * @param handler The handler which is activated upon match. + * @return The resulting PathHandler after this path has been added to it. + */ + public synchronized PathHandler addPrefixPath(final String path, final HttpHandler handler) { + Handlers.handlerNotNull(handler); + pathMatcher.addPrefixPath(path, handler); + return this; + } + + /** + * If the request path is exactly equal to the given path, run the handler. + *

+ * Exact paths are prioritized higher than prefix paths. + * + * @param path If the request path is exactly this, run handler. + * @param handler Handler run upon exact path match. + * @return The resulting PathHandler after this path has been added to it. + */ + public synchronized PathHandler addExactPath(final String path, final HttpHandler handler) { + Handlers.handlerNotNull(handler); + pathMatcher.addExactPath(path, handler); + return this; + } + + @Deprecated + public synchronized PathHandler removePath(final String path) { + return removePrefixPath(path); + } + + public synchronized PathHandler removePrefixPath(final String path) { + pathMatcher.removePrefixPath(path); + return this; + } + + public synchronized PathHandler removeExactPath(final String path) { + pathMatcher.removeExactPath(path); + return this; + } + + public synchronized PathHandler clearPaths() { + pathMatcher.clearPaths(); + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/PathTemplateHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/PathTemplateHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/PathTemplateHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,103 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; +import io.undertow.util.PathTemplateMatch; +import io.undertow.util.PathTemplateMatcher; + +import java.util.Map; + +/** + * A handler that matches URI templates + * + * @author Stuart Douglas + * @see PathTemplateMatcher + */ +public class PathTemplateHandler implements HttpHandler { + + private final boolean rewriteQueryParameters; + + /** + * @see io.undertow.util.PathTemplateMatch#ATTACHMENT_KEY + */ + @Deprecated + public static final AttachmentKey PATH_TEMPLATE_MATCH = AttachmentKey.create(PathTemplateMatch.class); + + private final PathTemplateMatcher pathTemplateMatcher = new PathTemplateMatcher<>(); + + public PathTemplateHandler(boolean rewriteQueryParameters) { + this.rewriteQueryParameters = rewriteQueryParameters; + } + + public PathTemplateHandler() { + this(true); + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + PathTemplateMatcher.PathMatchResult match = pathTemplateMatcher.match(exchange.getRelativePath()); + if (match == null) { + ResponseCodeHandler.HANDLE_404.handleRequest(exchange); + return; + } + exchange.putAttachment(PATH_TEMPLATE_MATCH, new PathTemplateMatch(match.getMatchedTemplate(), match.getParameters())); + exchange.putAttachment(io.undertow.util.PathTemplateMatch.ATTACHMENT_KEY, new io.undertow.util.PathTemplateMatch(match.getMatchedTemplate(), match.getParameters())); + if (rewriteQueryParameters) { + for (Map.Entry entry : match.getParameters().entrySet()) { + exchange.addQueryParam(entry.getKey(), entry.getValue()); + } + } + match.getValue().handleRequest(exchange); + } + + public PathTemplateHandler add(final String uriTemplate, final HttpHandler handler) { + pathTemplateMatcher.add(uriTemplate, handler); + return this; + } + + public PathTemplateHandler remove(final String uriTemplate) { + pathTemplateMatcher.remove(uriTemplate); + return this; + } + + /** + * @see io.undertow.util.PathTemplateMatch + */ + @Deprecated + public static final class PathTemplateMatch { + private final String matchedTemplate; + private final Map parameters; + + public PathTemplateMatch(String matchedTemplate, Map parameters) { + this.matchedTemplate = matchedTemplate; + this.parameters = parameters; + } + + public String getMatchedTemplate() { + return matchedTemplate; + } + + public Map getParameters() { + return parameters; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/PeerNameResolvingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/PeerNameResolvingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/PeerNameResolvingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,145 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.UndertowLogger; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.security.PrivilegedExceptionAction; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * A handler that performs reverse DNS lookup to resolve a peer address + * + * @author Stuart Douglas + */ +public class PeerNameResolvingHandler implements HttpHandler { + + private final HttpHandler next; + private final ResolveType resolveType; + + public PeerNameResolvingHandler(HttpHandler next) { + this.next = next; + this.resolveType = ResolveType.FORWARD_AND_REVERSE; + } + + public PeerNameResolvingHandler(HttpHandler next, ResolveType resolveType) { + this.next = next; + this.resolveType = resolveType; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final InetSocketAddress address = exchange.getSourceAddress(); + if (address != null) { + if ((resolveType == ResolveType.FORWARD || resolveType == ResolveType.FORWARD_AND_REVERSE) + && address.isUnresolved()) { + try { + if (System.getSecurityManager() == null) { + final InetSocketAddress resolvedAddress = new InetSocketAddress(InetAddress.getByName(address.getHostName()), address.getPort()); + exchange.setSourceAddress(resolvedAddress); + } else { + AccessController.doPrivileged(new PrivilegedExceptionAction() { + @Override + public Object run() throws UnknownHostException { + final InetSocketAddress resolvedAddress = new InetSocketAddress(InetAddress.getByName(address.getHostName()), address.getPort()); + exchange.setSourceAddress(resolvedAddress); + return null; + } + }); + } + } catch (UnknownHostException e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Could not resolve hostname %s", address.getHostString()); + } + + } else if (resolveType == ResolveType.REVERSE || resolveType == ResolveType.FORWARD_AND_REVERSE) { + if (System.getSecurityManager() == null) { + address.getHostName(); + } else { + AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + address.getHostName(); + return null; + } + }); + } + //we call set source address because otherwise the underlying channel could just return a new address + exchange.setSourceAddress(address); + } + } + + next.handleRequest(exchange); + } + + public static enum ResolveType { + FORWARD, + REVERSE, + FORWARD_AND_REVERSE + + } + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "resolve-peer-name"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new PeerNameResolvingHandler(handler); + } + } + + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/PredicateContextHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/PredicateContextHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/PredicateContextHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.predicate.Predicate; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +import java.util.HashMap; + +/** + * Handler that sets up the predicate context + * + * @author Stuart Douglas + */ +public class PredicateContextHandler implements HttpHandler { + + private final HttpHandler next; + + public PredicateContextHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + exchange.putAttachment(Predicate.PREDICATE_CONTEXT, new HashMap()); + next.handleRequest(exchange); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/PredicateHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/PredicateHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/PredicateHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,72 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.predicate.Predicate; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +public class PredicateHandler implements HttpHandler { + + private volatile Predicate predicate; + private volatile HttpHandler trueHandler; + private volatile HttpHandler falseHandler; + + public PredicateHandler(final Predicate predicate, final HttpHandler trueHandler, final HttpHandler falseHandler) { + this.predicate = predicate; + this.trueHandler = trueHandler; + this.falseHandler = falseHandler; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + HttpHandler next = predicate.resolve(exchange) ? trueHandler : falseHandler; + next.handleRequest(exchange); + } + + public Predicate getPredicate() { + return predicate; + } + + public PredicateHandler setPredicate(final Predicate predicate) { + this.predicate = predicate; + return this; + } + + public HttpHandler getTrueHandler() { + return trueHandler; + } + + public PredicateHandler setTrueHandler(final HttpHandler trueHandler) { + this.trueHandler = trueHandler; + return this; + } + + public HttpHandler getFalseHandler() { + return falseHandler; + } + + public PredicateHandler setFalseHandler(final HttpHandler falseHandler) { + this.falseHandler = falseHandler; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/ProxyPeerAddressHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/ProxyPeerAddressHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/ProxyPeerAddressHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,121 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.Headers; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Handler that sets the peer address to the value of the X-Forwarded-For header. + *

+ * This should only be used behind a proxy that always sets this header, otherwise it + * is possible for an attacker to forge their peer address; + * + * @author Stuart Douglas + */ +public class ProxyPeerAddressHandler implements HttpHandler { + + private final HttpHandler next; + + public ProxyPeerAddressHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + String forwardedFor = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_FOR); + if (forwardedFor != null) { + int index = forwardedFor.indexOf(','); + final String value; + if (index == -1) { + value = forwardedFor; + } else { + value = forwardedFor.substring(0, index); + } + //we have no way of knowing the port + exchange.setSourceAddress(InetSocketAddress.createUnresolved(value, 0)); + } + String forwardedProto = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_PROTO); + if (forwardedProto != null) { + exchange.setRequestScheme(forwardedProto); + } + String forwardedHost = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_HOST); + String forwardedPort = exchange.getRequestHeaders().getFirst(Headers.X_FORWARDED_PORT); + if (forwardedHost != null) { + int index = forwardedHost.indexOf(','); + final String value; + if (index == -1) { + value = forwardedHost; + } else { + value = forwardedHost.substring(0, index); + } + int port = 0; + if(forwardedPort != null) { + port = Integer.parseInt(forwardedPort); + } + exchange.setDestinationAddress(InetSocketAddress.createUnresolved(value, port)); + } + next.handleRequest(exchange); + } + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "proxy-peer-address"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new ProxyPeerAddressHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/RedirectHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/RedirectHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/RedirectHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,116 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributeParser; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.Headers; + +/** + * A redirect handler that redirects to the specified location via a 302 redirect. + *

+ * The location is specified as an exchange attribute string. + * + * @author Stuart Douglas + * @see ExchangeAttributes + */ +public class RedirectHandler implements HttpHandler { + + private final ExchangeAttribute attribute; + + public RedirectHandler(final String location) { + ExchangeAttributeParser parser = ExchangeAttributes.parser(getClass().getClassLoader()); + attribute = parser.parse(location); + } + + public RedirectHandler(final String location, final ClassLoader classLoader) { + ExchangeAttributeParser parser = ExchangeAttributes.parser(classLoader); + attribute = parser.parse(location); + } + + public RedirectHandler(ExchangeAttribute attribute) { + this.attribute = attribute; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.setResponseCode(302); + exchange.getResponseHeaders().put(Headers.LOCATION, attribute.readAttribute(exchange)); + exchange.endExchange(); + } + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "redirect"; + } + + @Override + public Map> parameters() { + Map> params = new HashMap<>(); + params.put("value", ExchangeAttribute.class); + + return params; + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public HandlerWrapper build(Map config) { + + return new Wrapper((ExchangeAttribute)config.get("value")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final ExchangeAttribute value; + + private Wrapper(ExchangeAttribute value) { + this.value = value; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new RedirectHandler(value); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/RequestDumpingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/RequestDumpingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/RequestDumpingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,189 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Collections; +import java.util.Deque; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +import io.undertow.UndertowLogger; +import io.undertow.security.api.SecurityContext; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.LocaleUtils; + +/** + * Handler that dumps a exchange to a log. + * + * @author Stuart Douglas + */ +public class RequestDumpingHandler implements HttpHandler { + + private final HttpHandler next; + + public RequestDumpingHandler(final HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final StringBuilder sb = new StringBuilder(); +// Log pre-service information + final SecurityContext sc = exchange.getSecurityContext(); + sb.append("\n----------------------------REQUEST---------------------------\n"); + sb.append(" URI=" + exchange.getRequestURI() + "\n"); + sb.append(" characterEncoding=" + exchange.getRequestHeaders().get(Headers.CONTENT_ENCODING) + "\n"); + sb.append(" contentLength=" + exchange.getRequestContentLength() + "\n"); + sb.append(" contentType=" + exchange.getRequestHeaders().get(Headers.CONTENT_TYPE) + "\n"); + //sb.append(" contextPath=" + exchange.getContextPath()); + if (sc != null) { + if (sc.isAuthenticated()) { + sb.append(" authType=" + sc.getMechanismName() + "\n"); + sb.append(" principle=" + sc.getAuthenticatedAccount().getPrincipal() + "\n"); + } else { + sb.append(" authType=none" + "\n"); + } + } + + Map cookies = exchange.getRequestCookies(); + if (cookies != null) { + for (Map.Entry entry : cookies.entrySet()) { + Cookie cookie = entry.getValue(); + sb.append(" cookie=" + cookie.getName() + "=" + + cookie.getValue() + "\n"); + } + } + for (HeaderValues header : exchange.getRequestHeaders()) { + for (String value : header) { + sb.append(" header=" + header.getHeaderName() + "=" + value + "\n"); + } + } + sb.append(" locale=" + LocaleUtils.getLocalesFromHeader(exchange.getRequestHeaders().get(Headers.ACCEPT_LANGUAGE)) + "\n"); + sb.append(" method=" + exchange.getRequestMethod() + "\n"); + Map> pnames = exchange.getQueryParameters(); + for (Map.Entry> entry : pnames.entrySet()) { + String pname = entry.getKey(); + Iterator pvalues = entry.getValue().iterator(); + sb.append(" parameter="); + sb.append(pname); + sb.append('='); + while (pvalues.hasNext()) { + sb.append(pvalues.next()); + if (pvalues.hasNext()) { + sb.append(", "); + } + } + sb.append("\n"); + } + //sb.append(" pathInfo=" + exchange.getPathInfo()); + sb.append(" protocol=" + exchange.getProtocol() + "\n"); + sb.append(" queryString=" + exchange.getQueryString() + "\n"); + sb.append(" remoteAddr=" + exchange.getSourceAddress() + "\n"); + sb.append(" remoteHost=" + exchange.getSourceAddress().getHostName() + "\n"); + //sb.append("requestedSessionId=" + exchange.getRequestedSessionId()); + sb.append(" scheme=" + exchange.getRequestScheme() + "\n"); + sb.append(" host=" + exchange.getRequestHeaders().getFirst(Headers.HOST) + "\n"); + sb.append(" serverPort=" + exchange.getDestinationAddress().getPort() + "\n"); + //sb.append(" servletPath=" + exchange.getServletPath()); + //sb.append(" isSecure=" + exchange.isSecure()); + + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) { + // Log post-service information + sb.append("--------------------------RESPONSE--------------------------\n"); + if (sc != null) { + if (sc.isAuthenticated()) { + sb.append(" authType=" + sc.getMechanismName() + "\n"); + sb.append(" principle=" + sc.getAuthenticatedAccount().getPrincipal() + "\n"); + } else { + sb.append(" authType=none" + "\n"); + } + } + sb.append(" contentLength=" + exchange.getResponseContentLength() + "\n"); + sb.append(" contentType=" + exchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE) + "\n"); + Map cookies = exchange.getResponseCookies(); + if (cookies != null) { + for (Cookie cookie : cookies.values()) { + sb.append(" cookie=" + cookie.getName() + "=" + cookie.getValue() + "; domain=" + cookie.getDomain() + "; path=" + cookie.getPath() + "\n"); + } + } + for (HeaderValues header : exchange.getResponseHeaders()) { + for (String value : header) { + sb.append(" header=" + header.getHeaderName() + "=" + value + "\n"); + } + } + sb.append(" status=" + exchange.getResponseCode() + "\n"); + sb.append("=============================================================="); + + nextListener.proceed(); + UndertowLogger.REQUEST_DUMPER_LOGGER.info(sb.toString()); + } + }); + + + // Perform the exchange + next.handleRequest(exchange); + } + + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "dump-request"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new RequestDumpingHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/RequestLimit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/RequestLimit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/RequestLimit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,184 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.server.Connectors; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.SameThreadExecutor; + +import java.util.Queue; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.atomic.AtomicLongFieldUpdater; + +import static org.xnio.Bits.longBitMask; + +/** + * Represents a limit on a number of running requests. + *

+ * This is basically a counter with a configured set of limits, that is used by {@link RequestLimitingHandler}. + *

+ * When the number of active requests goes over the configured max requests then requests will be suspended and queued. + *

+ * If the queue is full requests will be rejected with a 513. + *

+ * The reason why this is abstracted out into a separate class is so that multiple handlers can share the same state. This + * allows for fine grained control of resources. + * + * @author Stuart Douglas + * @see RequestLimitingHandler + */ +public class RequestLimit { + @SuppressWarnings("unused") + private volatile long state; + + private static final AtomicLongFieldUpdater stateUpdater = AtomicLongFieldUpdater.newUpdater(RequestLimit.class, "state"); + + private static final long MASK_MAX = longBitMask(32, 63); + private static final long MASK_CURRENT = longBitMask(0, 30); + + /** + * The handler that will be invoked if the queue is full. + */ + private volatile HttpHandler failureHandler = new ResponseCodeHandler(513); + + private final Queue queue; + + private final ExchangeCompletionListener COMPLETION_LISTENER = new ExchangeCompletionListener() { + + @Override + public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) { + try { + final SuspendedRequest task = queue.poll(); + if (task != null) { + task.exchange.dispatch(task.next); + } else { + decrementRequests(); + } + } finally { + nextListener.proceed(); + } + } + }; + + + public RequestLimit(int maximumConcurrentRequests) { + this(maximumConcurrentRequests, -1); + } + + /** + * Construct a new instance. The maximum number of concurrent requests must be at least one. + * + * @param maximumConcurrentRequests the maximum concurrent requests + * @param queueSize The maximum number of requests to queue + */ + public RequestLimit(int maximumConcurrentRequests, int queueSize) { + if (maximumConcurrentRequests < 1) { + throw new IllegalArgumentException("Maximum concurrent requests must be at least 1"); + } + state = (maximumConcurrentRequests & 0xFFFFFFFFL) << 32; + + this.queue = new LinkedBlockingQueue<>(queueSize <= 0 ? Integer.MAX_VALUE : queueSize); + } + + public void handleRequest(final HttpServerExchange exchange, final HttpHandler next) throws Exception { + exchange.addExchangeCompleteListener(COMPLETION_LISTENER); + long oldVal, newVal; + do { + oldVal = state; + final long current = oldVal & MASK_CURRENT; + final long max = (oldVal & MASK_MAX) >> 32L; + if (current >= max) { + exchange.dispatch(SameThreadExecutor.INSTANCE, new Runnable() { + @Override + public void run() { + if (!queue.offer(new SuspendedRequest(exchange, next))) { + Connectors.executeRootHandler(failureHandler, exchange); + } + } + }); + return; + } + newVal = oldVal + 1; + } while (!stateUpdater.compareAndSet(this, oldVal, newVal)); + next.handleRequest(exchange); + } + + /** + * Get the maximum concurrent requests. + * + * @return the maximum concurrent requests + */ + public int getMaximumConcurrentRequests() { + return (int) (state >> 32L); + } + + /** + * Set the maximum concurrent requests. The value must be greater than or equal to one. + * + * @param newMax the maximum concurrent requests + */ + public int setMaximumConcurrentRequests(int newMax) { + if (newMax < 1) { + throw new IllegalArgumentException("Maximum concurrent requests must be at least 1"); + } + long oldVal, newVal; + int current, oldMax; + do { + oldVal = state; + current = (int) (oldVal & MASK_CURRENT); + oldMax = (int) ((oldVal & MASK_MAX) >> 32L); + newVal = current | newMax & 0xFFFFFFFFL << 32L; + } while (!stateUpdater.compareAndSet(this, oldVal, newVal)); + while (current < newMax) { + // more space opened up! Process queue entries for a while + final SuspendedRequest request = queue.poll(); + if (request != null) { + // now bump up the counter by one; this *could* put us over the max if it changed in the meantime but that's OK + newVal = stateUpdater.getAndIncrement(this); + current = (int) (newVal & MASK_CURRENT); + request.exchange.dispatch(request.next); + } + } + return oldMax; + } + + private void decrementRequests() { + stateUpdater.decrementAndGet(this); + } + + public HttpHandler getFailureHandler() { + return failureHandler; + } + + public void setFailureHandler(HttpHandler failureHandler) { + this.failureHandler = failureHandler; + } + + private static final class SuspendedRequest { + final HttpServerExchange exchange; + final HttpHandler next; + + private SuspendedRequest(HttpServerExchange exchange, HttpHandler next) { + this.exchange = exchange; + this.next = next; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/RequestLimitingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/RequestLimitingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/RequestLimitingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,137 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; + +/** + * A handler which limits the maximum number of concurrent requests. Requests beyond the limit will + * block until the previous request is complete. + * + * @author David M. Lloyd + */ +public final class RequestLimitingHandler implements HttpHandler { + private final HttpHandler nextHandler; + + private final RequestLimit requestLimit; + + /** + * Construct a new instance. The maximum number of concurrent requests must be at least one. The next handler + * must not be {@code null}. + * + * @param maximumConcurrentRequests the maximum concurrent requests + * @param nextHandler the next handler + */ + public RequestLimitingHandler(int maximumConcurrentRequests, HttpHandler nextHandler) { + this(maximumConcurrentRequests, -1, nextHandler); + } + + /** + * Construct a new instance. The maximum number of concurrent requests must be at least one. The next handler + * must not be {@code null}. + * + * @param maximumConcurrentRequests the maximum concurrent requests + * @param queueSize the maximum number of requests to queue + * @param nextHandler the next handler + */ + public RequestLimitingHandler(int maximumConcurrentRequests, int queueSize, HttpHandler nextHandler) { + if (nextHandler == null) { + throw new IllegalArgumentException("nextHandler is null"); + } + if (maximumConcurrentRequests < 1) { + throw new IllegalArgumentException("Maximum concurrent requests must be at least 1"); + } + this.requestLimit = new RequestLimit(maximumConcurrentRequests, queueSize); + this.nextHandler = nextHandler; + } + + /** + * Construct a new instance. This version takes a {@link RequestLimit} directly which may be shared with other + * handlers. + * + * @param requestLimit the request limit information. + * @param nextHandler the next handler + */ + public RequestLimitingHandler(RequestLimit requestLimit, HttpHandler nextHandler) { + if (nextHandler == null) { + throw new IllegalArgumentException("nextHandler is null"); + } + this.requestLimit = requestLimit; + this.nextHandler = nextHandler; + } + + public void handleRequest(final HttpServerExchange exchange) throws Exception { + requestLimit.handleRequest(exchange, nextHandler); + } + + public RequestLimit getRequestLimit() { + return requestLimit; + } + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "request-limit"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("requests", int.class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("requests"); + } + + @Override + public String defaultParameter() { + return "requests"; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper((Integer) config.get("requests")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final int requests; + + private Wrapper(int requests) { + this.requests = requests; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new RequestLimitingHandler(requests, handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/ResponseCodeHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/ResponseCodeHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/ResponseCodeHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,84 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import org.jboss.logging.Logger; + +/** + * A handler which simply sets a response code. + * + * @author David M. Lloyd + */ +public final class ResponseCodeHandler implements HttpHandler { + + private static final Logger log = Logger.getLogger(ResponseCodeHandler.class); + private static final boolean traceEnabled; + + static { + traceEnabled = log.isTraceEnabled(); + } + + /** + * A handler which sets a 200 code. This is the default response code, so in most cases + * this simply has the result of finishing the request + */ + public static final ResponseCodeHandler HANDLE_200 = new ResponseCodeHandler(200); + + /** + * A handler which sets a 403 code. + */ + public static final ResponseCodeHandler HANDLE_403 = new ResponseCodeHandler(403); + /** + * A handler which sets a 404 code. + */ + public static final ResponseCodeHandler HANDLE_404 = new ResponseCodeHandler(404); + /** + * A handler which sets a 405 code. + */ + public static final ResponseCodeHandler HANDLE_405 = new ResponseCodeHandler(405); + /** + * A handler which sets a 406 code. + */ + public static final ResponseCodeHandler HANDLE_406 = new ResponseCodeHandler(406); + /** + * A handler which sets a 500 code. + */ + public static final ResponseCodeHandler HANDLE_500 = new ResponseCodeHandler(500); + + private final int responseCode; + + /** + * Construct a new instance. + * + * @param responseCode the response code to set + */ + public ResponseCodeHandler(final int responseCode) { + this.responseCode = responseCode; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.setResponseCode(responseCode); + if(traceEnabled) { + log.tracef("Setting response code %s for exchange %s", responseCode, exchange); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/SSLHeaderHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/SSLHeaderHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/SSLHeaderHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,148 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.UndertowLogger; +import io.undertow.server.BasicSSLSessionInfo; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.Certificates; +import io.undertow.util.HeaderMap; + +import javax.security.cert.CertificateException; + +import static io.undertow.util.Headers.SSL_CIPHER; +import static io.undertow.util.Headers.SSL_CLIENT_CERT; +import static io.undertow.util.Headers.SSL_SESSION_ID; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * Handler that sets SSL information on the connection based on the following headers: + *

+ *

    + *
  • SSL_CLIENT_CERT
  • + *
  • SSL_CIPHER
  • + *
  • SSL_SESSION_ID
  • + *
+ *

+ * If this handler is present in the chain it will always override the SSL session information, + * even if these headers are not present. + *

+ * This handler MUST only be used on servers that are behind a reverse proxy, where the reverse proxy + * has been configured to always set these header for EVERY request (or strip existing headers with these + * names if no SSL information is present). Otherwise it may be possible for a malicious client to spoof + * a SSL connection. + * + * @author Stuart Douglas + */ +public class SSLHeaderHandler implements HttpHandler { + + public static final String HTTPS = "https"; + + private static final ExchangeCompletionListener CLEAR_SSL_LISTENER = new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + exchange.getConnection().setSslSessionInfo(null); + nextListener.proceed(); + } + }; + + private final HttpHandler next; + + public SSLHeaderHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + HeaderMap requestHeaders = exchange.getRequestHeaders(); + final String sessionId = requestHeaders.getFirst(SSL_SESSION_ID); + if (sessionId != null) { + final String cipher = requestHeaders.getFirst(SSL_CIPHER); + String clientCert = requestHeaders.getFirst(SSL_CLIENT_CERT); + //the proxy client replaces \n with ' ' + if (clientCert != null && clientCert.length() > 28) { + StringBuilder sb = new StringBuilder(clientCert.length() + 1); + sb.append(Certificates.BEGIN_CERT); + sb.append('\n'); + sb.append(clientCert.replace(' ', '\n').substring(28, clientCert.length() - 26));//core certificate data + sb.append('\n'); + sb.append(Certificates.END_CERT); + clientCert = sb.toString(); + } + + try { + SSLSessionInfo info = new BasicSSLSessionInfo(sessionId, cipher, clientCert); + exchange.setRequestScheme(HTTPS); + exchange.getConnection().setSslSessionInfo(info); + exchange.addExchangeCompleteListener(CLEAR_SSL_LISTENER); + } catch (java.security.cert.CertificateException e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Could not create certificate from header %s", clientCert); + } catch (CertificateException e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Could not create certificate from header %s", clientCert); + } + } + next.handleRequest(exchange); + } + + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "ssl-headers"; + } + + @Override + public Map> parameters() { + return Collections.emptyMap(); + } + + @Override + public Set requiredParameters() { + return Collections.emptySet(); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper(); + } + + } + + private static class Wrapper implements HandlerWrapper { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new SSLHeaderHandler(handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/SetAttributeHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/SetAttributeHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/SetAttributeHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,64 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributeParser; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; + +/** + * Handler that can set an arbitrary attribute on the exchange. Both the attribute and the + * value to set are expressed as exchange attributes. + * + * + * @author Stuart Douglas + */ +public class SetAttributeHandler implements HttpHandler { + + private final HttpHandler next; + private final ExchangeAttribute attribute; + private final ExchangeAttribute value; + + public SetAttributeHandler(HttpHandler next, ExchangeAttribute attribute, ExchangeAttribute value) { + this.next = next; + this.attribute = attribute; + this.value = value; + } + + public SetAttributeHandler(HttpHandler next, final String attribute, final String value) { + this.next = next; + ExchangeAttributeParser parser = ExchangeAttributes.parser(getClass().getClassLoader()); + this.attribute = parser.parseSingleToken(attribute); + this.value = parser.parse(value); + } + + public SetAttributeHandler(HttpHandler next, final String attribute, final String value, final ClassLoader classLoader) { + this.next = next; + ExchangeAttributeParser parser = ExchangeAttributes.parser(classLoader); + this.attribute = parser.parseSingleToken(attribute); + this.value = parser.parse(value); + } + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + attribute.writeAttribute(exchange, value.readAttribute(exchange)); + next.handleRequest(exchange); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/SetHeaderHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/SetHeaderHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/SetHeaderHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HttpString; + +/** + * Set a fixed response header. + * + * @author Stuart Douglas + */ +public class SetHeaderHandler implements HttpHandler { + + private final HttpString header; + private final String value; + private final HttpHandler next; + + public SetHeaderHandler(final String header, final String value) { + this.next = ResponseCodeHandler.HANDLE_404; + this.value = value; + this.header = new HttpString(header); + } + + public SetHeaderHandler(final HttpHandler next, final String header, final String value) { + this.next = next; + this.value = value; + this.header = new HttpString(header); + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.getResponseHeaders().put(header, value); + next.handleRequest(exchange); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/URLDecodingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/URLDecodingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/URLDecodingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers; + +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Map; +import java.util.TreeMap; + +import io.undertow.UndertowOptions; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.URLUtils; + +/** + * A handler that will decode the URL and query parameters to the specified charset. + *

+ * If you are using this handler you must set the {@link io.undertow.UndertowOptions#DECODE_URL} parameter to false. + *

+ * This is not as efficient as using the parsers built in UTF-8 decoder. Unless you need to decode to something other + * than UTF-8 you should rely on the parsers decoding instead. + * + * @author Stuart Douglas + */ +public class URLDecodingHandler implements HttpHandler { + + private final HttpHandler next; + private final String charset; + + public URLDecodingHandler(final HttpHandler next, final String charset) { + this.next = next; + this.charset = charset; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + boolean decodeDone = exchange.getConnection().getUndertowOptions().get(UndertowOptions.DECODE_URL, true); + if (!decodeDone) { + final StringBuilder sb = new StringBuilder(); + final boolean decodeSlash = exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALLOW_ENCODED_SLASH, false); + exchange.setRequestPath(URLUtils.decode(exchange.getRequestPath(), charset, decodeSlash, sb)); + exchange.setRelativePath(URLUtils.decode(exchange.getRelativePath(), charset, decodeSlash, sb)); + exchange.setResolvedPath(URLUtils.decode(exchange.getResolvedPath(), charset, decodeSlash, sb)); + if (!exchange.getQueryString().isEmpty()) { + final TreeMap> newParams = new TreeMap<>(); + for (Map.Entry> param : exchange.getQueryParameters().entrySet()) { + final Deque newVales = new ArrayDeque<>(param.getValue().size()); + for (String val : param.getValue()) { + newVales.add(URLUtils.decode(val, charset, true, sb)); + } + newParams.put(URLUtils.decode(param.getKey(), charset, true, sb), newVales); + } + exchange.getQueryParameters().clear(); + exchange.getQueryParameters().putAll(newParams); + } + } + next.handleRequest(exchange); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/AccessLogHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/AccessLogHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/AccessLogHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,182 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.accesslog; + + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.attribute.SubstituteEmptyWrapper; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; + +/** + * Access log handler. This handler will generate access log messages based on the provided format string, + * and pass these messages into the provided {@link AccessLogReceiver}. + *

+ * This handler can log any attribute that is provides via the {@link io.undertow.attribute.ExchangeAttribute} + * mechanism. A general guide to the most common attribute is provided before, however this mechanism is extensible. + *

+ *

+ *

This factory produces token handlers for the following patterns

+ *
    + *
  • %a - Remote IP address + *
  • %A - Local IP address + *
  • %b - Bytes sent, excluding HTTP headers, or '-' if no bytes + * were sent + *
  • %B - Bytes sent, excluding HTTP headers + *
  • %h - Remote host name + *
  • %H - Request protocol + *
  • %l - Remote logical username from identd (always returns '-') + *
  • %m - Request method + *
  • %p - Local port + *
  • %q - Query string (excluding the '?' character) + *
  • %r - First line of the request + *
  • %s - HTTP status code of the response + *
  • %t - Date and time, in Common Log Format format + *
  • %u - Remote user that was authenticated + *
  • %U - Requested URL path + *
  • %v - Local server name + *
  • %D - Time taken to process the request, in millis + *
  • %T - Time taken to process the request, in seconds + *
  • %I - current Request thread name (can compare later with stacktraces) + *
+ *

In addition, the caller can specify one of the following aliases for + * commonly utilized patterns:

+ *
    + *
  • common - %h %l %u %t "%r" %s %b + *
  • combined - + * %h %l %u %t "%r" %s %b "%{i,Referer}" "%{i,User-Agent}" + *
+ *

+ *

+ * There is also support to write information from the cookie, incoming + * header, or the session
+ * It is modeled after the apache syntax: + *

    + *
  • %{i,xxx} for incoming headers + *
  • %{o,xxx} for outgoing response headers + *
  • %{c,xxx} for a specific cookie + *
  • %{r,xxx} xxx is an attribute in the ServletRequest + *
  • %{s,xxx} xxx is an attribute in the HttpSession + *
+ *

+ * + * @author Stuart Douglas + */ +public class AccessLogHandler implements HttpHandler { + + private final HttpHandler next; + private final AccessLogReceiver accessLogReceiver; + private final String formatString; + private final ExchangeAttribute tokens; + private final ExchangeCompletionListener exchangeCompletionListener = new AccessLogCompletionListener(); + + public AccessLogHandler(final HttpHandler next, final AccessLogReceiver accessLogReceiver, final String formatString, ClassLoader classLoader) { + this.next = next; + this.accessLogReceiver = accessLogReceiver; + this.formatString = handleCommonNames(formatString); + this.tokens = ExchangeAttributes.parser(classLoader, new SubstituteEmptyWrapper("-")).parse(this.formatString); + } + + private static String handleCommonNames(String formatString) { + if(formatString.equals("common")) { + return "%h %l %u %t \"%r\" %s %b"; + } else if (formatString.equals("combined")) { + return "%h %l %u %t \"%r\" %s %b \"%{i,Referer}\" \"%{i,User-Agent}\""; + } + return formatString; + } + + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.addExchangeCompleteListener(exchangeCompletionListener); + next.handleRequest(exchange); + } + + private class AccessLogCompletionListener implements ExchangeCompletionListener { + @Override + public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) { + try { + accessLogReceiver.logMessage(tokens.readAttribute(exchange)); + } finally { + nextListener.proceed(); + } + } + } + + @Override + public String toString() { + return "AccessLogHandler{" + + "formatString='" + formatString + '\'' + + '}'; + } + + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "access-log"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("format", String.class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("format"); + } + + @Override + public String defaultParameter() { + return "format"; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper((String) config.get("format")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final String format; + + private Wrapper(String format) { + this.format = format; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new AccessLogHandler(handler, new JBossLoggingAccessLogReceiver(), format, Wrapper.class.getClassLoader()); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/AccessLogReceiver.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/AccessLogReceiver.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/AccessLogReceiver.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,32 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.accesslog; + +/** + * Interface that is used by the access log handler to send data to the log file manager. + * + * Implementations of this interface must be thread safe. + * + * @author Stuart Douglas + */ +public interface AccessLogReceiver { + + void logMessage(final String message); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,222 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.accesslog; + +import java.io.BufferedWriter; +import java.io.Closeable; +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.Writer; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.Deque; +import java.util.List; +import java.util.concurrent.ConcurrentLinkedDeque; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import io.undertow.UndertowLogger; + +/** + * Log Receiver that stores logs in a directory under the specified file name, and rotates them after + * midnight. + *

+ * Web threads do not touch the log file, but simply queue messages to be written later by a worker thread. + * A lightweight CAS based locking mechanism is used to ensure than only 1 thread is active writing messages at + * any given time + * + * @author Stuart Douglas + */ +public class DefaultAccessLogReceiver implements AccessLogReceiver, Runnable, Closeable { + private static final String DEFAULT_LOG_SUFFIX = ".log"; + + private final Executor logWriteExecutor; + + private final Deque pendingMessages; + + //0 = not running + //1 = queued + //2 = running + @SuppressWarnings("unused") + private volatile int state = 0; + + private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater.newUpdater(DefaultAccessLogReceiver.class, "state"); + + private long changeOverPoint; + private String currentDateString; + private boolean forceLogRotation; + + private final File outputDirectory; + private final File defaultLogFile; + + private final String logBaseName; + private final String logNameSuffix; + + private Writer writer = null; + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName) { + this(logWriteExecutor, outputDirectory, logBaseName, null); + } + + public DefaultAccessLogReceiver(final Executor logWriteExecutor, final File outputDirectory, final String logBaseName, final String logNameSuffix) { + this.logWriteExecutor = logWriteExecutor; + this.outputDirectory = outputDirectory; + this.logBaseName = logBaseName; + this.logNameSuffix = (logNameSuffix != null) ? logNameSuffix : DEFAULT_LOG_SUFFIX; + this.pendingMessages = new ConcurrentLinkedDeque<>(); + this.defaultLogFile = new File(outputDirectory, logBaseName + this.logNameSuffix); + calculateChangeOverPoint(); + } + + private void calculateChangeOverPoint() { + Calendar calendar = Calendar.getInstance(); + calendar.set(Calendar.SECOND, 59); + calendar.set(Calendar.MINUTE, 59); + calendar.set(Calendar.HOUR, 23); + changeOverPoint = calendar.getTimeInMillis(); + SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd"); + currentDateString = df.format(new Date()); + } + + @Override + public void logMessage(final String message) { + this.pendingMessages.add(message); + int state = stateUpdater.get(this); + if (state == 0) { + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + } + + /** + * processes all queued log messages + */ + @Override + public void run() { + if (!stateUpdater.compareAndSet(this, 1, 2)) { + return; + } + if (forceLogRotation) { + doRotate(); + } + List messages = new ArrayList<>(); + String msg = null; + //only grab at most 1000 messages at a time + for (int i = 0; i < 1000; ++i) { + msg = pendingMessages.poll(); + if (msg == null) { + break; + } + messages.add(msg); + } + try { + if (!messages.isEmpty()) { + writeMessage(messages); + } + } finally { + stateUpdater.set(this, 0); + //check to see if there is still more messages + //if so then run this again + if (!pendingMessages.isEmpty() || forceLogRotation) { + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + } + } + + /** + * For tests only. Blocks the current thread until all messages are written + * Just does a busy wait. + *

+ * DO NOT USE THIS OUTSIDE OF A TEST + */ + void awaitWrittenForTest() throws InterruptedException { + while (!pendingMessages.isEmpty() || forceLogRotation) { + Thread.sleep(10); + } + while (state != 0) { + Thread.sleep(10); + } + } + + private void writeMessage(final List messages) { + if (System.currentTimeMillis() > changeOverPoint) { + doRotate(); + } + try { + if (writer == null) { + writer = new BufferedWriter(new FileWriter(defaultLogFile, true)); + } + for (String message : messages) { + writer.write(message); + writer.write('\n'); + } + writer.flush(); + } catch (IOException e) { + UndertowLogger.ROOT_LOGGER.errorWritingAccessLog(e); + } + } + + private void doRotate() { + forceLogRotation = false; + try { + if (writer != null) { + writer.flush(); + writer.close(); + writer = null; + } + File newFile = new File(outputDirectory, logBaseName + "_" + currentDateString + logNameSuffix); + int count = 0; + while (newFile.exists()) { + ++count; + newFile = new File(outputDirectory, logBaseName + "_" + currentDateString + "-" + count + logNameSuffix); + } + if (!defaultLogFile.renameTo(newFile)) { + UndertowLogger.ROOT_LOGGER.errorRotatingAccessLog(new IOException()); + } + } catch (IOException e) { + UndertowLogger.ROOT_LOGGER.errorRotatingAccessLog(e); + } finally { + calculateChangeOverPoint(); + } + } + + /** + * forces a log rotation. This rotation is performed in an async manner, you cannot rely on the rotation + * being performed immediately after this method returns. + */ + public void rotate() { + forceLogRotation = true; + if (stateUpdater.compareAndSet(this, 0, 1)) { + logWriteExecutor.execute(this); + } + } + + @Override + public void close() throws IOException { + writer.flush(); + writer.close(); + writer = null; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/JBossLoggingAccessLogReceiver.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/JBossLoggingAccessLogReceiver.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/accesslog/JBossLoggingAccessLogReceiver.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.accesslog; + +import org.jboss.logging.Logger; + +/** + * Access log receiver that logs messages at INFO level. + * + * @author Stuart Douglas + */ +public class JBossLoggingAccessLogReceiver implements AccessLogReceiver { + + public static final String DEFAULT_CATEGORY = "io.undertow.accesslog"; + + private final Logger logger; + + public JBossLoggingAccessLogReceiver(final String category) { + this.logger = Logger.getLogger(category); + } + + public JBossLoggingAccessLogReceiver() { + this.logger = Logger.getLogger(DEFAULT_CATEGORY); + } + + @Override + public void logMessage(String message) { + logger.info(message); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/HandlerBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/HandlerBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/HandlerBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,63 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.server.HandlerWrapper; +import java.util.Map; +import java.util.Set; + +/** + * Interface that provides a way of providing a textual representation of a handler. + * + * @author Stuart Douglas + */ +public interface HandlerBuilder { + + /** + * The string representation of the handler name. + * + * @return The handler name + */ + String name(); + + /** + * Returns a map of parameters and their types. + */ + Map> parameters(); + + /** + * @return The required parameters + */ + Set requiredParameters(); + + /** + * @return The default parameter name, or null if it does not have a default parameter + */ + String defaultParameter(); + + /** + * Creates the handler + * + * @param config The handler config + * @return The new predicate + */ + HandlerWrapper build(final Map config); + + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/HandlerParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/HandlerParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/HandlerParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,400 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.UndertowMessages; +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributeParser; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HandlerWrapper; + +import java.lang.reflect.Array; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.ServiceLoader; +import java.util.Set; + +/** + * Parser that can build a handler from a string representation. The underlying syntax is quite simple, and example is + * shown below: + *

+ * + * rewrite[value="/path"] + * + * If a handler is only being passed a single parameter then the parameter name can be omitted. + * Strings can be enclosed in optional double or single quotations marks, and quotation marks can be escaped using + * \". + *

+ * Array types are represented via a comma separated list of values enclosed in curly braces. + *

+ * TODO: some way of + * + * @author Stuart Douglas + */ +public class HandlerParser { + + + public static final HandlerWrapper parse(String string, final ClassLoader classLoader) { + final Map builders = loadBuilders(classLoader); + final ExchangeAttributeParser attributeParser = ExchangeAttributes.parser(classLoader); + return parse(string, builders, attributeParser); + } + + private static Map loadBuilders(final ClassLoader classLoader) { + ServiceLoader loader = ServiceLoader.load(HandlerBuilder.class, classLoader); + final Map ret = new HashMap<>(); + for (HandlerBuilder builder : loader) { + if (ret.containsKey(builder.name())) { + if (ret.get(builder.name()).getClass() != builder.getClass()) { + throw UndertowMessages.MESSAGES.moreThanOneHandlerWithName(builder.name(), builder.getClass(), ret.get(builder.name()).getClass()); + } + } else { + ret.put(builder.name(), builder); + } + } + return ret; + } + + private static IllegalStateException error(final String string, int pos, String reason) { + StringBuilder b = new StringBuilder(); + b.append(string); + b.append('\n'); + for (int i = 0; i < pos; ++i) { + b.append(' '); + } + b.append('^'); + throw UndertowMessages.MESSAGES.errorParsingHandlerString(reason, b.toString()); + } + + static HandlerWrapper parse(final String string, final Map builders, final ExchangeAttributeParser attributeParser) { + + //shunting yard algorithm + //gets rid or parentheses and fixes up operator ordering + Deque tokens = tokenize(string); + return parseBuilder(string, tokens.pop(), tokens, builders, attributeParser); + + } + + private static HandlerWrapper parseBuilder(final String string, final Token token, final Deque tokens, final Map builders, final ExchangeAttributeParser attributeParser) { + HandlerBuilder builder = builders.get(token.token); + if (builder == null) { + throw error(string, token.position, "no predicate named " + token.token); + } + Token next = tokens.peek(); + if (next.token.equals("[")) { + final Map values = new HashMap<>(); + + tokens.poll(); + next = tokens.poll(); + if (next == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + if (next.token.equals("{")) { + return handleSingleArrayValue(string, builder, tokens, next, attributeParser); + } + while (!next.token.equals("]")) { + Token equals = tokens.poll(); + if (!equals.token.equals("=")) { + if (equals.token.equals("]") && values.isEmpty()) { + //single value case + return handleSingleValue(string, builder, next, attributeParser); + } else if (equals.token.equals(",")) { + tokens.push(equals); + tokens.push(next); + return handleSingleVarArgsValue(string, builder, tokens, next, attributeParser); + } + throw error(string, equals.position, "Unexpected token"); + } + Token value = tokens.poll(); + if (value == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + if (value.token.equals("{")) { + values.put(next.token, readArrayType(string, tokens, next, builder, attributeParser, "}")); + } else { + if (isOperator(value.token) || isSpecialChar(value.token)) { + throw error(string, value.position, "Unexpected token"); + } + + Class type = builder.parameters().get(next.token); + if (type == null) { + throw error(string, next.position, "Unexpected parameter " + next.token); + } + values.put(next.token, coerceToType(string, value, type, attributeParser)); + } + + next = tokens.poll(); + if (next == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + if (!next.token.equals("]")) { + if (!next.token.equals(",")) { + throw error(string, string.length(), "Expecting , or ]"); + } + next = tokens.poll(); + if (next == null) { + throw error(string, string.length(), "Unexpected end of input"); + } + } + } + checkParameters(string, next.position, values, builder); + return builder.build(values); + + } else { + throw error(string, next.position, "Unexpected character"); + } + } + + private static HandlerWrapper handleSingleArrayValue(final String string, final HandlerBuilder builder, final Deque tokens, final Token token, final ExchangeAttributeParser attributeParser) { + String sv = builder.defaultParameter(); + if (sv == null) { + throw error(string, token.position, "default parameter not supported"); + } + Object array = readArrayType(string, tokens, new Token(sv, token.position), builder, attributeParser, "}"); + Token close = tokens.poll(); + if (!close.token.equals("]")) { + throw error(string, close.position, "expected ]"); + } + return builder.build(Collections.singletonMap(sv, array)); + } + + private static HandlerWrapper handleSingleVarArgsValue(final String string, final HandlerBuilder builder, final Deque tokens, final Token token, final ExchangeAttributeParser attributeParser) { + String sv = builder.defaultParameter(); + if (sv == null) { + throw error(string, token.position, "default parameter not supported"); + } + Object array = readArrayType(string, tokens, new Token(sv, token.position), builder, attributeParser, "]"); + return builder.build(Collections.singletonMap(sv, array)); + } + + private static Object readArrayType(final String string, final Deque tokens, Token paramName, HandlerBuilder builder, final ExchangeAttributeParser attributeParser, String expectedEndToken) { + Class type = builder.parameters().get(paramName.token); + if (type == null) { + throw error(string, paramName.position, "no parameter called " + paramName.token); + } else if (!type.isArray()) { + throw error(string, paramName.position, "parameter is not an array type " + paramName.token); + } + + Class componentType = type.getComponentType(); + final List values = new ArrayList<>(); + Token token = tokens.poll(); + while (token != null) { + Token commaOrEnd = tokens.poll(); + values.add(coerceToType(string, token, componentType, attributeParser)); + if (commaOrEnd.token.equals(expectedEndToken)) { + Object array = Array.newInstance(componentType, values.size()); + for (int i = 0; i < values.size(); ++i) { + Array.set(array, i, values.get(i)); + } + return array; + } else if (!commaOrEnd.token.equals(",")) { + throw error(string, commaOrEnd.position, "expected either , or }"); + } + token = tokens.poll(); + } + throw error(string, string.length(), "unexpected end of input in array"); + } + + + private static HandlerWrapper handleSingleValue(final String string, final HandlerBuilder builder, final Token next, final ExchangeAttributeParser attributeParser) { + String sv = builder.defaultParameter(); + if (sv == null) { + throw error(string, next.position, "default parameter not supported"); + } + Map values = Collections.singletonMap(sv, coerceToType(string, next, builder.parameters().get(sv), attributeParser)); + checkParameters(string, next.position, values, builder); + return builder.build(values); + } + + private static void checkParameters(final String string, int pos, final Map values, final HandlerBuilder builder) { + final Set required = new HashSet<>(builder.requiredParameters()); + for (String key : values.keySet()) { + required.remove(key); + } + if (!required.isEmpty()) { + throw error(string, pos, "Missing required parameters " + required); + } + } + + + private static Object coerceToType(final String string, final Token token, final Class type, final ExchangeAttributeParser attributeParser) { + if (type.isArray()) { + Object array = Array.newInstance(type.getComponentType(), 1); + Array.set(array, 0, coerceToType(string, token, type.getComponentType(), attributeParser)); + return array; + } + + if (type == String.class) { + return token.token; + } else if (type.equals(Boolean.class) || type.equals(boolean.class)) { + return Boolean.valueOf(token.token); + } else if (type.equals(Byte.class) || type.equals(byte.class)) { + return Byte.valueOf(token.token); + } else if (type.equals(Character.class) || type.equals(char.class)) { + if (token.token.length() != 1) { + throw error(string, token.position, "Cannot coerce " + token.token + " to a Character"); + } + return Character.valueOf(token.token.charAt(0)); + } else if (type.equals(Short.class) || type.equals(short.class)) { + return Short.valueOf(token.token); + } else if (type.equals(Integer.class) || type.equals(int.class)) { + return Integer.valueOf(token.token); + } else if (type.equals(Long.class) || type.equals(long.class)) { + return Long.valueOf(token.token); + } else if (type.equals(Float.class) || type.equals(float.class)) { + return Float.valueOf(token.token); + } else if (type.equals(Double.class) || type.equals(double.class)) { + return Double.valueOf(token.token); + } else if (type.equals(ExchangeAttribute.class)) { + return attributeParser.parse(token.token); + } + + return token.token; + } + + private static int precedence(String operator) { + if (operator.equals("not")) { + return 3; + } else if (operator.equals("and")) { + return 2; + } else if (operator.equals("or")) { + return 1; + } + throw new IllegalStateException(); + } + + + private static boolean isOperator(final String op) { + return op.equals("and") || op.equals("or") || op.equals("not"); + } + + private static boolean isSpecialChar(String token) { + if (token.length() != 1) { + return false; + } + char c = token.charAt(0); + switch (c) { + case '(': + case ')': + case ',': + case '=': + case '{': + case '}': + case '[': + case ']': + return true; + default: + return false; + } + } + + static Deque tokenize(final String string) { + char currentStringDelim = 0; + boolean inVariable = false; + + int pos = 0; + StringBuilder current = new StringBuilder(); + Deque ret = new ArrayDeque<>(); + while (pos < string.length()) { + char c = string.charAt(pos); + if (currentStringDelim != 0) { + if (c == currentStringDelim && current.charAt(current.length() - 1) != '\\') { + ret.add(new Token(current.toString(), pos)); + current.setLength(0); + currentStringDelim = 0; + } else { + current.append(c); + } + } else { + switch (c) { + case ' ': + case '\t': { + if (current.length() != 0) { + ret.add(new Token(current.toString(), pos)); + current.setLength(0); + } + break; + } + case '(': + case ')': + case ',': + case '=': + case '[': + case ']': + case '{': + case '}': { + if (inVariable) { + current.append(c); + if (c == '}') { + inVariable = false; + } + } else { + if (current.length() != 0) { + ret.add(new Token(current.toString(), pos)); + current.setLength(0); + } + ret.add(new Token("" + c, pos)); + } + break; + } + case '"': + case '\'': { + if (current.length() != 0) { + throw error(string, pos, "Unexpected token"); + } + currentStringDelim = c; + break; + } + case '%': { + current.append(c); + if (string.charAt(pos + 1) == '{') { + inVariable = true; + } + break; + } + default: + current.append(c); + } + } + ++pos; + } + if (current.length() > 0) { + ret.add(new Token(current.toString(), string.length())); + } + return ret; + } + + + static final class Token { + final String token; + final int position; + + private Token(final String token, final int position) { + this.token = token; + this.position = position; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/PredicatedHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/PredicatedHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/PredicatedHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.predicate.Predicate; +import io.undertow.server.HandlerWrapper; + +/** + * @author Stuart Douglas + */ +public class PredicatedHandler { + private final Predicate predicate; + private final HandlerWrapper handler; + + public PredicatedHandler(Predicate predicate, HandlerWrapper handler) { + this.predicate = predicate; + this.handler = handler; + } + + public Predicate getPredicate() { + return predicate; + } + + public HandlerWrapper getHandler() { + return handler; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/PredicatedHandlersParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/PredicatedHandlersParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/PredicatedHandlersParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,82 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.predicate.Predicate; +import io.undertow.predicate.PredicateParser; +import io.undertow.predicate.Predicates; +import io.undertow.server.HandlerWrapper; +import io.undertow.util.ChaninedHandlerWrapper; +import io.undertow.util.FileUtils; + +import java.io.File; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Parser for the undertow-handlers.conf file. + *

+ * This file has a line by line syntax, specifying predicate -> handler. If no predicate is specified then + * the line is assumed to just contain a handler. + * + * @author Stuart Douglas + */ +public class PredicatedHandlersParser { + + + public static List parse(final File file, final ClassLoader classLoader) { + return parse(FileUtils.readFile(file), classLoader); + } + + public static List parse(final InputStream inputStream, final ClassLoader classLoader) { + return parse(FileUtils.readFile(inputStream), classLoader); + } + + public static List parse(final String contents, final ClassLoader classLoader) { + String[] lines = contents.split("\\n"); + final List wrappers = new ArrayList<>(); + + for (String line : lines) { + if (line.trim().length() > 0) { + Predicate predicate; + HandlerWrapper handler; + String[] parts = line.split("->"); + if (parts.length == 2) { + predicate = PredicateParser.parse(parts[0], classLoader); + handler = HandlerParser.parse(parts[1], classLoader); + } else if (parts.length == 1) { + predicate = Predicates.truePredicate(); + handler = HandlerParser.parse(parts[0], classLoader); + } else { + predicate = PredicateParser.parse(parts[0], classLoader); + HandlerWrapper[] handlers = new HandlerWrapper[parts.length -1]; + for(int i = 0; i < handlers.length; ++i) { + handlers[i] = HandlerParser.parse(parts[i + 1], classLoader); + } + handler = new ChaninedHandlerWrapper(Arrays.asList(handlers)); + } + wrappers.add(new PredicatedHandler(predicate, handler)); + } + } + return wrappers; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/ResponseCodeHandlerBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.ResponseCodeHandler; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class ResponseCodeHandlerBuilder implements HandlerBuilder { + @Override + public String name() { + return "response-code"; + } + + @Override + public Map> parameters() { + Map> parameters = new HashMap<>(); + parameters.put("value", Integer.class); + return parameters; + } + + @Override + public Set requiredParameters() { + final Set req = new HashSet<>(); + req.add("value"); + return req; + } + + @Override + public String defaultParameter() { + return "200"; + } + + @Override + public HandlerWrapper build(final Map config) { + final Integer value = (Integer) config.get("value"); + return new HandlerWrapper() { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new ResponseCodeHandler(value); + } + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/RewriteHandlerBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/RewriteHandlerBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/RewriteHandlerBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.SetAttributeHandler; + +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class RewriteHandlerBuilder implements HandlerBuilder { + @Override + public String name() { + return "rewrite"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("value", ExchangeAttribute.class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("value"); + } + + @Override + public String defaultParameter() { + return "value"; + } + + @Override + public HandlerWrapper build(final Map config) { + final ExchangeAttribute value = (ExchangeAttribute) config.get("value"); + + return new HandlerWrapper() { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new SetAttributeHandler(handler, ExchangeAttributes.relativePath(), value); + } + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/builder/SetHandlerBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/builder/SetHandlerBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/builder/SetHandlerBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,74 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.builder; + +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.SetAttributeHandler; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class SetHandlerBuilder implements HandlerBuilder { + @Override + public String name() { + return "set"; + } + + @Override + public Map> parameters() { + Map> parameters = new HashMap<>(); + parameters.put("value", ExchangeAttribute.class); + parameters.put("attribute", ExchangeAttribute.class); + + return parameters; + } + + @Override + public Set requiredParameters() { + final Set req = new HashSet<>(); + req.add("value"); + req.add("attribute"); + return req; + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(final Map config) { + final ExchangeAttribute value = (ExchangeAttribute) config.get("value"); + final ExchangeAttribute attribute = (ExchangeAttribute) config.get("attribute"); + + return new HandlerWrapper() { + @Override + public HttpHandler wrap(HttpHandler handler) { + return new SetAttributeHandler(handler, attribute, value); + } + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/CacheHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/CacheHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/CacheHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,104 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import io.undertow.Handlers; +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.server.handlers.encoding.AllowedContentEncodings; +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.StreamSinkConduit; + +import static io.undertow.util.Headers.CONTENT_LENGTH; + +/** + * + * Handler that attaches a cache to the exchange, a handler can query this cache to see if the + * cache has a cached copy of the content, and if so have the cache serve this content automatically. + * + * + * @author Stuart Douglas + */ +public class CacheHandler implements HttpHandler { + + private final DirectBufferCache cache; + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + + public CacheHandler(final DirectBufferCache cache, final HttpHandler next) { + this.cache = cache; + this.next = next; + } + + public CacheHandler(final DirectBufferCache cache) { + this.cache = cache; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final ResponseCache responseCache = new ResponseCache(cache, exchange); + exchange.putAttachment(ResponseCache.ATTACHMENT_KEY, responseCache); + exchange.addResponseWrapper(new ConduitWrapper() { + @Override + public StreamSinkConduit wrap(final ConduitFactory factory, final HttpServerExchange exchange) { + if(!responseCache.isResponseCachable()) { + return factory.create(); + } + final AllowedContentEncodings contentEncodings = exchange.getAttachment(AllowedContentEncodings.ATTACHMENT_KEY); + if(contentEncodings != null) { + if(!contentEncodings.isIdentity()) { + //we can't cache content encoded responses, as we have no idea how big they will end up being + return factory.create(); + } + } + String lengthString = exchange.getResponseHeaders().getFirst(CONTENT_LENGTH); + if(lengthString == null) { + //we don't cache chunked requests + return factory.create(); + } + int length = Integer.parseInt(lengthString); + final CachedHttpRequest key = new CachedHttpRequest(exchange); + final DirectBufferCache.CacheEntry entry = cache.add(key, length); + + if (entry == null || entry.buffers().length == 0 || !entry.claimEnable()) { + return factory.create(); + } + + if (!entry.reference()) { + entry.disable(); + return factory.create(); + } + + return new ResponseCachingStreamSinkConduit(factory.create(), entry, length); + } + }); + next.handleRequest(exchange); + } + + public HttpHandler getNext() { + return next; + } + + public CacheHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/CachedHttpRequest.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/CachedHttpRequest.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/CachedHttpRequest.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,132 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import java.util.Date; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.encoding.AllowedContentEncodings; +import io.undertow.util.DateUtils; +import io.undertow.util.ETag; +import io.undertow.util.ETagUtils; +import io.undertow.util.Headers; + +/** + * @author Stuart Douglas + */ +public class CachedHttpRequest { + private final String path; + private final ETag etag; + private final String contentEncoding; + private final String contentLocation; + private final String language; + private final String contentType; + private final Date lastModified; + private final int responseCode; + + + public CachedHttpRequest(final HttpServerExchange exchange) { + this.path = exchange.getRequestPath(); + this.etag = ETagUtils.getETag(exchange); + this.contentLocation = exchange.getResponseHeaders().getFirst(Headers.CONTENT_LOCATION); + this.language = exchange.getResponseHeaders().getFirst(Headers.CONTENT_LANGUAGE); + this.contentType = exchange.getResponseHeaders().getFirst(Headers.CONTENT_TYPE); + String lmString = exchange.getResponseHeaders().getFirst(Headers.LAST_MODIFIED); + if (lmString == null) { + this.lastModified = null; + } else { + this.lastModified = DateUtils.parseDate(lmString); + } + //the content encoding can be decided dynamically, based on the current state of the request + //as the decision to compress generally depends on size and mime type + final AllowedContentEncodings encoding = exchange.getAttachment(AllowedContentEncodings.ATTACHMENT_KEY); + if(encoding != null) { + this.contentEncoding = encoding.getCurrentContentEncoding(); + } else { + this.contentEncoding = exchange.getResponseHeaders().getFirst(Headers.CONTENT_ENCODING); + } + this.responseCode = exchange.getResponseCode(); + } + + public String getPath() { + return path; + } + + public ETag getEtag() { + return etag; + } + + public String getContentEncoding() { + return contentEncoding; + } + + public String getLanguage() { + return language; + } + + public String getContentType() { + return contentType; + } + + public Date getLastModified() { + return lastModified; + } + + public String getContentLocation() { + return contentLocation; + } + + public int getResponseCode() { + return responseCode; + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final CachedHttpRequest that = (CachedHttpRequest) o; + + if (responseCode != that.responseCode) return false; + if (contentEncoding != null ? !contentEncoding.equals(that.contentEncoding) : that.contentEncoding != null) + return false; + if (contentLocation != null ? !contentLocation.equals(that.contentLocation) : that.contentLocation != null) + return false; + if (contentType != null ? !contentType.equals(that.contentType) : that.contentType != null) return false; + if (etag != null ? !etag.equals(that.etag) : that.etag != null) return false; + if (language != null ? !language.equals(that.language) : that.language != null) return false; + if (lastModified != null ? !lastModified.equals(that.lastModified) : that.lastModified != null) return false; + if (path != null ? !path.equals(that.path) : that.path != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = path != null ? path.hashCode() : 0; + result = 31 * result + (etag != null ? etag.hashCode() : 0); + result = 31 * result + (contentEncoding != null ? contentEncoding.hashCode() : 0); + result = 31 * result + (contentLocation != null ? contentLocation.hashCode() : 0); + result = 31 * result + (language != null ? language.hashCode() : 0); + result = 31 * result + (contentType != null ? contentType.hashCode() : 0); + result = 31 * result + (lastModified != null ? lastModified.hashCode() : 0); + result = 31 * result + responseCode; + return result; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/DirectBufferCache.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/DirectBufferCache.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/DirectBufferCache.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,354 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import static io.undertow.server.handlers.cache.LimitedBufferSlicePool.PooledByteBuffer; + +import java.nio.ByteBuffer; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; +import java.util.concurrent.ConcurrentHashMap; + +import io.undertow.util.ConcurrentDirectDeque; +import org.xnio.BufferAllocator; + +/** + * A non-blocking buffer cache where entries are indexed by a path and are made up of a + * subsequence of blocks in a fixed large direct buffer. An ideal application is + * a file system cache, where the path corresponds to a file location. + * + *

To reduce contention, entry allocation and eviction execute in a sampling + * fashion (entry hits modulo N). Eviction follows an LRU approach (oldest sampled + * entries are removed first) when the cache is out of capacity

+ * + *

In order to expedite reclamation, cache entries are reference counted as + * opposed to garbage collected.

+ * + * @author Jason T. Greene + */ +public class DirectBufferCache { + private static final int SAMPLE_INTERVAL = 5; + + private final LimitedBufferSlicePool pool; + private final ConcurrentHashMap cache; + private final ConcurrentDirectDeque accessQueue; + private final int sliceSize; + private final int maxAge; + + public DirectBufferCache(int sliceSize, int slicesPerPage, int maxMemory) { + this(sliceSize, slicesPerPage, maxMemory, BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR); + } + + public DirectBufferCache(int sliceSize, int slicesPerPage, int maxMemory, final BufferAllocator bufferAllocator) { + this(sliceSize, slicesPerPage, maxMemory, bufferAllocator, -1); + } + + public DirectBufferCache(int sliceSize, int slicesPerPage, int maxMemory, final BufferAllocator bufferAllocator, int maxAge) { + this.sliceSize = sliceSize; + this.pool = new LimitedBufferSlicePool(bufferAllocator, sliceSize, sliceSize * slicesPerPage, maxMemory / (sliceSize * slicesPerPage)); + this.cache = new ConcurrentHashMap<>(16); + this.accessQueue = ConcurrentDirectDeque.newInstance(); + this.maxAge = maxAge; + } + + public CacheEntry add(Object key, int size) { + return add(key, size, maxAge); + } + + public CacheEntry add(Object key, int size, int maxAge) { + CacheEntry value = cache.get(key); + if (value == null) { + value = new CacheEntry(key, size, this, maxAge); + CacheEntry result = cache.putIfAbsent(key, value); + if (result != null) { + value = result; + } else { + bumpAccess(value); + } + } + + return value; + } + + public CacheEntry get(Object key) { + CacheEntry cacheEntry = cache.get(key); + if (cacheEntry == null) { + return null; + } + + long expires = cacheEntry.getExpires(); + if(expires != -1) { + if(System.currentTimeMillis() > expires) { + remove(key); + return null; + } + } + + if (cacheEntry.hit() % SAMPLE_INTERVAL == 0) { + + bumpAccess(cacheEntry); + + if (! cacheEntry.allocate()) { + // Try and make room + int reclaimSize = cacheEntry.size(); + for (CacheEntry oldest : accessQueue) { + if (oldest == cacheEntry) { + continue; + } + + if (oldest.buffers().length > 0) { + reclaimSize -= oldest.size(); + } + + this.remove(oldest.key()); + + if (reclaimSize <= 0) { + break; + } + } + + // Maybe lucky? + cacheEntry.allocate(); + } + } + + return cacheEntry; + } + + /** + * Returns a set of all the keys in the cache. This is a copy of the + * key set at the time of method invocation. + * + * @return all the keys in this cache + */ + public Set getAllKeys() { + return new HashSet<>(cache.keySet()); + } + + private void bumpAccess(CacheEntry cacheEntry) { + Object prevToken = cacheEntry.claimToken(); + if (prevToken != Boolean.FALSE) { + if (prevToken != null) { + accessQueue.removeToken(prevToken); + } + + Object token = null; + try { + token = accessQueue.offerLastAndReturnToken(cacheEntry); + } catch (Throwable t) { + // In case of disaster (OOME), we need to release the claim, so leave it aas null + } + + if (! cacheEntry.setToken(token) && token != null) { // Always set if null + accessQueue.removeToken(token); + } + } + } + + + public void remove(Object key) { + CacheEntry remove = cache.remove(key); + if (remove != null) { + Object old = remove.clearToken(); + if (old != null) { + accessQueue.removeToken(old); + } + remove.dereference(); + } + } + + public static final class CacheEntry { + private static final PooledByteBuffer[] EMPTY_BUFFERS = new PooledByteBuffer[0]; + private static final PooledByteBuffer[] INIT_BUFFERS = new PooledByteBuffer[0]; + private static final Object CLAIM_TOKEN = new Object(); + + private static final AtomicIntegerFieldUpdater hitsUpdater = AtomicIntegerFieldUpdater.newUpdater(CacheEntry.class, "hits"); + private static final AtomicIntegerFieldUpdater refsUpdater = AtomicIntegerFieldUpdater.newUpdater(CacheEntry.class, "refs"); + private static final AtomicIntegerFieldUpdater enabledUpdator = AtomicIntegerFieldUpdater.newUpdater(CacheEntry.class, "enabled"); + + private static final AtomicReferenceFieldUpdater bufsUpdater = AtomicReferenceFieldUpdater.newUpdater(CacheEntry.class, PooledByteBuffer[].class, "buffers"); + private static final AtomicReferenceFieldUpdater tokenUpdator = AtomicReferenceFieldUpdater.newUpdater(CacheEntry.class, Object.class, "accessToken"); + + private final Object key; + private final int size; + private final DirectBufferCache cache; + private final int maxAge; + private volatile PooledByteBuffer[] buffers = INIT_BUFFERS; + private volatile int refs = 1; + private volatile int hits = 1; + private volatile Object accessToken; + private volatile int enabled; + private volatile long expires = -1; + + private CacheEntry(Object key, int size, DirectBufferCache cache, final int maxAge) { + this.key = key; + this.size = size; + this.cache = cache; + this.maxAge = maxAge; + } + + public int size() { + return size; + } + + public PooledByteBuffer[] buffers() { + return buffers; + } + + public int hit() { + for (;;) { + int i = hits; + + if (hitsUpdater.weakCompareAndSet(this, i, ++i)) { + return i; + } + + } + } + + public Object key() { + return key; + } + + public boolean enabled() { + return enabled == 2; + } + + public void enable() { + if(maxAge == -1) { + this.expires = -1; + } else { + this.expires = System.currentTimeMillis() + maxAge; + } + this.enabled = 2; + } + + public void disable() { + this.enabled = 0; + } + + public boolean claimEnable() { + return enabledUpdator.compareAndSet(this, 0, 1); + } + + public boolean reference() { + for(;;) { + int refs = this.refs; + if (refs < 1) { + return false; // destroying + } + + if (refsUpdater.compareAndSet(this, refs++, refs)) { + return true; + } + } + } + + public boolean dereference() { + for(;;) { + int refs = this.refs; + if (refs < 1) { + return false; // destroying + } + + if (refsUpdater.compareAndSet(this, refs--, refs)) { + if (refs == 0) { + destroy(); + } + return true; + } + } + } + + public boolean allocate() { + if (buffers.length > 0) + return true; + + if (! bufsUpdater.compareAndSet(this, INIT_BUFFERS, EMPTY_BUFFERS)) { + return true; + } + + int reserveSize = size; + int n = 1; + DirectBufferCache bufferCache = cache; + while ((reserveSize -= bufferCache.sliceSize) > 0) { + n++; + } + + // Try to avoid mutations + LimitedBufferSlicePool slicePool = bufferCache.pool; + if (! slicePool.canAllocate(n)) { + this.buffers = INIT_BUFFERS; + return false; + } + + PooledByteBuffer[] buffers = new PooledByteBuffer[n]; + for (int i = 0; i < n; i++) { + PooledByteBuffer allocate = slicePool.allocate(); + if (allocate == null) { + while (--i >= 0) { + buffers[i].free(); + } + + this.buffers = INIT_BUFFERS; + return false; + } + buffers[i] = allocate; + } + + this.buffers = buffers; + return true; + } + + private void destroy() { + this.buffers = EMPTY_BUFFERS; + for (PooledByteBuffer buffer : buffers) { + buffer.free(); + } + } + + Object claimToken() { + for (;;) { + Object current = this.accessToken; + if (current == CLAIM_TOKEN) { + return Boolean.FALSE; + } + + if (tokenUpdator.compareAndSet(this, current, CLAIM_TOKEN)) { + return current; + } + } + } + + boolean setToken(Object token) { + return tokenUpdator.compareAndSet(this, CLAIM_TOKEN, token); + } + + Object clearToken() { + Object old = tokenUpdator.getAndSet(this, null); + return old == CLAIM_TOKEN ? null : old; + } + + long getExpires() { + return expires; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/LRUCache.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/LRUCache.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/LRUCache.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,209 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import io.undertow.util.ConcurrentDirectDeque; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +/** + * A non-blocking cache where entries are indexed by a key. + *

+ *

To reduce contention, entry allocation and eviction execute in a sampling + * fashion (entry hits modulo N). Eviction follows an LRU approach (oldest sampled + * entries are removed first) when the cache is out of capacity.

+ *

+ * + * @author Jason T. Greene + * @author Stuart Douglas + */ +public class LRUCache { + private static final int SAMPLE_INTERVAL = 5; + + /** + * Max active entries that are present in the cache. + */ + private final int maxEntries; + + private final ConcurrentMap> cache; + private final ConcurrentDirectDeque> accessQueue; + /** + * How long an item can stay in the cache in milliseconds + */ + private final int maxAge; + + public LRUCache(int maxEntries, final int maxAge) { + this.maxAge = maxAge; + this.cache = new ConcurrentHashMap<>(16); + this.accessQueue = ConcurrentDirectDeque.newInstance(); + this.maxEntries = maxEntries; + } + + public void add(K key, V newValue) { + CacheEntry value = cache.get(key); + if (value == null) { + long expires; + if(maxAge == -1) { + expires = -1; + } else { + expires = System.currentTimeMillis() + maxAge; + } + value = new CacheEntry<>(key, newValue, expires); + CacheEntry result = cache.putIfAbsent(key, value); + if (result != null) { + value = result; + value.setValue(newValue); + } + bumpAccess(value); + if (cache.size() > maxEntries) { + //remove the oldest + CacheEntry oldest = accessQueue.poll(); + if (oldest != value) { + this.remove(oldest.key()); + } + } + } + } + + public V get(K key) { + CacheEntry cacheEntry = cache.get(key); + if (cacheEntry == null) { + return null; + } + long expires = cacheEntry.getExpires(); + if(expires != -1) { + if(System.currentTimeMillis() > expires) { + remove(key); + return null; + } + } + + if (cacheEntry.hit() % SAMPLE_INTERVAL == 0) { + bumpAccess(cacheEntry); + } + + return cacheEntry.getValue(); + } + + private void bumpAccess(CacheEntry cacheEntry) { + Object prevToken = cacheEntry.claimToken(); + if (prevToken != Boolean.FALSE) { + if (prevToken != null) { + accessQueue.removeToken(prevToken); + } + + Object token = null; + try { + token = accessQueue.offerLastAndReturnToken(cacheEntry); + } catch (Throwable t) { + // In case of disaster (OOME), we need to release the claim, so leave it aas null + } + + if (!cacheEntry.setToken(token) && token != null) { // Always set if null + accessQueue.removeToken(token); + } + } + } + + public V remove(K key) { + CacheEntry remove = cache.remove(key); + if (remove != null) { + Object old = remove.clearToken(); + if (old != null) { + accessQueue.removeToken(old); + } + return remove.getValue(); + } else { + return null; + } + } + + public static final class CacheEntry { + + private static final Object CLAIM_TOKEN = new Object(); + + private static final AtomicIntegerFieldUpdater hitsUpdater = AtomicIntegerFieldUpdater.newUpdater(CacheEntry.class, "hits"); + + private static final AtomicReferenceFieldUpdater tokenUpdator = AtomicReferenceFieldUpdater.newUpdater(CacheEntry.class, Object.class, "accessToken"); + + private final K key; + private volatile V value; + private final long expires; + private volatile int hits = 1; + private volatile Object accessToken; + + private CacheEntry(K key, V value, final long expires) { + this.key = key; + this.value = value; + this.expires = expires; + } + + public void setValue(final V value) { + this.value = value; + } + + public V getValue() { + return value; + } + + public int hit() { + for (; ; ) { + int i = hits; + + if (hitsUpdater.weakCompareAndSet(this, i, ++i)) { + return i; + } + + } + } + + public K key() { + return key; + } + + Object claimToken() { + for (; ; ) { + Object current = this.accessToken; + if (current == CLAIM_TOKEN) { + return Boolean.FALSE; + } + + if (tokenUpdator.compareAndSet(this, current, CLAIM_TOKEN)) { + return current; + } + } + } + + boolean setToken(Object token) { + return tokenUpdator.compareAndSet(this, CLAIM_TOKEN, token); + } + + Object clearToken() { + Object old = tokenUpdator.getAndSet(this, null); + return old == CLAIM_TOKEN ? null : old; + } + + public long getExpires() { + return expires; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/LimitedBufferSlicePool.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/LimitedBufferSlicePool.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/LimitedBufferSlicePool.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,189 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import java.nio.ByteBuffer; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import org.xnio.BufferAllocator; + +/** + * A limited buffer pooled allocator. This pool uses a series of buffer regions to back the + * returned pooled buffers. When the buffer is no longer needed, it should be freed back into the pool; failure + * to do so will cause the corresponding buffer area to be unavailable until the buffer is garbage-collected. + * + * @author David M. Lloyd + * @author Jason T. Greene + */ +public final class LimitedBufferSlicePool { + + private static final AtomicIntegerFieldUpdater regionUpdater = AtomicIntegerFieldUpdater.newUpdater(LimitedBufferSlicePool.class, "regionsUsed"); + private final Queue sliceQueue = new ConcurrentLinkedQueue<>(); + private final BufferAllocator allocator; + private final int bufferSize; + private final int buffersPerRegion; + private final int maxRegions; + private volatile int regionsUsed; + + + /** + * Construct a new instance. + * + * @param allocator the buffer allocator to use + * @param bufferSize the size of each buffer + * @param maxRegionSize the maximum region size for each backing buffer + * @param maxRegions the maximum regions to create, zero for unlimited + */ + public LimitedBufferSlicePool(final BufferAllocator allocator, final int bufferSize, final int maxRegionSize, final int maxRegions) { + if (bufferSize <= 0) { + throw new IllegalArgumentException("Buffer size must be greater than zero"); + } + if (maxRegionSize < bufferSize) { + throw new IllegalArgumentException("Maximum region size must be greater than or equal to the buffer size"); + } + buffersPerRegion = maxRegionSize / bufferSize; + this.bufferSize = bufferSize; + this.allocator = allocator; + this.maxRegions = maxRegions; + } + + /** + * Construct a new instance. + * + * @param allocator the buffer allocator to use + * @param bufferSize the size of each buffer + * @param maxRegionSize the maximum region size for each backing buffer + */ + public LimitedBufferSlicePool(BufferAllocator allocator, int bufferSize, int maxRegionSize) { + this(allocator, bufferSize, maxRegionSize, 0); + } + + + /** + * Construct a new instance, using a direct buffer allocator. + * + * @param bufferSize the size of each buffer + * @param maxRegionSize the maximum region size for each backing buffer + */ + public LimitedBufferSlicePool(final int bufferSize, final int maxRegionSize) { + this(BufferAllocator.DIRECT_BYTE_BUFFER_ALLOCATOR, bufferSize, maxRegionSize); + } + + /** + * Allocates a new byte buffer if possible + * + * @return new buffer or null if none available + **/ + public PooledByteBuffer allocate() { + final Queue sliceQueue = this.sliceQueue; + final Slice slice = sliceQueue.poll(); + if (slice == null && (maxRegions <= 0 || regionUpdater.getAndIncrement(this) < maxRegions)) { + final int bufferSize = this.bufferSize; + final int buffersPerRegion = this.buffersPerRegion; + final ByteBuffer region = allocator.allocate(buffersPerRegion * bufferSize); + int idx = bufferSize; + for (int i = 1; i < buffersPerRegion; i ++) { + sliceQueue.add(new Slice(region, idx, bufferSize)); + idx += bufferSize; + } + final Slice newSlice = new Slice(region, 0, bufferSize); + return new PooledByteBuffer(newSlice, newSlice.slice(), sliceQueue); + } + if (slice == null) { + return null; + } + return new PooledByteBuffer(slice, slice.slice(), sliceQueue); + } + + public boolean canAllocate(int slices) { + if (regionsUsed < maxRegions) + return true; + + if (sliceQueue.isEmpty()) + return false; + + Iterator iterator = sliceQueue.iterator(); + for (int i = 0; i < slices; i++) { + if (! iterator.hasNext()) { + return false; + } + try { + iterator.next(); + } catch (NoSuchElementException e) { + return false; + } + } + + return true; + } + + public static final class PooledByteBuffer { + private final Slice region; + private final Queue slices; + volatile ByteBuffer buffer; + + private static final AtomicReferenceFieldUpdater bufferUpdater = AtomicReferenceFieldUpdater.newUpdater(PooledByteBuffer.class, ByteBuffer.class, "buffer"); + + private PooledByteBuffer(final Slice region, final ByteBuffer buffer, final Queue slices) { + this.region = region; + this.buffer = buffer; + this.slices = slices; + } + + public void free() { + if (bufferUpdater.getAndSet(this, null) != null) { + // trust the user, repool the buffer + slices.add(region); + } + } + + public ByteBuffer getResource() { + final ByteBuffer buffer = this.buffer; + if (buffer == null) { + throw new IllegalStateException(); + } + return buffer; + } + + public String toString() { + return "Pooled buffer " + buffer; + } + } + + private final class Slice { + private final ByteBuffer parent; + private final int start; + private final int size; + + private Slice(final ByteBuffer parent, final int start, final int size) { + this.parent = parent; + this.start = start; + this.size = size; + } + + ByteBuffer slice() { + return ((ByteBuffer)parent.duplicate().position(start).limit(start+size)).slice(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCache.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCache.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCache.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,221 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; +import io.undertow.util.DateUtils; +import io.undertow.util.ETag; +import io.undertow.util.ETagUtils; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; + +import static io.undertow.util.Methods.GET; +import static io.undertow.util.Methods.HEAD; + +/** + * Facade for an underlying buffer cache that contains response information. + *

+ * This facade is attached to the exchange and provides a mechanism for handlers to + * serve cached content. By default a request to serve cached content is interpreted + * to mean that the resulting response is cacheable, and so by default this will result + * in the current response being cached (as long as it meets the criteria for caching). + *

+ * Calling tryServeResponse can also result in the exchange being ended with a not modified + * response code, if the response headers indicate that this is justified (e.g. if the + * If-Modified-Since or If-None-Match headers indicate that the client has a cached copy + * of the response) + *

+ * This should be installed early in the handler chain, before any content encoding handlers. + * This allows it to cache compressed copies of the response, which can significantly reduce + * CPU load. + *

+ * NOTE: This cache has no concept of authentication, it assumes that if the underlying handler + * indicates that a response is cachable, then the current user has been properly authenticated + * to access that resource, and that the resource will not change per user. + * + * @author Stuart Douglas + */ +public class ResponseCache { + + public static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(ResponseCache.class); + + private final DirectBufferCache cache; + private final HttpServerExchange exchange; + private boolean responseCachable; + + public ResponseCache(final DirectBufferCache cache, final HttpServerExchange exchange) { + this.cache = cache; + this.exchange = exchange; + } + + /** + * Attempts to serve the response from a cache. + *

+ * If this fails, then the response will be considered cachable, and may be cached + * to be served by future handlers. + *

+ * If this returns true then the caller should not modify the exchange any more, as this + * can result in a handoff to an IO thread + * + * @return true if serving succeeded, + */ + public boolean tryServeResponse() { + return tryServeResponse(true); + } + + /** + * Attempts to serve the response from a cache. + *

+ * If this fails, and the markCachable parameter is true then the response will be considered cachable, + * and may be cached to be served by future handlers. + *

+ * If this returns true then the caller should not modify the exchange any more, as this + * can result in a handoff to an IO thread + * + * @param markCacheable If this is true then the resulting response will be considered cachable + * @return true if serving succeeded, + */ + public boolean tryServeResponse(boolean markCacheable) { + final CachedHttpRequest key = new CachedHttpRequest(exchange); + DirectBufferCache.CacheEntry entry = cache.get(key); + + //we only cache get and head requests + if (!exchange.getRequestMethod().equals(GET) && + !exchange.getRequestMethod().equals(HEAD)) { + return false; + } + + if (entry == null) { + this.responseCachable = markCacheable; + return false; + } + + // It's loading retry later + if (!entry.enabled() || !entry.reference()) { + this.responseCachable = markCacheable; + return false; + } + + CachedHttpRequest existingKey = (CachedHttpRequest) entry.key(); + //if any of the header matches fail we just return + //we don't can the request, as it is possible the underlying handler + //may have additional etags + final ETag etag = existingKey.getEtag(); + if (!ETagUtils.handleIfMatch(exchange, etag, false)) { + return false; + } + //we do send a 304 if the if-none-match header matches + if (!ETagUtils.handleIfNoneMatch(exchange, etag, true)) { + exchange.setResponseCode(304); + exchange.endExchange(); + return true; + } + //the server may have a more up to date representation + if (!DateUtils.handleIfUnmodifiedSince(exchange, existingKey.getLastModified())) { + return false; + } + if (!DateUtils.handleIfModifiedSince(exchange, existingKey.getLastModified())) { + exchange.setResponseCode(304); + exchange.endExchange(); + return true; + } + + //we are going to proceed. Set the appropriate headers + if(existingKey.getContentType() != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, existingKey.getContentType()); + } + if(existingKey.getContentEncoding() != null && !Headers.IDENTITY.equals(HttpString.tryFromString(existingKey.getContentEncoding()))) { + exchange.getResponseHeaders().put(Headers.CONTENT_ENCODING, existingKey.getContentEncoding()); + } + if(existingKey.getLastModified() != null) { + exchange.getResponseHeaders().put(Headers.LAST_MODIFIED, DateUtils.toDateString(existingKey.getLastModified())); + } + if(existingKey.getContentLocation() != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_LOCATION, existingKey.getContentLocation()); + } + if(existingKey.getLanguage() != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_LANGUAGE, existingKey.getLanguage()); + } + if(etag != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_LANGUAGE, etag.toString()); + } + + //TODO: support if-range + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, Long.toString(entry.size())); + if (exchange.getRequestMethod().equals(HEAD)) { + exchange.endExchange(); + return true; + } + + final ByteBuffer[] buffers; + + + boolean ok = false; + try { + LimitedBufferSlicePool.PooledByteBuffer[] pooled = entry.buffers(); + buffers = new ByteBuffer[pooled.length]; + for (int i = 0; i < buffers.length; i++) { + // Keep position from mutating + buffers[i] = pooled[i].getResource().duplicate(); + } + ok = true; + } finally { + if (!ok) { + entry.dereference(); + } + } + + // Transfer Inline, or register and continue transfer + // Pass off the entry dereference call to the listener + exchange.getResponseSender().send(buffers, new DereferenceCallback(entry)); + return true; + } + + boolean isResponseCachable() { + return responseCachable; + } + + private static class DereferenceCallback implements IoCallback { + private final DirectBufferCache.CacheEntry cache; + + public DereferenceCallback(DirectBufferCache.CacheEntry cache) { + this.cache = cache; + } + + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + cache.dereference(); + exchange.endExchange(); + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + cache.dereference(); + exchange.endExchange(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCachingSender.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCachingSender.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCachingSender.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,184 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.charset.Charset; + +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import org.xnio.Buffers; + +/** + * @author Stuart Douglas + */ +public class ResponseCachingSender implements Sender { + + private final Sender delegate; + private final DirectBufferCache.CacheEntry cacheEntry; + private final long length; + private long written; + + public ResponseCachingSender(final Sender delegate, final DirectBufferCache.CacheEntry cacheEntry, final long length) { + this.delegate = delegate; + this.cacheEntry = cacheEntry; + this.length = length; + } + + @Override + public void send(final ByteBuffer src, final IoCallback callback) { + ByteBuffer origSrc = src.duplicate(); + handleUpdate(origSrc); + delegate.send(src, callback); + } + + + @Override + public void send(final ByteBuffer[] srcs, final IoCallback callback) { + ByteBuffer[] origSrc = new ByteBuffer[srcs.length]; + long total = 0; + for (int i = 0; i < srcs.length; i++) { + origSrc[i] = srcs[i].duplicate(); + total += origSrc[i].remaining(); + } + handleUpdate(origSrc, total); + delegate.send(srcs, callback); + } + + @Override + public void send(final ByteBuffer src) { + ByteBuffer origSrc = src.duplicate(); + handleUpdate(origSrc); + delegate.send(src); + } + + @Override + public void send(final ByteBuffer[] srcs) { + ByteBuffer[] origSrc = new ByteBuffer[srcs.length]; + long total = 0; + for (int i = 0; i < srcs.length; i++) { + origSrc[i] = srcs[i].duplicate(); + total += origSrc[i].remaining(); + } + handleUpdate(origSrc, total); + delegate.send(srcs); + } + + @Override + public void send(final String data, final IoCallback callback) { + try { + handleUpdate(ByteBuffer.wrap(data.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + delegate.send(data, callback); + } + + @Override + public void send(final String data, final Charset charset, final IoCallback callback) { + handleUpdate(ByteBuffer.wrap(data.getBytes(charset))); + delegate.send(data, charset, callback); + } + + @Override + public void send(final String data) { + try { + handleUpdate(ByteBuffer.wrap(data.getBytes("UTF-8"))); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + delegate.send(data); + } + + @Override + public void send(final String data, final Charset charset) { + handleUpdate(ByteBuffer.wrap(data.getBytes(charset))); + delegate.send(data, charset); + } + + @Override + public void transferFrom(FileChannel channel, IoCallback callback) { + // Transfer never caches + delegate.transferFrom(channel, callback); + } + + @Override + public void close(final IoCallback callback) { + if (written != length) { + cacheEntry.disable(); + cacheEntry.dereference(); + } + delegate.close(); + } + + @Override + public void close() { + if (written != length) { + cacheEntry.disable(); + cacheEntry.dereference(); + } + delegate.close(); + } + + private void handleUpdate(final ByteBuffer origSrc) { + LimitedBufferSlicePool.PooledByteBuffer[] pooled = cacheEntry.buffers(); + ByteBuffer[] buffers = new ByteBuffer[pooled.length]; + for (int i = 0; i < buffers.length; i++) { + buffers[i] = pooled[i].getResource(); + } + written += Buffers.copy(buffers, 0, buffers.length, origSrc); + if (written == length) { + for (ByteBuffer buffer : buffers) { + //prepare buffers for reading + buffer.flip(); + } + cacheEntry.enable(); + } + } + + private void handleUpdate(final ByteBuffer[] origSrc, long totalWritten) { + LimitedBufferSlicePool.PooledByteBuffer[] pooled = cacheEntry.buffers(); + ByteBuffer[] buffers = new ByteBuffer[pooled.length]; + for (int i = 0; i < buffers.length; i++) { + buffers[i] = pooled[i].getResource(); + } + long leftToCopy = totalWritten; + for (int i = 0; i < origSrc.length; ++i) { + ByteBuffer buf = origSrc[i]; + if (buf.remaining() > leftToCopy) { + buf.limit((int) (buf.position() + leftToCopy)); + } + leftToCopy -= buf.remaining(); + Buffers.copy(buffers, 0, buffers.length, buf); + if (leftToCopy == 0) { + break; + } + } + written += totalWritten; + if (written == length) { + for (ByteBuffer buffer : buffers) { + //prepare buffers for reading + buffer.flip(); + } + cacheEntry.enable(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCachingStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCachingStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/cache/ResponseCachingStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,145 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.cache; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.StreamSinkConduit; + +/** + * @author Stuart Douglas + */ +public class ResponseCachingStreamSinkConduit extends AbstractStreamSinkConduit { + + private final DirectBufferCache.CacheEntry cacheEntry; + private final long length; + private long written; + + /** + * Construct a new instance. + * + * @param next the delegate conduit to set + * @param cacheEntry + * @param length + */ + public ResponseCachingStreamSinkConduit(final StreamSinkConduit next, final DirectBufferCache.CacheEntry cacheEntry, final long length) { + super(next); + this.cacheEntry = cacheEntry; + this.length = length; + for(LimitedBufferSlicePool.PooledByteBuffer buffer: cacheEntry.buffers()) { + buffer.getResource().clear(); + } + } + + @Override + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + @Override + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public int write(final ByteBuffer src) throws IOException { + ByteBuffer origSrc = src.duplicate(); + int totalWritten = super.write(src); + if(totalWritten > 0) { + LimitedBufferSlicePool.PooledByteBuffer[] pooled = cacheEntry.buffers(); + ByteBuffer[] buffers = new ByteBuffer[pooled.length]; + for (int i = 0; i < buffers.length; i++) { + buffers[i] = pooled[i].getResource(); + } + origSrc.limit(origSrc.position() + totalWritten); + written += Buffers.copy(buffers, 0, buffers.length, origSrc); + if (written == length) { + for (ByteBuffer buffer : buffers) { + //prepare buffers for reading + buffer.flip(); + } + cacheEntry.enable(); + } + } + return totalWritten; + } + + @Override + public long write(final ByteBuffer[] srcs, final int offs, final int len) throws IOException { + + ByteBuffer[] origSrc = new ByteBuffer[srcs.length]; + for (int i = 0; i < srcs.length; i++) { + origSrc[i] = srcs[i].duplicate(); + } + long totalWritten = super.write(srcs, offs, len); + if(totalWritten > 0) { + LimitedBufferSlicePool.PooledByteBuffer[] pooled = cacheEntry.buffers(); + ByteBuffer[] buffers = new ByteBuffer[pooled.length]; + for (int i = 0; i < buffers.length; i++) { + buffers[i] = pooled[i].getResource(); + } + long leftToCopy = totalWritten; + for(int i = 0; i < len; ++i) { + ByteBuffer buf = origSrc[offs + i]; + if(buf.remaining() > leftToCopy) { + buf.limit((int) (buf.position() + leftToCopy)); + } + leftToCopy -= buf.remaining(); + Buffers.copy(buffers, 0, buffers.length, buf); + if(leftToCopy == 0) { + break; + } + } + written += totalWritten; + if (written == length) { + for (ByteBuffer buffer : buffers) { + //prepare buffers for reading + buffer.flip(); + } + cacheEntry.enable(); + } + } + return totalWritten; + } + + @Override + public void terminateWrites() throws IOException { + if (written != length) { + cacheEntry.disable(); + cacheEntry.dereference(); + } + super.terminateWrites(); + } + + @Override + public void truncateWrites() throws IOException { + if (written != length) { + cacheEntry.disable(); + cacheEntry.dereference(); + } + super.truncateWrites(); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/AllowedContentEncodings.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/AllowedContentEncodings.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/AllowedContentEncodings.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,105 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import java.util.List; + +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; +import io.undertow.util.ConduitFactory; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import org.xnio.conduits.StreamSinkConduit; + +/** + * An attachment that provides information about the current content encoding that will be chosen for the response + * + * @author Stuart Douglas + */ +public class AllowedContentEncodings implements ConduitWrapper { + + public static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(AllowedContentEncodings.class); + + private final HttpServerExchange exchange; + private final List encodings; + + + public AllowedContentEncodings(final HttpServerExchange exchange, final List encodings) { + this.exchange = exchange; + this.encodings = encodings; + } + + /** + * @return The content encoding that will be set, given the current state of the HttpServerExchange + */ + public String getCurrentContentEncoding() { + for (EncodingMapping encoding : encodings) { + if (encoding.getAllowed() == null || encoding.getAllowed().resolve(exchange)) { + return encoding.getName(); + } + } + return Headers.IDENTITY.toString(); + } + + public EncodingMapping getEncoding() { + for (EncodingMapping encoding : encodings) { + if (encoding.getAllowed() == null || encoding.getAllowed().resolve(exchange)) { + return encoding; + } + } + return null; + } + + public boolean isIdentity() { + return getCurrentContentEncoding().equals(Headers.IDENTITY.toString()); + } + + /** + * If the list of allowed encodings was empty then it means that no encodings were allowed, and + * identity was explicitly prohibited with a q value of 0. + */ + public boolean isNoEncodingsAllowed() { + return encodings.isEmpty(); + } + + @Override + public StreamSinkConduit wrap(final ConduitFactory factory, final HttpServerExchange exchange) { + if (exchange.getResponseHeaders().contains(Headers.CONTENT_ENCODING)) { + //already encoded + return factory.create(); + } + //if this is a zero length response we don't want to encode + if (exchange.getResponseContentLength() != 0 + && exchange.getResponseCode() != 204 + && exchange.getResponseCode() != 304) { + EncodingMapping encoding = getEncoding(); + if (encoding != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_ENCODING, encoding.getName()); + if (exchange.getRequestMethod().equals(Methods.HEAD)) { + //we don't create an actual encoder for HEAD requests, but we set the header + return factory.create(); + } else { + return encoding.getEncoding().getResponseWrapper().wrap(factory, exchange); + } + } + } + return factory.create(); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodedResource.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodedResource.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodedResource.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.server.handlers.resource.Resource; + +/** + * A resource that has been pre-compressed + * + * @author Stuart Douglas + */ +public class ContentEncodedResource { + + private final Resource resource; + private final String contentEncoding; + + public ContentEncodedResource(Resource resource, String contentEncoding) { + this.resource = resource; + this.contentEncoding = contentEncoding; + } + + public Resource getResource() { + return resource; + } + + public String getContentEncoding() { + return contentEncoding; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodedResourceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodedResourceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodedResourceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,293 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.UndertowLogger; +import io.undertow.predicate.Predicate; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.resource.CachingResourceManager; +import io.undertow.server.handlers.resource.Resource; +import io.undertow.util.ImmediateConduitFactory; +import org.xnio.FileAccess; +import org.xnio.IoUtils; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.WriteReadyHandler; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +/** + * Class that provides a way of serving pre-encoded resources. + * + * @author Stuart Douglas + */ +public class ContentEncodedResourceManager { + + + private final File encodedResourcesRoot; + private final CachingResourceManager encoded; + private final ContentEncodingRepository contentEncodingRepository; + private final int minResourceSize; + private final int maxResourceSize; + private final Predicate encodingAllowed; + + private final ConcurrentMap fileLocks = new ConcurrentHashMap<>(); + + public ContentEncodedResourceManager(File encodedResourcesRoot, CachingResourceManager encodedResourceManager, ContentEncodingRepository contentEncodingRepository, int minResourceSize, int maxResourceSize, Predicate encodingAllowed) { + this.encodedResourcesRoot = encodedResourcesRoot; + this.encoded = encodedResourceManager; + this.contentEncodingRepository = contentEncodingRepository; + this.minResourceSize = minResourceSize; + this.maxResourceSize = maxResourceSize; + this.encodingAllowed = encodingAllowed; + } + + /** + * Gets a pre-encoded resource. + *

+ * TODO: blocking / non-blocking semantics + * + * @param resource + * @param exchange + * @return + * @throws IOException + */ + public ContentEncodedResource getResource(final Resource resource, final HttpServerExchange exchange) throws IOException { + final String path = resource.getPath(); + File file = resource.getFile(); + if (file == null) { + return null; + } + if (minResourceSize > 0 && resource.getContentLength() < minResourceSize || + maxResourceSize > 0 && resource.getContentLength() > maxResourceSize || + !(encodingAllowed == null || encodingAllowed.resolve(exchange))) { + return null; + } + AllowedContentEncodings encodings = contentEncodingRepository.getContentEncodings(exchange); + if (encodings == null || encodings.isNoEncodingsAllowed()) { + return null; + } + EncodingMapping encoding = encodings.getEncoding(); + if (encoding == null || encoding.getName().equals(ContentEncodingRepository.IDENTITY)) { + return null; + } + String newPath = path + ".undertow.encoding." + encoding.getName(); + Resource preCompressed = encoded.getResource(newPath); + if (preCompressed != null) { + return new ContentEncodedResource(preCompressed, encoding.getName()); + } + final LockKey key = new LockKey(path, encoding.getName()); + if (fileLocks.putIfAbsent(key, this) != null) { + //another thread is already compressing + //we don't do anything fancy here, just return and serve non-compressed content + return null; + } + FileChannel targetFileChannel = null; + FileChannel sourceFileChannel = null; + try { + //double check, the compressing thread could have finished just before we acquired the lock + preCompressed = encoded.getResource(newPath); + if (preCompressed != null) { + return new ContentEncodedResource(preCompressed, encoding.getName()); + } + + final File finalTarget = new File(encodedResourcesRoot, newPath); + final File tempTarget = new File(encodedResourcesRoot, newPath); + + //horrible hack to work around XNIO issue + FileOutputStream tmp = new FileOutputStream(tempTarget); + try { + tmp.close(); + } finally { + IoUtils.safeClose(tmp); + } + + targetFileChannel = exchange.getConnection().getWorker().getXnio().openFile(tempTarget, FileAccess.READ_WRITE); + sourceFileChannel = exchange.getConnection().getWorker().getXnio().openFile(file, FileAccess.READ_ONLY); + + StreamSinkConduit conduit = encoding.getEncoding().getResponseWrapper().wrap(new ImmediateConduitFactory(new FileConduitTarget(targetFileChannel, exchange)), exchange); + final ConduitStreamSinkChannel targetChannel = new ConduitStreamSinkChannel(null, conduit); + long transferred = sourceFileChannel.transferTo(0, resource.getContentLength(), targetChannel); + targetChannel.shutdownWrites(); + org.xnio.channels.Channels.flushBlocking(targetChannel); + if (transferred != resource.getContentLength()) { + UndertowLogger.REQUEST_LOGGER.error("Failed to write pre-cached file"); + } + tempTarget.renameTo(finalTarget); + encoded.invalidate(newPath); + final Resource encodedResource = encoded.getResource(newPath); + return new ContentEncodedResource(encodedResource, encoding.getName()); + } finally { + IoUtils.safeClose(targetFileChannel); + IoUtils.safeClose(sourceFileChannel); + fileLocks.remove(key); + } + } + + private final class LockKey { + private final String path; + private final String encoding; + + private LockKey(String path, String encoding) { + this.path = path; + this.encoding = encoding; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LockKey lockKey = (LockKey) o; + + if (encoding != null ? !encoding.equals(lockKey.encoding) : lockKey.encoding != null) return false; + if (path != null ? !path.equals(lockKey.path) : lockKey.path != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = path != null ? path.hashCode() : 0; + result = 31 * result + (encoding != null ? encoding.hashCode() : 0); + return result; + } + } + + private static final class FileConduitTarget implements StreamSinkConduit { + private final FileChannel fileChannel; + private final HttpServerExchange exchange; + private WriteReadyHandler writeReadyHandler; + private boolean writesResumed = false; + + private FileConduitTarget(FileChannel fileChannel, HttpServerExchange exchange) { + this.fileChannel = fileChannel; + this.exchange = exchange; + } + + @Override + public long transferFrom(FileChannel fileChannel, long l, long l2) throws IOException { + return this.fileChannel.transferFrom(fileChannel, l, l2); + } + + @Override + public long transferFrom(StreamSourceChannel streamSourceChannel, long l, ByteBuffer byteBuffer) throws IOException { + return IoUtils.transfer(streamSourceChannel, l, byteBuffer, fileChannel); + } + + @Override + public int write(ByteBuffer byteBuffer) throws IOException { + return fileChannel.write(byteBuffer); + } + + @Override + public long write(ByteBuffer[] byteBuffers, int i, int i2) throws IOException { + return fileChannel.write(byteBuffers, i, i2); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + @Override + public void terminateWrites() throws IOException { + fileChannel.close(); + } + + @Override + public boolean isWriteShutdown() { + return !fileChannel.isOpen(); + } + + @Override + public void resumeWrites() { + wakeupWrites(); + } + + @Override + public void suspendWrites() { + writesResumed = false; + } + + @Override + public void wakeupWrites() { + if (writeReadyHandler != null) { + writesResumed = true; + while (writesResumed && writeReadyHandler != null) { + writeReadyHandler.writeReady(); + } + } + } + + @Override + public boolean isWriteResumed() { + return writesResumed; + } + + @Override + public void awaitWritable() throws IOException { + } + + @Override + public void awaitWritable(long l, TimeUnit timeUnit) throws IOException { + } + + @Override + public XnioIoThread getWriteThread() { + return exchange.getIoThread(); + } + + @Override + public void setWriteReadyHandler(WriteReadyHandler writeReadyHandler) { + this.writeReadyHandler = writeReadyHandler; + } + + @Override + public void truncateWrites() throws IOException { + fileChannel.close(); + } + + @Override + public boolean flush() throws IOException { + return true; + } + + @Override + public XnioWorker getWorker() { + return exchange.getConnection().getWorker(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodingProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodingProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodingProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,49 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.StreamSinkConduit; + +/** + * @author Stuart Douglas + */ +public interface ContentEncodingProvider { + + + ContentEncodingProvider IDENTITY = new ContentEncodingProvider() { + + private final ConduitWrapper CONDUIT_WRAPPER = new ConduitWrapper() { + @Override + public StreamSinkConduit wrap(final ConduitFactory factory, final HttpServerExchange exchange) { + return factory.create(); + } + }; + + @Override + public ConduitWrapper getResponseWrapper() { + return CONDUIT_WRAPPER; + } + }; + + ConduitWrapper getResponseWrapper(); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodingRepository.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodingRepository.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/ContentEncodingRepository.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,110 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.predicate.Predicate; +import io.undertow.predicate.Predicates; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.Headers; +import io.undertow.util.QValueParser; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * + * + * @author Stuart Douglas + */ +public class ContentEncodingRepository { + + public static final String IDENTITY = "identity"; + + private final Map encodingMap = new CopyOnWriteMap<>(); + + /** + * Gets all allow + * @param exchange + * @return + */ + public AllowedContentEncodings getContentEncodings(final HttpServerExchange exchange) { + final List res = exchange.getRequestHeaders().get(Headers.ACCEPT_ENCODING); + if (res == null || res.isEmpty()) { + return null; + } + final List resultingMappings = new ArrayList<>(); + final List> found = QValueParser.parse(res); + for (List result : found) { + List available = new ArrayList<>(); + boolean includesIdentity = false; + boolean isQValue0 = false; + + for (final QValueParser.QValueResult value : result) { + EncodingMapping encoding; + if (value.getValue().equals("*")) { + includesIdentity = true; + encoding = new EncodingMapping(IDENTITY, ContentEncodingProvider.IDENTITY, 0, Predicates.truePredicate()); + } else { + encoding = encodingMap.get(value.getValue()); + } + if (value.isQValueZero()) { + isQValue0 = true; + } + if (encoding != null) { + available.add(encoding); + } + } + if (isQValue0) { + if (resultingMappings.isEmpty()) { + if (includesIdentity) { + return new AllowedContentEncodings(exchange, Collections.emptyList()); + } else { + return null; + } + } + } else if (!available.isEmpty()) { + Collections.sort(available, Collections.reverseOrder()); + resultingMappings.addAll(available); + } + } + if (!resultingMappings.isEmpty()) { + return new AllowedContentEncodings(exchange, resultingMappings); + } + return null; + } + + public synchronized ContentEncodingRepository addEncodingHandler(final String encoding, final ContentEncodingProvider encoder, int priority) { + addEncodingHandler(encoding, encoder, priority, Predicates.truePredicate()); + return this; + } + + public synchronized ContentEncodingRepository addEncodingHandler(final String encoding, final ContentEncodingProvider encoder, int priority, final Predicate enabledPredicate) { + this.encodingMap.put(encoding, new EncodingMapping(encoding, encoder, priority, enabledPredicate)); + return this; + } + + public synchronized ContentEncodingRepository removeEncodingHandler(final String encoding) { + encodingMap.remove(encoding); + return this; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/DeflateEncodingProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/DeflateEncodingProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/DeflateEncodingProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.conduits.DeflatingStreamSinkConduit; +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.StreamSinkConduit; + +/** + * Content coding for 'deflate' + * + * @author Stuart Douglas + */ +public class DeflateEncodingProvider implements ContentEncodingProvider { + + @Override + public ConduitWrapper getResponseWrapper() { + return new ConduitWrapper() { + @Override + public StreamSinkConduit wrap(final ConduitFactory factory, final HttpServerExchange exchange) { + return new DeflatingStreamSinkConduit(factory, exchange); + } + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/EncodingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/EncodingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/EncodingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,93 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; + +/** + * Handler that serves as the basis for content encoding implementations. + *

+ * Encoding handlers are added as delegates to this handler, with a specified server side priority. + *

+ * If a request comes in with no q value then then server will pick the handler with the highest priority + * as the encoding to use, otherwise the q value will be used to determine the correct handler. + *

+ * If no handler matches then the identity encoding is assumed. If the identity encoding has been + * specifically disallowed due to a q value of 0 then the handler will set the response code + * 406 (Not Acceptable) and return. + * + * @author Stuart Douglas + */ +public class EncodingHandler implements HttpHandler { + + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + private volatile HttpHandler noEncodingHandler = ResponseCodeHandler.HANDLE_406; + + private final ContentEncodingRepository contentEncodingRepository; + + public EncodingHandler(final HttpHandler next, ContentEncodingRepository contentEncodingRepository) { + this.next = next; + this.contentEncodingRepository = contentEncodingRepository; + } + + public EncodingHandler(ContentEncodingRepository contentEncodingRepository) { + this.contentEncodingRepository = contentEncodingRepository; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + AllowedContentEncodings encodings = contentEncodingRepository.getContentEncodings(exchange); + if (encodings == null) { + next.handleRequest(exchange); + } else if (encodings.isNoEncodingsAllowed()) { + noEncodingHandler.handleRequest(exchange); + } else { + exchange.addResponseWrapper(encodings); + exchange.putAttachment(AllowedContentEncodings.ATTACHMENT_KEY, encodings); + next.handleRequest(exchange); + } + } + + + public HttpHandler getNext() { + return next; + } + + public EncodingHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + + public HttpHandler getNoEncodingHandler() { + return noEncodingHandler; + } + + public EncodingHandler setNoEncodingHandler(HttpHandler noEncodingHandler) { + Handlers.handlerNotNull(noEncodingHandler); + this.noEncodingHandler = noEncodingHandler; + return this; + } + + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/EncodingMapping.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/EncodingMapping.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/EncodingMapping.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,60 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.predicate.Predicate; + +/** +* @author Stuart Douglas +*/ +final class EncodingMapping implements Comparable { + + private final String name; + private final ContentEncodingProvider encoding; + private final int priority; + private final Predicate allowed; + + EncodingMapping(final String name, final ContentEncodingProvider encoding, final int priority, final Predicate allowed) { + this.name = name; + this.encoding = encoding; + this.priority = priority; + this.allowed = allowed; + } + + public String getName() { + return name; + } + + public ContentEncodingProvider getEncoding() { + return encoding; + } + + public int getPriority() { + return priority; + } + + public Predicate getAllowed() { + return allowed; + } + + @Override + public int compareTo(final EncodingMapping o) { + return priority - o.priority; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/GzipEncodingProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/GzipEncodingProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/encoding/GzipEncodingProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.encoding; + +import io.undertow.conduits.GzipStreamSinkConduit; +import io.undertow.server.ConduitWrapper; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConduitFactory; +import org.xnio.conduits.StreamSinkConduit; + +/** + * Content coding for 'deflate' + * + * @author Stuart Douglas + */ +public class GzipEncodingProvider implements ContentEncodingProvider { + + @Override + public ConduitWrapper getResponseWrapper() { + return new ConduitWrapper() { + @Override + public StreamSinkConduit wrap(final ConduitFactory factory, final HttpServerExchange exchange) { + return new GzipStreamSinkConduit(factory, exchange); + } + }; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/error/FileErrorPageHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/error/FileErrorPageHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/error/FileErrorPageHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,223 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.error; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import org.jboss.logging.Logger; +import org.xnio.FileAccess; +import org.xnio.IoUtils; +import org.xnio.channels.Channels; +import org.xnio.channels.StreamSinkChannel; + +import io.undertow.Handlers; +import io.undertow.UndertowLogger; +import io.undertow.server.DefaultResponseListener; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.util.Headers; + +/** + * Handler that serves up a file from disk to serve as an error page. + *

+ * This handler does not server up and response codes by default, you must configure + * the response codes it responds to. + * + * @author Stuart Douglas + */ +public class FileErrorPageHandler implements HttpHandler { + + private static final Logger log = Logger.getLogger("io.undertow.server.error.file"); + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + + /** + * The response codes that this handler will handle. If this is empty then this handler will have no effect. + */ + private volatile Set responseCodes; + + private volatile File file; + + public FileErrorPageHandler(final File file, final Integer... responseCodes) { + this.file = file; + this.responseCodes = new HashSet<>(Arrays.asList(responseCodes)); + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.addDefaultResponseListener(new DefaultResponseListener() { + @Override + public boolean handleDefaultResponse(final HttpServerExchange exchange) { + Set codes = responseCodes; + if (!exchange.isResponseStarted() && codes.contains(exchange.getResponseCode())) { + serveFile(exchange); + return true; + } + return false; + } + }); + + next.handleRequest(exchange); + } + + private void serveFile(final HttpServerExchange exchange) { + exchange.dispatch(new Runnable() { + @Override + public void run() { + final FileChannel fileChannel; + try { + try { + fileChannel = exchange.getConnection().getWorker().getXnio().openFile(file, FileAccess.READ_ONLY); + } catch (FileNotFoundException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + exchange.endExchange(); + return; + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + exchange.endExchange(); + return; + } + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, file.length()); + final StreamSinkChannel response = exchange.getResponseChannel(); + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + IoUtils.safeClose(fileChannel); + nextListener.proceed(); + } + }); + + try { + log.tracef("Serving file %s (blocking)", fileChannel); + Channels.transferBlocking(response, fileChannel, 0, file.length()); + log.tracef("Finished serving %s, shutting down (blocking)", fileChannel); + response.shutdownWrites(); + log.tracef("Finished serving %s, flushing (blocking)", fileChannel); + Channels.flushBlocking(response); + log.tracef("Finished serving %s (complete)", fileChannel); + exchange.endExchange(); + } catch (IOException ignored) { + log.tracef("Failed to serve %s: %s", fileChannel, ignored); + exchange.endExchange(); + IoUtils.safeClose(response); + } finally { + IoUtils.safeClose(fileChannel); + } + } + }); + } + + public HttpHandler getNext() { + return next; + } + + public FileErrorPageHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + public Set getResponseCodes() { + return Collections.unmodifiableSet(responseCodes); + } + + public FileErrorPageHandler setResponseCodes(final Set responseCodes) { + if (responseCodes == null) { + this.responseCodes = Collections.emptySet(); + } else { + this.responseCodes = new HashSet<>(responseCodes); + } + return this; + } + + public FileErrorPageHandler setResponseCodes(final Integer... responseCodes) { + this.responseCodes = new HashSet<>(Arrays.asList(responseCodes)); + return this; + } + + public File getFile() { + return file; + } + + public FileErrorPageHandler setFile(final File file) { + this.file = file; + return this; + } + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "error-file"; + } + + @Override + public Map> parameters() { + Map> params = new HashMap<>(); + params.put("file", String.class); + params.put("response-codes", Integer[].class); + return params; + } + + @Override + public Set requiredParameters() { + return new HashSet<>(Arrays.asList(new String[]{"file", "response-codes"})); + } + + @Override + public String defaultParameter() { + return null; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper((String)config.get("file"), (Integer[]) config.get("response-codes")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final String file; + private final Integer[] responseCodes; + + private Wrapper(String file, Integer[] responseCodes) { + this.file = file; + this.responseCodes = responseCodes; + } + + + @Override + public HttpHandler wrap(HttpHandler handler) { + return new FileErrorPageHandler(new File(file), responseCodes); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/error/SimpleErrorPageHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/error/SimpleErrorPageHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/error/SimpleErrorPageHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,102 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.error; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +import io.undertow.Handlers; +import io.undertow.io.Sender; +import io.undertow.server.DefaultResponseListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.util.Headers; +import io.undertow.util.StatusCodes; + +/** + * Handler that generates an extremely simple no frills error page + * + * @author Stuart Douglas + */ +public class SimpleErrorPageHandler implements HttpHandler { + + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + + /** + * The response codes that this handler will handle. If this is null then it will handle all 4xx and 5xx codes. + */ + private volatile Set responseCodes = null; + + public SimpleErrorPageHandler(final HttpHandler next) { + this.next = next; + } + + public SimpleErrorPageHandler() { + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.addDefaultResponseListener(new DefaultResponseListener() { + @Override + public boolean handleDefaultResponse(final HttpServerExchange exchange) { + if (!exchange.isResponseChannelAvailable()) { + return false; + } + Set codes = responseCodes; + if (codes == null ? exchange.getResponseCode() >= 400 : codes.contains(Integer.valueOf(exchange.getResponseCode()))) { + final String errorPage = "Error" + exchange.getResponseCode() + " - " + StatusCodes.getReason(exchange.getResponseCode()) + ""; + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, "" + errorPage.length()); + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html"); + Sender sender = exchange.getResponseSender(); + sender.send(errorPage); + return true; + } + return false; + } + }); + next.handleRequest(exchange); + } + + public HttpHandler getNext() { + return next; + } + + public SimpleErrorPageHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + public Set getResponseCodes() { + return Collections.unmodifiableSet(responseCodes); + } + + public SimpleErrorPageHandler setResponseCodes(final Set responseCodes) { + this.responseCodes = new HashSet<>(responseCodes); + return this; + } + + public SimpleErrorPageHandler setResponseCodes(final Integer... responseCodes) { + this.responseCodes = new HashSet<>(Arrays.asList(responseCodes)); + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/form/EagerFormParsingHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/form/EagerFormParsingHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/form/EagerFormParsingHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,70 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.form; + +import io.undertow.Handlers; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; + +/** + * Handler that eagerly parses form data. The request chain will pause while the data is being read, + * and then continue when the form data is fully passed. + *

+ *

+ * NOTE: This is not strictly compatible with servlet, as it removes the option for the user to + * parse the request themselves, however in practice this requirement is probably rare, and + * using this handler gives a significant performance advantage in that a thread is not blocked + * for the duration of the upload. + * + * @author Stuart Douglas + */ +public class EagerFormParsingHandler implements HttpHandler { + + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + private final FormParserFactory formParserFactory; + + public EagerFormParsingHandler(final FormParserFactory formParserFactory) { + this.formParserFactory = formParserFactory; + } + + public EagerFormParsingHandler() { + this.formParserFactory = FormParserFactory.builder().build(); + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + FormDataParser parser = formParserFactory.createParser(exchange); + if (parser == null) { + next.handleRequest(exchange); + return; + } + parser.parse(next); + } + + public HttpHandler getNext() { + return next; + } + + public EagerFormParsingHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormData.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormData.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormData.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,233 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.form; + +import java.io.File; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.Map; + +import io.undertow.UndertowMessages; +import io.undertow.util.HeaderMap; + +/** + * Representation of form data. + *

+ * TODO: add representation of multipart data + */ +public final class FormData implements Iterable { + + private final Map> values = new LinkedHashMap<>(); + + private final int maxValues; + private int valueCount = 0; + + public FormData(final int maxValues) { + this.maxValues = maxValues; + } + + + public Iterator iterator() { + return values.keySet().iterator(); + } + + public FormValue getFirst(String name) { + final Deque deque = values.get(name); + return deque == null ? null : deque.peekFirst(); + } + + public FormValue getLast(String name) { + final Deque deque = values.get(name); + return deque == null ? null : deque.peekLast(); + } + + public Deque get(String name) { + return values.get(name); + } + + public void add(String name, String value) { + add(name, value, null); + } + + public void add(String name, String value, final HeaderMap headers) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, values = new ArrayDeque<>(1)); + } + values.add(new FormValueImpl(value, headers)); + if (++valueCount > maxValues) { + throw UndertowMessages.MESSAGES.tooManyParameters(maxValues); + } + } + + public void add(String name, File value, String fileName, final HeaderMap headers) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, values = new ArrayDeque<>(1)); + } + values.add(new FormValueImpl(value, fileName, headers)); + if (values.size() > maxValues) { + throw UndertowMessages.MESSAGES.tooManyParameters(maxValues); + } + if (++valueCount > maxValues) { + throw UndertowMessages.MESSAGES.tooManyParameters(maxValues); + } + } + + public void put(String name, String value, final HeaderMap headers) { + Deque values = new ArrayDeque<>(1); + Deque old = this.values.put(name, values); + if (old != null) { + valueCount -= old.size(); + } + values.add(new FormValueImpl(value, headers)); + + if (++valueCount > maxValues) { + throw UndertowMessages.MESSAGES.tooManyParameters(maxValues); + } + } + + public Deque remove(String name) { + Deque old = values.remove(name); + if (old != null) { + valueCount -= old.size(); + } + return old; + } + + public boolean contains(String name) { + final Deque value = values.get(name); + return value != null && !value.isEmpty(); + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final FormData strings = (FormData) o; + + if (values != null ? !values.equals(strings.values) : strings.values != null) return false; + + return true; + } + + @Override + public int hashCode() { + return values != null ? values.hashCode() : 0; + } + + @Override + public String toString() { + return "FormData{" + + "values=" + values + + '}'; + } + + + public interface FormValue { + + /** + * @return the simple string value. + * @throws IllegalStateException If this is not a simple string value + */ + String getValue(); + + /** + * Returns true if this is a file and not a simple string + * + * @return + */ + boolean isFile(); + + /** + * @return The temp file that the file data was saved to + * @throws IllegalStateException if this is not a file + */ + File getFile(); + + /** + * @return The filename specified in the disposition header. + */ + String getFileName(); + + /** + * @return The headers that were present in the multipart request, or null if this was not a multipart request + */ + HeaderMap getHeaders(); + + + } + + + static class FormValueImpl implements FormValue { + + private final String value; + private final String fileName; + private final File file; + private final HeaderMap headers; + + FormValueImpl(String value, HeaderMap headers) { + this.value = value; + this.headers = headers; + this.file = null; + this.fileName = null; + } + + FormValueImpl(File file, final String fileName, HeaderMap headers) { + this.file = file; + this.headers = headers; + this.fileName = fileName; + this.value = null; + } + + + @Override + public String getValue() { + if (value == null) { + throw UndertowMessages.MESSAGES.formValueIsAFile(); + } + return value; + } + + @Override + public boolean isFile() { + return file != null; + } + + @Override + public File getFile() { + if (file == null) { + throw UndertowMessages.MESSAGES.formValueIsAString(); + } + return file; + } + + @Override + public HeaderMap getHeaders() { + return headers; + } + + public String getFileName() { + return fileName; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormDataParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormDataParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormDataParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.form; + +import java.io.Closeable; +import java.io.IOException; + +import io.undertow.server.HttpHandler; +import io.undertow.util.AttachmentKey; + +/** + * Parser for form data. This can be used by down-stream handlers to parse + * form data. + *

+ * This parser must be closed to make sure any temporary files have been cleaned up. + * + * @author Stuart Douglas + */ +public interface FormDataParser extends Closeable { + + /** + * When the form data is parsed it will be attached under this key. + */ + AttachmentKey FORM_DATA = AttachmentKey.create(FormData.class); + + /** + * Parse the form data asynchronously. If all the data cannot be read immediately then a read listener will be + * registered, and the data will be parsed by the read thread. + *

+ * When this method completes the handler will be invoked, and the data + * will be attached under {@link #FORM_DATA}. + *

+ * The method can either invoke the next handler directly, or may delegate to the IO thread + * to perform the parsing. + */ + void parse(final HttpHandler next) throws Exception; + + /** + * Parse the data, blocking the current thread until parsing is complete. For blocking handlers this method is + * more efficient than {@link #parse()}, as the calling thread should do that actual parsing, rather than the + * read thread + * + * @return The parsed form data + * @throws IOException If the data could not be read + */ + FormData parseBlocking() throws IOException; + + /** + * Closes the parser, and removes and temporary files that may have been created. + * + * @throws IOException + */ + void close() throws IOException; + + /** + * Sets the character encoding that will be used by this parser. If the request is already processed this will have + * no effect + * + * @param encoding The encoding + */ + void setCharacterEncoding(String encoding); +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormEncodedDataDefinition.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormEncodedDataDefinition.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormEncodedDataDefinition.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,271 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.form; + +import java.io.IOException; +import java.net.URLDecoder; +import java.nio.ByteBuffer; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.SameThreadExecutor; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; + +/** + * Parser definition for form encoded data. This handler takes effect for any request that has a mime type + * of application/x-www-form-urlencoded. The handler attaches a {@link FormDataParser} to the chain + * that can parse the underlying form data asynchronously. + * + * @author Stuart Douglas + */ +public class FormEncodedDataDefinition implements FormParserFactory.ParserDefinition { + + public static final String APPLICATION_X_WWW_FORM_URLENCODED = "application/x-www-form-urlencoded"; + private String defaultEncoding = "ISO-8859-1"; + private boolean forceCreation = false; //if the parser should be created even if the correct headers are missing + + public FormEncodedDataDefinition() { + } + + @Override + public FormDataParser create(final HttpServerExchange exchange) { + String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE); + if (forceCreation || (mimeType != null && mimeType.startsWith(APPLICATION_X_WWW_FORM_URLENCODED))) { + + String charset = defaultEncoding; + String contentType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE); + if (contentType != null) { + String cs = Headers.extractQuotedValueFromHeader(contentType, "charset"); + if (cs != null) { + charset = cs; + } + } + return new FormEncodedDataParser(charset, exchange); + } + return null; + } + + public String getDefaultEncoding() { + return defaultEncoding; + } + + public boolean isForceCreation() { + return forceCreation; + } + + public FormEncodedDataDefinition setForceCreation(boolean forceCreation) { + this.forceCreation = forceCreation; + return this; + } + + public FormEncodedDataDefinition setDefaultEncoding(final String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + return this; + } + + private static final class FormEncodedDataParser implements ChannelListener, FormDataParser { + + private final HttpServerExchange exchange; + private final FormData data; + private final StringBuilder builder = new StringBuilder(); + private String name = null; + private String charset; + private HttpHandler handler; + + //0= parsing name + //1=parsing name, decode required + //2=parsing value + //3=parsing value, decode required + //4=finished + private int state = 0; + + private FormEncodedDataParser(final String charset, final HttpServerExchange exchange) { + this.exchange = exchange; + this.charset = charset; + this.data = new FormData(exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, 1000)); + } + + @Override + public void handleEvent(final StreamSourceChannel channel) { + try { + doParse(channel); + if (state == 4) { + exchange.dispatch(SameThreadExecutor.INSTANCE, handler); + } + } catch (IOException e) { + IoUtils.safeClose(channel); + UndertowLogger.REQUEST_LOGGER.ioExceptionReadingFromChannel(e); + exchange.endExchange(); + + } + } + + private void doParse(final StreamSourceChannel channel) throws IOException { + int c = 0; + final Pooled pooled = exchange.getConnection().getBufferPool().allocate(); + try { + final ByteBuffer buffer = pooled.getResource(); + do { + buffer.clear(); + c = channel.read(buffer); + if (c > 0) { + buffer.flip(); + while (buffer.hasRemaining()) { + byte n = buffer.get(); + switch (state) { + case 0: { + if (n == '=') { + name = builder.toString(); + builder.setLength(0); + state = 2; + } else if (n == '&') { + data.add(builder.toString(), ""); + builder.setLength(0); + state = 0; + } else if (n == '%' || n == '+') { + state = 1; + builder.append((char) n); + } else { + builder.append((char) n); + } + break; + } + case 1: { + if (n == '=') { + name = URLDecoder.decode(builder.toString(), charset); + builder.setLength(0); + state = 2; + } else if (n == '&') { + data.add(URLDecoder.decode(builder.toString(), charset), ""); + builder.setLength(0); + state = 0; + } else { + builder.append((char) n); + } + break; + } + case 2: { + if (n == '&') { + data.add(name, builder.toString()); + builder.setLength(0); + state = 0; + } else if (n == '%' || n == '+') { + state = 3; + builder.append((char) n); + } else { + builder.append((char) n); + } + break; + } + case 3: { + if (n == '&') { + data.add(name, URLDecoder.decode(builder.toString(), charset)); + builder.setLength(0); + state = 0; + } else { + builder.append((char) n); + } + break; + } + } + } + } + } while (c > 0); + if (c == -1) { + if (state == 2) { + data.add(name, builder.toString()); + } else if (state == 3) { + data.add(name, URLDecoder.decode(builder.toString(), charset)); + } else if(builder.length() > 0) { + if(state == 1) { + data.add(URLDecoder.decode(builder.toString(), charset), ""); + } else { + data.add(builder.toString(), ""); + } + } + state = 4; + exchange.putAttachment(FORM_DATA, data); + } + } finally { + pooled.free(); + } + } + + + @Override + public void parse(HttpHandler handler) throws Exception { + if (exchange.getAttachment(FORM_DATA) != null) { + handler.handleRequest(exchange); + return; + } + this.handler = handler; + StreamSourceChannel channel = exchange.getRequestChannel(); + if (channel == null) { + throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided()); + } else { + doParse(channel); + if (state != 4) { + channel.getReadSetter().set(this); + channel.resumeReads(); + } else { + exchange.dispatch(SameThreadExecutor.INSTANCE, handler); + } + } + } + + @Override + public FormData parseBlocking() throws IOException { + final FormData existing = exchange.getAttachment(FORM_DATA); + if (existing != null) { + return existing; + } + + StreamSourceChannel channel = exchange.getRequestChannel(); + if (channel == null) { + throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided()); + } else { + while (state != 4) { + doParse(channel); + if (state != 4) { + channel.awaitReadable(); + } + } + } + return data; + } + + @Override + public void close() throws IOException { + + } + + @Override + public void setCharacterEncoding(final String encoding) { + this.charset = encoding; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormParserFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormParserFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/form/FormParserFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,109 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.form; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +/** + * Factory class that can create a form data parser for a given request. + *

+ * It does this by iterating the available parser definitions, and returning + * the first parser that is created. + * + * @author Stuart Douglas + */ +public class FormParserFactory { + + private static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(FormDataParser.class); + + private final ParserDefinition[] parserDefinitions; + + FormParserFactory(final List parserDefinitions) { + this.parserDefinitions = parserDefinitions.toArray(new ParserDefinition[parserDefinitions.size()]); + } + + /** + * Creates a form data parser for this request. If a parser has already been created for the request the + * existing parser will be returned rather than creating a new one. + * + * @param exchange The exchange + * @return A form data parser, or null if there is no parser registered for the request content type + */ + public FormDataParser createParser(final HttpServerExchange exchange) { + FormDataParser existing = exchange.getAttachment(ATTACHMENT_KEY); + if(existing != null) { + return existing; + } + for (int i = 0; i < parserDefinitions.length; ++i) { + FormDataParser parser = parserDefinitions[i].create(exchange); + if (parser != null) { + exchange.putAttachment(ATTACHMENT_KEY, parser); + return parser; + } + } + return null; + } + + public interface ParserDefinition { + FormDataParser create(final HttpServerExchange exchange); + } + + public static Builder builder() { + return builder(true); + } + + public static Builder builder(boolean includeDefault) { + Builder builder = new Builder(); + if (includeDefault) { + builder.addParsers(new FormEncodedDataDefinition(), new MultiPartParserDefinition()); + } + return builder; + } + + public static class Builder { + + private List parsers = new ArrayList<>(); + + public Builder addParser(final ParserDefinition definition) { + parsers.add(definition); + return this; + } + + public Builder addParsers(final ParserDefinition... definition) { + parsers.addAll(Arrays.asList(definition)); + return this; + } + + public Builder addParsers(final List definition) { + parsers.addAll(definition); + return this; + } + + public FormParserFactory build() { + return new FormParserFactory(parsers); + } + + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/form/MultiPartParserDefinition.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/form/MultiPartParserDefinition.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/form/MultiPartParserDefinition.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,315 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.form; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.MalformedMessageException; +import io.undertow.util.MultipartParser; +import io.undertow.util.SameThreadExecutor; +import org.xnio.FileAccess; +import org.xnio.IoUtils; + +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * @author Stuart Douglas + */ +public class MultiPartParserDefinition implements FormParserFactory.ParserDefinition { + + public static final String MULTIPART_FORM_DATA = "multipart/form-data"; + + private Executor executor; + + private File tempFileLocation; + + private String defaultEncoding = "ISO-8859-1"; + + private long maxIndividualFileSize = -1; + + public MultiPartParserDefinition() { + tempFileLocation = new File(System.getProperty("java.io.tmpdir")); + } + + public MultiPartParserDefinition(final File tempDir) { + tempFileLocation = tempDir; + } + + @Override + public FormDataParser create(final HttpServerExchange exchange) { + String mimeType = exchange.getRequestHeaders().getFirst(Headers.CONTENT_TYPE); + if (mimeType != null && mimeType.startsWith(MULTIPART_FORM_DATA)) { + String boundary = Headers.extractTokenFromHeader(mimeType, "boundary"); + if (boundary == null) { + UndertowLogger.REQUEST_LOGGER.debugf("Could not find boundary in multipart request with ContentType: %s, multipart data will not be available", mimeType); + return null; + } + final MultiPartUploadHandler parser = new MultiPartUploadHandler(exchange, boundary, maxIndividualFileSize, defaultEncoding); + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(final HttpServerExchange exchange, final NextListener nextListener) { + IoUtils.safeClose(parser); + nextListener.proceed(); + } + }); + return parser; + + } + return null; + } + + public Executor getExecutor() { + return executor; + } + + public MultiPartParserDefinition setExecutor(final Executor executor) { + this.executor = executor; + return this; + } + + public File getTempFileLocation() { + return tempFileLocation; + } + + public MultiPartParserDefinition setTempFileLocation(File tempFileLocation) { + this.tempFileLocation = tempFileLocation; + return this; + } + + public String getDefaultEncoding() { + return defaultEncoding; + } + + public MultiPartParserDefinition setDefaultEncoding(final String defaultEncoding) { + this.defaultEncoding = defaultEncoding; + return this; + } + + public long getMaxIndividualFileSize() { + return maxIndividualFileSize; + } + + public void setMaxIndividualFileSize(final long maxIndividualFileSize) { + this.maxIndividualFileSize = maxIndividualFileSize; + } + + private final class MultiPartUploadHandler implements FormDataParser, Runnable, MultipartParser.PartHandler { + + private final HttpServerExchange exchange; + private final FormData data; + private final String boundary; + private final List createdFiles = new ArrayList<>(); + private final long maxIndividualFileSize; + private String defaultEncoding; + + private final ByteArrayOutputStream contentBytes = new ByteArrayOutputStream(); + private String currentName; + private String fileName; + private File file; + private FileChannel fileChannel; + private HeaderMap headers; + private HttpHandler handler; + private long currentFileSize; + + + private MultiPartUploadHandler(final HttpServerExchange exchange, final String boundary, final long maxIndividualFileSize, final String defaultEncoding) { + this.exchange = exchange; + this.boundary = boundary; + this.maxIndividualFileSize = maxIndividualFileSize; + this.defaultEncoding = defaultEncoding; + this.data = new FormData(exchange.getConnection().getUndertowOptions().get(UndertowOptions.MAX_PARAMETERS, 1000)); + } + + + @Override + public void parse(final HttpHandler handler) throws Exception { + if (exchange.getAttachment(FORM_DATA) != null) { + handler.handleRequest(exchange); + return; + } + this.handler = handler; + //we need to delegate to a thread pool + //as we parse with blocking operations + if (executor == null) { + exchange.dispatch(this); + } else { + exchange.dispatch(executor, this); + } + } + + @Override + public FormData parseBlocking() throws IOException { + final FormData existing = exchange.getAttachment(FORM_DATA); + if (existing != null) { + return existing; + } + + final MultipartParser.ParseState parser = MultipartParser.beginParse(exchange.getConnection().getBufferPool(), this, boundary.getBytes(), exchange.getRequestCharset()); + InputStream inputStream = exchange.getInputStream(); + if (inputStream == null) { + throw new IOException(UndertowMessages.MESSAGES.requestChannelAlreadyProvided()); + } + byte[] buf = new byte[1024]; + try { + while (true) { + int c = inputStream.read(buf); + if (c == -1) { + if (parser.isComplete()) { + break; + } else { + throw UndertowMessages.MESSAGES.connectionTerminatedReadingMultiPartData(); + } + } else if (c != 0) { + parser.parse(ByteBuffer.wrap(buf, 0, c)); + } + } + exchange.putAttachment(FORM_DATA, data); + } catch (MalformedMessageException e) { + throw new IOException(e); + } + return exchange.getAttachment(FORM_DATA); + } + + @Override + public void run() { + try { + parseBlocking(); + exchange.dispatch(SameThreadExecutor.INSTANCE, handler); + } catch (Throwable e) { + UndertowLogger.REQUEST_LOGGER.debug("Exception parsing data", e); + exchange.setResponseCode(500); + exchange.endExchange(); + } + } + + @Override + public void beginPart(final HeaderMap headers) { + this.currentFileSize = 0; + this.headers = headers; + final String disposition = headers.getFirst(Headers.CONTENT_DISPOSITION); + if (disposition != null) { + if (disposition.startsWith("form-data")) { + currentName = Headers.extractQuotedValueFromHeader(disposition, "name"); + fileName = Headers.extractQuotedValueFromHeader(disposition, "filename"); + if (fileName != null) { + try { + file = File.createTempFile("undertow", "upload", tempFileLocation); + createdFiles.add(file); + fileChannel = exchange.getConnection().getWorker().getXnio().openFile(file, FileAccess.READ_WRITE); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + } + } + + @Override + public void data(final ByteBuffer buffer) throws IOException { + this.currentFileSize += buffer.remaining(); + if (this.maxIndividualFileSize > 0 && this.currentFileSize > this.maxIndividualFileSize) { + throw UndertowMessages.MESSAGES.maxFileSizeExceeded(this.maxIndividualFileSize); + } + if (file == null) { + while (buffer.hasRemaining()) { + contentBytes.write(buffer.get()); + } + } else { + fileChannel.write(buffer); + } + } + + @Override + public void endPart() { + if (file != null) { + data.add(currentName, file, fileName, headers); + file = null; + try { + fileChannel.close(); + fileChannel = null; + } catch (IOException e) { + throw new RuntimeException(e); + } + } else { + + + try { + String charset = defaultEncoding; + String contentType = headers.getFirst(Headers.CONTENT_TYPE); + if (contentType != null) { + String cs = Headers.extractQuotedValueFromHeader(contentType, "charset"); + if (cs != null) { + charset = cs; + } + } + + data.add(currentName, new String(contentBytes.toByteArray(), charset), headers); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + contentBytes.reset(); + } + } + + + public List getCreatedFiles() { + return createdFiles; + } + + @Override + public void close() throws IOException { + //we have to dispatch this, as it may result in file IO + final List files = new ArrayList<>(getCreatedFiles()); + exchange.getConnection().getWorker().execute(new Runnable() { + @Override + public void run() { + for (final File file : files) { + if (file.exists()) { + if (!file.delete()) { + UndertowLogger.REQUEST_LOGGER.cannotRemoveUploadedFile(file); + } + } + } + } + }); + + } + + @Override + public void setCharacterEncoding(final String encoding) { + this.defaultEncoding = encoding; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ConnectionPoolErrorHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ConnectionPoolErrorHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ConnectionPoolErrorHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,138 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +/** + * The connection pool error handler is intended to be used per node and will therefore be shared across I/O threads. + * + * @author Emanuel Muckenhuber + */ +public interface ConnectionPoolErrorHandler { + + /** + * Check whether pool is available + * + * @return whether the pool is available + */ + boolean isAvailable(); + + /** + * Handle a connection error. + * + * @return {@code true} if the pool is still available, {@code false} otherwise + */ + boolean handleError(); + + /** + * Clear the connection errors. + * + * @return {@code true} if the pool is available again, {@code false} otherwise + */ + boolean clearError(); + + class SimpleConnectionPoolErrorHandler implements ConnectionPoolErrorHandler { + + private volatile boolean problem; + + @Override + public boolean isAvailable() { + return !problem; + } + + @Override + public boolean handleError() { + problem = true; + return false; + } + + @Override + public boolean clearError() { + problem = false; + return true; + } + } + + /** + * Counting error handler, this only propagates the state to the delegate handler after reaching a given limit. + */ + class CountingErrorHandler implements ConnectionPoolErrorHandler { + + private int count; + private long timeout; + + private final long interval; + private final int errorCount; + private final int successCount; + private final ConnectionPoolErrorHandler delegate; + + public CountingErrorHandler(int errorCount, int successCount, long interval) { + this(errorCount, successCount, interval, new SimpleConnectionPoolErrorHandler()); + } + + public CountingErrorHandler(int errorCount, int successCount, long interval, ConnectionPoolErrorHandler delegate) { + this.errorCount = Math.max(errorCount, 1); + this.successCount = Math.max(successCount, 1); + this.interval = Math.max(interval, 0); + this.delegate = delegate; + } + + @Override + public boolean isAvailable() { + return delegate.isAvailable(); + } + + @Override + public synchronized boolean handleError() { + if (delegate.isAvailable()) { + final long time = System.currentTimeMillis(); + // If the timeout is reached reset the error count + if (time >= timeout) { + count = 1; + timeout = time + interval; + } else { + if (count++ == 1) { + timeout = time + interval; + } + } + if (count >= errorCount) { + return delegate.handleError(); + } + return true; + } else { + count = 0; // if in error reset the successful count + return false; + } + } + + @Override + public synchronized boolean clearError() { + if (delegate.isAvailable()) { + count = 0; // Just reset the error count + return true; + } else { + // Count the successful attempts + if (count++ == successCount) { + return delegate.clearError(); + } + return false; + } + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ConnectionPoolManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ConnectionPoolManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ConnectionPoolManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,34 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +/** + * Manager that controls the behaviour of a {@link ProxyConnectionPool} + * + * @author Stuart Douglas + */ +public interface ConnectionPoolManager extends ProxyConnectionPoolConfig, ConnectionPoolErrorHandler { + + /** + * + * @return The amount of time that we should wait before re-testing a problem server + */ + int getProblemServerRetry(); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ExclusivityChecker.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ExclusivityChecker.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ExclusivityChecker.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,35 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.server.HttpServerExchange; + +/** + * Interface that is used to determine if a connection should be exclusive. + * + * If a connection is exclusive then it is removed from the connection pool, and a one + * to one mapping will be maintained between the front and back end servers. + * +* @author Stuart Douglas +*/ +public interface ExclusivityChecker { + + boolean isExclusivityRequired(HttpServerExchange exchange); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/HostTable.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/HostTable.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/HostTable.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,140 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.UndertowMessages; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.PathMatcher; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; + +/** + * Class that maintains a table of remote hosts that this proxy knows about. + * + * Basically this maps a virtual host + context path pair to a set of hosts. + * + * Note that this class does not have any knowledge of connection pooling + * + * @author Stuart Douglas + */ +public class HostTable { + + private final Map> hosts = new CopyOnWriteMap<>(); + private final Map>> targets = new CopyOnWriteMap<>(); + + public synchronized HostTable addHost(H host) { + if(hosts.containsKey(host)) { + throw UndertowMessages.MESSAGES.hostAlreadyRegistered(host); + } + hosts.put(host, new CopyOnWriteArraySet()); + return this; + } + + public synchronized HostTable removeHost(H host) { + Set targets = hosts.remove(host); + for(Target target : targets) { + removeRoute(host, target.virtualHost, target.contextPath); + } + return this; + } + + public synchronized HostTable addRoute(H host, String virtualHost, String contextPath) { + Set hostData = hosts.get(host); + if(hostData == null) { + throw UndertowMessages.MESSAGES.hostHasNotBeenRegistered(host); + } + hostData.add(new Target(virtualHost, contextPath)); + PathMatcher> paths = targets.get(virtualHost); + if(paths == null) { + paths = new PathMatcher<>(); + targets.put(virtualHost, paths); + } + Set hostSet = paths.getPrefixPath(contextPath); + if(hostSet == null) { + hostSet = new CopyOnWriteArraySet<>(); + paths.addPrefixPath(contextPath, hostSet); + } + hostSet.add(host); + return this; + } + + public synchronized HostTable removeRoute(H host, String virtualHost, String contextPath) { + + Set hostData = hosts.get(host); + if(hostData != null) { + hostData.remove(new Target(virtualHost, contextPath)); + } + PathMatcher> paths = targets.get(virtualHost); + if(paths == null) { + return this; + } + Set hostSet = paths.getPrefixPath(contextPath); + if(hostSet == null) { + return this; + } + hostSet.remove(host); + if(hostSet.isEmpty()) { + paths.removePrefixPath(contextPath); + } + return this; + } + + public Set getHostsForTarget(final String hostName, final String path) { + PathMatcher> matcher = targets.get(hostName); + if(matcher == null) { + return null; + } + return matcher.match(path).getValue(); + } + + private static final class Target { + final String virtualHost; + final String contextPath; + + private Target(String virtualHost, String contextPath) { + this.virtualHost = virtualHost; + this.contextPath = contextPath; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Target target = (Target) o; + + if (contextPath != null ? !contextPath.equals(target.contextPath) : target.contextPath != null) + return false; + if (virtualHost != null ? !virtualHost.equals(target.virtualHost) : target.virtualHost != null) + return false; + + return true; + } + + @Override + public int hashCode() { + int result = virtualHost != null ? virtualHost.hashCode() : 0; + result = 31 * result + (contextPath != null ? contextPath.hashCode() : 0); + return result; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/LoadBalancingProxyClient.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/LoadBalancingProxyClient.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/LoadBalancingProxyClient.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,383 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.UndertowLogger; +import io.undertow.client.ClientConnection; +import io.undertow.client.UndertowClient; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.ServerConnection; +import io.undertow.server.handlers.Cookie; +import io.undertow.util.AttachmentKey; +import io.undertow.util.CopyOnWriteMap; +import org.xnio.OptionMap; +import org.xnio.ssl.XnioSsl; + +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArraySet; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static io.undertow.server.handlers.proxy.ProxyConnectionPool.AvailabilityType.*; +import static org.xnio.IoUtils.safeClose; + +/** + * Initial implementation of a load balancing proxy client. This initial implementation is rather simplistic, and + * will likely change. + *

+ * + * @author Stuart Douglas + */ +public class LoadBalancingProxyClient implements ProxyClient { + + /** + * The attachment key that is used to attach the proxy connection to the exchange. + *

+ * This cannot be static as otherwise a connection from a different client could be re-used. + */ + private final AttachmentKey exclusiveConnectionKey = AttachmentKey.create(ExclusiveConnectionHolder.class); + + + /** + * Time in seconds between retries for problem servers + */ + private volatile int problemServerRetry = 10; + + private final Set sessionCookieNames = new CopyOnWriteArraySet<>(); + + /** + * The number of connections to create per thread + */ + private volatile int connectionsPerThread = 10; + + /** + * The hosts list. + */ + private volatile Host[] hosts = {}; + + private final AtomicInteger currentHost = new AtomicInteger(0); + private final UndertowClient client; + + private final Map routes = new CopyOnWriteMap<>(); + + private final ExclusivityChecker exclusivityChecker; + + private static final ProxyTarget PROXY_TARGET = new ProxyTarget() { + }; + + public LoadBalancingProxyClient() { + this(UndertowClient.getInstance()); + } + + public LoadBalancingProxyClient(UndertowClient client) { + this(client, null); + } + + public LoadBalancingProxyClient(ExclusivityChecker client) { + this(UndertowClient.getInstance(), client); + } + + public LoadBalancingProxyClient(UndertowClient client, ExclusivityChecker exclusivityChecker) { + this.client = client; + this.exclusivityChecker = exclusivityChecker; + sessionCookieNames.add("JSESSIONID"); + } + + public LoadBalancingProxyClient addSessionCookieName(final String sessionCookieName) { + sessionCookieNames.add(sessionCookieName); + return this; + } + + public LoadBalancingProxyClient removeSessionCookieName(final String sessionCookieName) { + sessionCookieNames.remove(sessionCookieName); + return this; + } + + public LoadBalancingProxyClient setProblemServerRetry(int problemServerRetry) { + this.problemServerRetry = problemServerRetry; + return this; + } + + public int getProblemServerRetry() { + return problemServerRetry; + } + + public int getConnectionsPerThread() { + return connectionsPerThread; + } + + public LoadBalancingProxyClient setConnectionsPerThread(int connectionsPerThread) { + this.connectionsPerThread = connectionsPerThread; + return this; + } + + public synchronized LoadBalancingProxyClient addHost(final URI host) { + return addHost(host, null, null); + } + + public synchronized LoadBalancingProxyClient addHost(final URI host, XnioSsl ssl) { + return addHost(host, null, ssl); + } + + public synchronized LoadBalancingProxyClient addHost(final URI host, String jvmRoute) { + return addHost(host, jvmRoute, null); + } + + + public synchronized LoadBalancingProxyClient addHost(final URI host, String jvmRoute, XnioSsl ssl) { + + Host h = new Host(jvmRoute, null, host, ssl, OptionMap.EMPTY); + Host[] existing = hosts; + Host[] newHosts = new Host[existing.length + 1]; + System.arraycopy(existing, 0, newHosts, 0, existing.length); + newHosts[existing.length] = h; + this.hosts = newHosts; + if (jvmRoute != null) { + this.routes.put(jvmRoute, h); + } + return this; + } + + + public synchronized LoadBalancingProxyClient addHost(final URI host, String jvmRoute, XnioSsl ssl, OptionMap options) { + return addHost(null, host, jvmRoute, ssl, options); + } + + + public synchronized LoadBalancingProxyClient addHost(final InetSocketAddress bindAddress, final URI host, String jvmRoute, XnioSsl ssl, OptionMap options) { + Host h = new Host(jvmRoute, bindAddress, host, ssl, options); + Host[] existing = hosts; + Host[] newHosts = new Host[existing.length + 1]; + System.arraycopy(existing, 0, newHosts, 0, existing.length); + newHosts[existing.length] = h; + this.hosts = newHosts; + if (jvmRoute != null) { + this.routes.put(jvmRoute, h); + } + return this; + } + + public synchronized LoadBalancingProxyClient removeHost(final URI uri) { + int found = -1; + Host[] existing = hosts; + Host removedHost = null; + for (int i = 0; i < existing.length; ++i) { + if (existing[i].uri.equals(uri)) { + found = i; + removedHost = existing[i]; + break; + } + } + if (found == -1) { + return this; + } + Host[] newHosts = new Host[existing.length - 1]; + System.arraycopy(existing, 0, newHosts, 0, found); + System.arraycopy(existing, found + 1, newHosts, found, existing.length - found - 1); + this.hosts = newHosts; + removedHost.connectionPool.close(); + if (removedHost.jvmRoute != null) { + routes.remove(removedHost.jvmRoute); + } + return this; + } + + @Override + public ProxyTarget findTarget(HttpServerExchange exchange) { + return PROXY_TARGET; + } + + @Override + public void getConnection(ProxyTarget target, HttpServerExchange exchange, final ProxyCallback callback, long timeout, TimeUnit timeUnit) { + final ExclusiveConnectionHolder holder = exchange.getConnection().getAttachment(exclusiveConnectionKey); + if (holder != null && holder.connection.getConnection().isOpen()) { + // Something has already caused an exclusive connection to be allocated so keep using it. + callback.completed(exchange, holder.connection); + return; + } + + final Host host = selectHost(exchange); + if (host == null) { + callback.couldNotResolveBackend(exchange); + } else { + if (holder != null || (exclusivityChecker != null && exclusivityChecker.isExclusivityRequired(exchange))) { + // If we have a holder, even if the connection was closed we now exclusivity was already requested so our client + // may be assuming it still exists. + host.connectionPool.connect(target, exchange, new ProxyCallback() { + + @Override + public void completed(HttpServerExchange exchange, ProxyConnection result) { + if (holder != null) { + holder.connection = result; + } else { + final ExclusiveConnectionHolder newHolder = new ExclusiveConnectionHolder(); + newHolder.connection = result; + ServerConnection connection = exchange.getConnection(); + connection.putAttachment(exclusiveConnectionKey, newHolder); + connection.addCloseListener(new ServerConnection.CloseListener() { + + @Override + public void closed(ServerConnection connection) { + ClientConnection clientConnection = newHolder.connection.getConnection(); + if (clientConnection.isOpen()) { + safeClose(clientConnection); + } + } + }); + } + callback.completed(exchange, result); + } + + @Override + public void queuedRequestFailed(HttpServerExchange exchange) { + callback.queuedRequestFailed(exchange); + } + + @Override + public void failed(HttpServerExchange exchange) { + UndertowLogger.PROXY_REQUEST_LOGGER.proxyFailedToConnectToBackend(exchange.getRequestURI(), host.uri); + callback.failed(exchange); + } + + @Override + public void couldNotResolveBackend(HttpServerExchange exchange) { + callback.couldNotResolveBackend(exchange); + } + }, timeout, timeUnit, true); + } else { + host.connectionPool.connect(target, exchange, callback, timeout, timeUnit, false); + } + } + } + + protected Host selectHost(HttpServerExchange exchange) { + Host[] hosts = this.hosts; + if (hosts.length == 0) { + return null; + } + Host sticky = findStickyHost(exchange); + if (sticky != null) { + return sticky; + } + int host = currentHost.incrementAndGet() % hosts.length; + + final int startHost = host; //if the all hosts have problems we come back to this one + Host full = null; + Host problem = null; + do { + Host selected = hosts[host]; + ProxyConnectionPool.AvailabilityType available = selected.connectionPool.available(); + if (available == AVAILABLE) { + return selected; + } else if (available == FULL && full == null) { + full = selected; + } else if (available == PROBLEM && problem == null) { + problem = selected; + } + host = (host + 1) % hosts.length; + } while (host != startHost); + if (full != null) { + return full; + } + if (problem != null) { + return problem; + } + //no available hosts + return null; + } + + protected Host findStickyHost(HttpServerExchange exchange) { + Map cookies = exchange.getRequestCookies(); + for (String cookieName : sessionCookieNames) { + Cookie sk = cookies.get(cookieName); + if (sk != null) { + int index = sk.getValue().indexOf('.'); + + if (index == -1) { + continue; + } + String route = sk.getValue().substring(index + 1); + index = route.indexOf('.'); + if (index != -1) { + route = route.substring(0, index); + } + return routes.get(route); + } + } + return null; + } + + protected final class Host extends ConnectionPoolErrorHandler.SimpleConnectionPoolErrorHandler implements ConnectionPoolManager { + final ProxyConnectionPool connectionPool; + final String jvmRoute; + final URI uri; + final XnioSsl ssl; + + private Host(String jvmRoute, InetSocketAddress bindAddress, URI uri, XnioSsl ssl, OptionMap options) { + this.connectionPool = new ProxyConnectionPool(this, bindAddress, uri, ssl, client, options); + this.jvmRoute = jvmRoute; + this.uri = uri; + this.ssl = ssl; + } + + // @Override + public void queuedConnectionFailed(ProxyTarget proxyTarget, HttpServerExchange exchange, ProxyCallback callback, long timeoutMills) { + getConnection(proxyTarget, exchange, callback, timeoutMills, TimeUnit.MILLISECONDS); + } + + @Override + public int getProblemServerRetry() { + return problemServerRetry; + } + + @Override + public int getMaxConnections() { + return connectionsPerThread; + } + + @Override + public int getMaxCachedConnections() { + return connectionsPerThread; + } + + @Override + public int getSMaxConnections() { + return connectionsPerThread; + } + + @Override + public long getTtl() { + return -1; + } + + @Override + public int getMaxQueueSize() { + return 0; + } + } + + private static class ExclusiveConnectionHolder { + + private ProxyConnection connection; + + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyCallback.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyCallback.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyCallback.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,55 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.server.HttpServerExchange; + +/** + * Yet another callback class, this one used by the proxy handler + * + * @author Stuart Douglas + */ +public interface ProxyCallback { + + void completed(final HttpServerExchange exchange, T result); + + /** + * Callback if establishing the connection to a backend server fails. + * + * @param exchange the http server exchange + */ + void failed(final HttpServerExchange exchange); + + /** + * Callback if no backend server could be found. + * + * @param exchange the http server exchange + */ + void couldNotResolveBackend(final HttpServerExchange exchange); + + /** + * This is invoked when the target connection pool transitions to problem status. It will be called once for each queued request + * that has not yet been allocated a connection. The manager can redistribute these requests to other hosts, or can end the + * exchange with an error status. + * + * @param exchange The exchange + */ + void queuedRequestFailed(HttpServerExchange exchange); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyClient.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyClient.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyClient.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,66 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.server.HttpServerExchange; + +import java.util.concurrent.TimeUnit; + +/** + * A client that provides connections for the proxy handler. The provided connection is valid for the duration of the + * current exchange. + * + * Note that implementation are required to manage the lifecycle of these connections themselves, generally by registering callbacks + * on the exchange. + * + * + * + * + * @author Stuart Douglas + */ +public interface ProxyClient { + + /** + * Finds a proxy target for this request, returning null if none can be found. + * + * If this method returns null it means that there is no backend available to handle + * this request, and it should proceed as normal. + * + * @param exchange The exchange + * @return The proxy target + */ + ProxyTarget findTarget(final HttpServerExchange exchange); + + /** + * Gets a proxy connection for the given request. + * + * @param exchange The exchange + * @param callback The callback + * @param timeout The timeout + * @param timeUnit Time unit for the timeout + */ + void getConnection(final ProxyTarget target, final HttpServerExchange exchange, final ProxyCallback callback, long timeout, TimeUnit timeUnit); + + /** + * An opaque interface that may contain information about the proxy target + */ + public interface ProxyTarget { + + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.client.ClientConnection; + +/** + * A connection to a backend proxy. + * + * @author Stuart Douglas + */ +public class ProxyConnection { + + private final ClientConnection connection; + private final String targetPath; + + public ProxyConnection(ClientConnection connection, String targetPath) { + this.connection = connection; + this.targetPath = targetPath; + } + + public ClientConnection getConnection() { + return connection; + } + + public String getTargetPath() { + return targetPath; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnectionPool.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnectionPool.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnectionPool.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,546 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.UndertowClient; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.CopyOnWriteMap; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.ssl.XnioSsl; + +/** + * A pool of connections to a target host. + * + * This pool can also be used to open connections in exclusive mode, in which case they will not be added to the connection pool. + * + * In this case the caller is responsible for closing any connections. + * + * @author Stuart Douglas + */ +public class ProxyConnectionPool implements Closeable { + + private final URI uri; + + private final InetSocketAddress bindAddress; + + private final XnioSsl ssl; + + private final UndertowClient client; + + private final ConnectionPoolManager connectionPoolManager; + + private final OptionMap options; + + /** + * Set to true when the connection pool is closed. + */ + private volatile boolean closed; + + private final int maxConnections; + private final int maxCachedConnections; + private final int sMaxConnections; + private final int maxRequestQueueSize; + private final long ttl; + + private final ConcurrentMap hostThreadData = new CopyOnWriteMap<>(); + + public ProxyConnectionPool(ConnectionPoolManager connectionPoolManager, URI uri, UndertowClient client, OptionMap options) { + this(connectionPoolManager, uri, null, client, options); + } + + public ProxyConnectionPool(ConnectionPoolManager connectionPoolManager,InetSocketAddress bindAddress, URI uri, UndertowClient client, OptionMap options) { + this(connectionPoolManager, bindAddress, uri, null, client, options); + } + + public ProxyConnectionPool(ConnectionPoolManager connectionPoolManager, URI uri, XnioSsl ssl, UndertowClient client, OptionMap options) { + this(connectionPoolManager, null, uri, ssl, client, options); + } + + public ProxyConnectionPool(ConnectionPoolManager connectionPoolManager, InetSocketAddress bindAddress,URI uri, XnioSsl ssl, UndertowClient client, OptionMap options) { + this.connectionPoolManager = connectionPoolManager; + this.maxConnections = Math.max(connectionPoolManager.getMaxConnections(), 1); + this.maxCachedConnections = Math.max(connectionPoolManager.getMaxCachedConnections(), 0); + this.sMaxConnections = Math.max(connectionPoolManager.getSMaxConnections(), 0); + this.maxRequestQueueSize = Math.max(connectionPoolManager.getMaxQueueSize(), 0); + this.ttl = connectionPoolManager.getTtl(); + this.bindAddress = bindAddress; + this.uri = uri; + this.ssl = ssl; + this.client = client; + this.options = options; + } + + public URI getUri() { + return uri; + } + + public InetSocketAddress getBindAddress() { + return bindAddress; + } + + public void close() { + this.closed = true; + for (HostThreadData data : hostThreadData.values()) { + final ConnectionHolder holder = data.availableConnections.poll(); + if (holder != null) { + IoUtils.safeClose(holder.clientConnection); + } + } + } + + /** + * Called when the IO thread has completed a successful request + * + * @param connectionHolder The client connection holder + */ + private void returnConnection(final ConnectionHolder connectionHolder) { + HostThreadData hostData = getData(); + if (closed) { + //the host has been closed + IoUtils.safeClose(connectionHolder.clientConnection); + ConnectionHolder con = hostData.availableConnections.poll(); + while (con != null) { + IoUtils.safeClose(con.clientConnection); + con = hostData.availableConnections.poll(); + } + redistributeQueued(hostData); + return; + } + + //only do something if the connection is open. If it is closed then + //the close setter will handle creating a new connection and decrementing + //the connection count + final ClientConnection connection = connectionHolder.clientConnection; + if (connection.isOpen() && !connection.isUpgraded()) { + CallbackHolder callback = hostData.awaitingConnections.poll(); + while (callback != null && callback.isCancelled()) { + callback = hostData.awaitingConnections.poll(); + } + if (callback != null) { + if (callback.getTimeoutKey() != null) { + callback.getTimeoutKey().remove(); + } + // Anything waiting for a connection is not expecting exclusivity. + connectionReady(connectionHolder, callback.getCallback(), callback.getExchange(), false); + } else { + final int cachedConnectionCount = hostData.availableConnections.size(); + if (cachedConnectionCount >= maxCachedConnections) { + // Close the longest idle connection instead of the current one + final ConnectionHolder holder = hostData.availableConnections.poll(); + if (holder != null) { + IoUtils.safeClose(holder.clientConnection); + } + } + hostData.availableConnections.add(connectionHolder); + // If the soft max and ttl are configured + if (sMaxConnections >= 0 && ttl > 0) { + final long currentTime = System.currentTimeMillis(); + connectionHolder.timeout = currentTime + ttl; + timeoutConnections(currentTime, hostData); + } + } + } else if (connection.isOpen() && connection.isUpgraded()) { + //we treat upgraded connections as closed + //as we do not want the connection pool filled with upgraded connections + //if the connection is actually closed the close setter will handle it + connection.getCloseSetter().set(null); + handleClosedConnection(hostData, connectionHolder); + } + } + + private void handleClosedConnection(HostThreadData hostData, final ConnectionHolder connection) { + + int connections = --hostData.connections; + hostData.availableConnections.remove(connection); + if (connections < maxConnections) { + CallbackHolder task = hostData.awaitingConnections.poll(); + while (task != null && task.isCancelled()) { + task = hostData.awaitingConnections.poll(); + } + if (task != null) { + openConnection(task.exchange, task.callback, hostData, false); + } + } + } + + private void openConnection(final HttpServerExchange exchange, final ProxyCallback callback, final HostThreadData data, final boolean exclusive) { + if (!exclusive) { + data.connections++; + } + client.connect(new ClientCallback() { + @Override + public void completed(final ClientConnection result) { + final ConnectionHolder connectionHolder = new ConnectionHolder(result); + if (!exclusive) { + result.getCloseSetter().set(new ChannelListener() { + @Override + public void handleEvent(ClientConnection channel) { + handleClosedConnection(data, connectionHolder); + } + }); + } + connectionReady(connectionHolder, callback, exchange, exclusive); + } + + @Override + public void failed(IOException e) { + if (!exclusive) { + data.connections--; + } + UndertowLogger.REQUEST_LOGGER.debug("Failed to connect", e); + if (!connectionPoolManager.handleError()) { + redistributeQueued(getData()); + scheduleFailedHostRetry(exchange); + } + callback.failed(exchange); + } + }, bindAddress, getUri(), exchange.getIoThread(), ssl, exchange.getConnection().getBufferPool(), options); + } + + private void redistributeQueued(HostThreadData hostData) { + CallbackHolder callback = hostData.awaitingConnections.poll(); + while (callback != null) { + if (callback.getTimeoutKey() != null) { + callback.getTimeoutKey().remove(); + } + if (!callback.isCancelled()) { + long time = System.currentTimeMillis(); + if (callback.getExpireTime() > 0 && callback.getExpireTime() < time) { + callback.getCallback().failed(callback.getExchange()); + } else { + callback.getCallback().queuedRequestFailed(callback.getExchange()); + } + } + callback = hostData.awaitingConnections.poll(); + } + } + + private void connectionReady(final ConnectionHolder result, final ProxyCallback callback, final HttpServerExchange exchange, final boolean exclusive) { + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + if (!exclusive) { + returnConnection(result); + } + nextListener.proceed(); + } + }); + + callback.completed(exchange, new ProxyConnection(result.clientConnection, uri.getPath() == null ? "/" : uri.getPath())); + } + + public AvailabilityType available() { + if (closed) { + return AvailabilityType.CLOSED; + } + if (!connectionPoolManager.isAvailable()) { + return AvailabilityType.PROBLEM; + } + HostThreadData data = getData(); + if (data.connections < maxConnections) { + return AvailabilityType.AVAILABLE; + } + if (!data.availableConnections.isEmpty()) { + return AvailabilityType.AVAILABLE; + } + if (data.awaitingConnections.size() >= maxConnections) { + return AvailabilityType.FULL_QUEUE; + } + return AvailabilityType.FULL; + } + + /** + * If a host fails we periodically retry + * + * @param exchange The server exchange + */ + private void scheduleFailedHostRetry(final HttpServerExchange exchange) { + final int retry = connectionPoolManager.getProblemServerRetry(); + // only schedule a retry task if the node is not available + if (retry > 0 && !connectionPoolManager.isAvailable()) { + exchange.getIoThread().executeAfter(new Runnable() { + @Override + public void run() { + if (closed) { + return; + } + + UndertowLogger.PROXY_REQUEST_LOGGER.debugf("Attempting to reconnect to failed host %s", getUri()); + client.connect(new ClientCallback() { + @Override + public void completed(ClientConnection result) { + UndertowLogger.PROXY_REQUEST_LOGGER.debugf("Connected to previously failed host %s, returning to service", getUri()); + if (connectionPoolManager.clearError()) { + // In case the node is available now, return the connection + final ConnectionHolder connectionHolder = new ConnectionHolder(result); + final HostThreadData data = getData(); + result.getCloseSetter().set(new ChannelListener() { + @Override + public void handleEvent(ClientConnection channel) { + handleClosedConnection(data, connectionHolder); + } + }); + data.connections++; + returnConnection(connectionHolder); + } else { + // Otherwise reschedule the retry task + scheduleFailedHostRetry(exchange); + } + } + + @Override + public void failed(IOException e) { + UndertowLogger.PROXY_REQUEST_LOGGER.debugf("Failed to reconnect to failed host %s", getUri()); + connectionPoolManager.handleError(); + scheduleFailedHostRetry(exchange); + } + }, bindAddress, getUri(), exchange.getIoThread(), ssl, exchange.getConnection().getBufferPool(), options); + } + }, retry, TimeUnit.SECONDS); + } + } + + /** + * Timeout idle connections which are above the soft max cached connections limit. + * + * @param currentTime the current time + * @param data the local host thread data + */ + private void timeoutConnections(final long currentTime, final HostThreadData data) { + int idleConnections = data.availableConnections.size(); + for (;;) { + ConnectionHolder holder; + if (idleConnections > 0 && idleConnections >= sMaxConnections && (holder = data.availableConnections.peek()) != null) { + if (!holder.clientConnection.isOpen()) { + // Already closed connections decrease the available connections + idleConnections--; + } else if (currentTime >= holder.timeout) { + // If the timeout is reached already, just close + holder = data.availableConnections.poll(); + IoUtils.safeClose(holder.clientConnection); + idleConnections--; + } else { + // If the next run is after the connection timeout don't reschedule the task + if (data.timeoutKey == null || data.nextTimeout > holder.timeout) { + if (data.timeoutKey != null) { + data.timeoutKey.remove(); + data.timeoutKey = null; + } + // Schedule a timeout task + final long remaining = holder.timeout - currentTime + 1; + data.nextTimeout = holder.timeout; + data.timeoutKey = holder.clientConnection.getIoThread().executeAfter(data.timeoutTask, remaining, TimeUnit.MILLISECONDS); + } + return; + } + } else { + // If we are below the soft limit, just cancel the task + if (data.timeoutKey != null) { + data.timeoutKey.remove(); + data.timeoutKey = null; + } + return; + } + } + } + + /** + * Gets the host data for this thread + * + * @return The data for this thread + */ + private HostThreadData getData() { + Thread thread = Thread.currentThread(); + if (!(thread instanceof XnioIoThread)) { + throw UndertowMessages.MESSAGES.canOnlyBeCalledByIoThread(); + } + XnioIoThread ioThread = (XnioIoThread) thread; + HostThreadData data = hostThreadData.get(ioThread); + if (data != null) { + return data; + } + data = new HostThreadData(); + HostThreadData existing = hostThreadData.putIfAbsent(ioThread, data); + if (existing != null) { + return existing; + } + return data; + } + + /** + * @param exclusive - Is connection for the exclusive use of one client? + */ + public void connect(ProxyClient.ProxyTarget proxyTarget, HttpServerExchange exchange, ProxyCallback callback, final long timeout, final TimeUnit timeUnit, boolean exclusive) { + HostThreadData data = getData(); + ConnectionHolder connectionHolder = data.availableConnections.poll(); + while (connectionHolder != null && !connectionHolder.clientConnection.isOpen()) { + connectionHolder = data.availableConnections.poll(); + } + if (connectionHolder != null) { + if (exclusive) { + data.connections--; + } + connectionReady(connectionHolder, callback, exchange, exclusive); + } else if (exclusive || data.connections < maxConnections) { + openConnection(exchange, callback, data, exclusive); + } else { + // Reject the request directly if we reached the max request queue size + if (data.awaitingConnections.size() >= maxRequestQueueSize) { + callback.queuedRequestFailed(exchange); + return; + } + CallbackHolder holder; + if (timeout > 0) { + long time = System.currentTimeMillis(); + holder = new CallbackHolder(proxyTarget, callback, exchange, time + timeUnit.toMillis(timeout)); + holder.setTimeoutKey(exchange.getIoThread().executeAfter(holder, timeout, timeUnit)); + } else { + holder = new CallbackHolder(proxyTarget, callback, exchange, -1); + } + data.awaitingConnections.add(holder); + } + } + + private final class HostThreadData { + + int connections = 0; + XnioIoThread.Key timeoutKey; + long nextTimeout; + + final Deque availableConnections = new ArrayDeque<>(); + final Deque awaitingConnections = new ArrayDeque<>(); + final Runnable timeoutTask = new Runnable() { + @Override + public void run() { + final long currentTime = System.currentTimeMillis(); + timeoutConnections(currentTime, HostThreadData.this); + } + }; + + } + + private static final class ConnectionHolder { + + private long timeout; + private final ClientConnection clientConnection; + + private ConnectionHolder(ClientConnection clientConnection) { + this.clientConnection = clientConnection; + } + + } + + + private static final class CallbackHolder implements Runnable { + final ProxyClient.ProxyTarget proxyTarget; + final ProxyCallback callback; + final HttpServerExchange exchange; + final long expireTime; + XnioExecutor.Key timeoutKey; + boolean cancelled = false; + + private CallbackHolder(ProxyClient.ProxyTarget proxyTarget, ProxyCallback callback, HttpServerExchange exchange, long expireTime) { + this.proxyTarget = proxyTarget; + this.callback = callback; + this.exchange = exchange; + this.expireTime = expireTime; + } + + private ProxyCallback getCallback() { + return callback; + } + + private HttpServerExchange getExchange() { + return exchange; + } + + private long getExpireTime() { + return expireTime; + } + + private XnioExecutor.Key getTimeoutKey() { + return timeoutKey; + } + + private boolean isCancelled() { + return cancelled; + } + + private void setTimeoutKey(XnioExecutor.Key timeoutKey) { + this.timeoutKey = timeoutKey; + } + + @Override + public void run() { + cancelled = true; + callback.failed(exchange); + } + + public ProxyClient.ProxyTarget getProxyTarget() { + return proxyTarget; + } + } + + public enum AvailabilityType { + /** + * The host is read to accept requests + */ + AVAILABLE, + /** + * The host is stopped. No request should be forwarded that are not tied + * to this node via sticky sessions + */ + DRAIN, + /** + * All connections are in use, connections will be queued + */ + FULL, + /** + * All connections are in use and the queue is full. Requests will be rejected. + */ + FULL_QUEUE, + /** + * The host is probably down, only try as a last resort + */ + PROBLEM, + /** + * The host is closed. connections will always fail + */ + CLOSED; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnectionPoolConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnectionPoolConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyConnectionPoolConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +/** + * @author Emanuel Muckenhuber + */ +public interface ProxyConnectionPoolConfig { + + /** + * Get the maximum number of connections per thread. + * + * @return + */ + int getMaxConnections(); + + /** + * Get the maximum number of cached (idle) connections per thread. + * + * @return + */ + int getMaxCachedConnections(); + + /** + * Get number of cached connections above which are closed after the time to live. + * + * @return + */ + int getSMaxConnections(); + + /** + * Get the time to live for idle connections. + * + * @return + */ + long getTtl(); + + /** + * Get the maximum number of requests which can be queued if there are no connections available. + * + * @return + */ + int getMaxQueueSize(); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/ProxyHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,756 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.CertificateEncodingException; +import javax.security.cert.X509Certificate; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.channels.Channel; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowLogger; +import io.undertow.attribute.ExchangeAttribute; +import io.undertow.attribute.ExchangeAttributes; +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.ClientResponse; +import io.undertow.client.ContinueNotification; +import io.undertow.client.ProxiedRequestAttachments; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.RenegotiationRequiredException; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.server.protocol.http.HttpAttachments; +import io.undertow.server.protocol.http.HttpContinue; +import io.undertow.util.Attachable; +import io.undertow.util.AttachmentKey; +import io.undertow.util.Certificates; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.SameThreadExecutor; +import org.jboss.logging.Logger; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.StreamConnection; +import org.xnio.XnioExecutor; +import org.xnio.channels.StreamSinkChannel; + +/** + * An HTTP handler which proxies content to a remote server. + *

+ * This handler acts like a filter. The {@link ProxyClient} has a chance to decide if it + * knows how to proxy the request. If it does then it will provide a connection that can + * used to connect to the remote server, otherwise the next handler will be invoked and the + * request will proceed as normal. + * + * @author David M. Lloyd + */ +public final class ProxyHandler implements HttpHandler { + + private static final Logger log = Logger.getLogger(ProxyHandler.class); + + public static final String UTF_8 = "UTF-8"; + private final ProxyClient proxyClient; + private final int maxRequestTime; + + private static final AttachmentKey CONNECTION = AttachmentKey.create(ProxyConnection.class); + private static final AttachmentKey EXCHANGE = AttachmentKey.create(HttpServerExchange.class); + private static final AttachmentKey TIMEOUT_KEY = AttachmentKey.create(XnioExecutor.Key.class); + + /** + * Map of additional headers to add to the request. + */ + private final Map requestHeaders = new CopyOnWriteMap<>(); + + private final HttpHandler next; + + private final boolean rewriteHostHeader; + private final boolean reuseXForwarded; + + public ProxyHandler(ProxyClient proxyClient, int maxRequestTime, HttpHandler next) { + this(proxyClient, maxRequestTime, next, false, false); + } + + /** + * + * @param proxyClient the client to use to make the proxy call + * @param maxRequestTime the maximum amount of time to allow the request to be processed + * @param next the next handler in line + * @param rewriteHostHeader should the HOST header be rewritten to use the target host of the call. + * @param reuseXForwarded should any existing X-Forwarded-For header be used or should it be overwritten. + */ + public ProxyHandler(ProxyClient proxyClient, int maxRequestTime, HttpHandler next, boolean rewriteHostHeader, boolean reuseXForwarded) { + this.proxyClient = proxyClient; + this.maxRequestTime = maxRequestTime; + this.next = next; + this.rewriteHostHeader = rewriteHostHeader; + this.reuseXForwarded = reuseXForwarded; + } + + + public ProxyHandler(ProxyClient proxyClient, HttpHandler next) { + this(proxyClient, -1, next); + } + + public void handleRequest(final HttpServerExchange exchange) throws Exception { + final ProxyClient.ProxyTarget target = proxyClient.findTarget(exchange); + if (target == null) { + log.debugf("No proxy target for request to %s", exchange.getRequestURL()); + next.handleRequest(exchange); + return; + } + final long timeout = maxRequestTime > 0 ? System.currentTimeMillis() + maxRequestTime : 0; + final ProxyClientHandler clientHandler = new ProxyClientHandler(exchange, target, timeout, 1); + if (timeout > 0) { + final XnioExecutor.Key key = exchange.getIoThread().executeAfter(new Runnable() { + @Override + public void run() { + clientHandler.cancel(exchange); + } + }, maxRequestTime, TimeUnit.MILLISECONDS); + exchange.putAttachment(TIMEOUT_KEY, key); + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + key.remove(); + nextListener.proceed(); + } + }); + } + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : exchange.getIoThread(), clientHandler); + } + + /** + * Adds a request header to the outgoing request. If the header resolves to null or an empty string + * it will not be added, however any existing header with the same name will be removed. + * + * @param header The header name + * @param attribute The header value attribute. + * @return this + */ + public ProxyHandler addRequestHeader(final HttpString header, final ExchangeAttribute attribute) { + requestHeaders.put(header, attribute); + return this; + } + + /** + * Adds a request header to the outgoing request. If the header resolves to null or an empty string + * it will not be added, however any existing header with the same name will be removed. + * + * @param header The header name + * @param value The header value attribute. + * @return this + */ + public ProxyHandler addRequestHeader(final HttpString header, final String value) { + requestHeaders.put(header, ExchangeAttributes.constant(value)); + return this; + } + + /** + * Adds a request header to the outgoing request. If the header resolves to null or an empty string + * it will not be added, however any existing header with the same name will be removed. + *

+ * The attribute value will be parsed, and the resulting exchange attribute will be used to create the actual header + * value. + * + * @param header The header name + * @param attribute The header value attribute. + * @return this + */ + public ProxyHandler addRequestHeader(final HttpString header, final String attribute, final ClassLoader classLoader) { + requestHeaders.put(header, ExchangeAttributes.parser(classLoader).parse(attribute)); + return this; + } + + /** + * Removes a request header + * + * @param header the header + * @return this + */ + public ProxyHandler removeRequestHeader(final HttpString header) { + requestHeaders.remove(header); + return this; + } + + + static void copyHeaders(final HeaderMap to, final HeaderMap from) { + long f = from.fastIterateNonEmpty(); + HeaderValues values; + while (f != -1L) { + values = from.fiCurrent(f); + to.putAll(values.getHeaderName(), values); + f = from.fiNextNonEmpty(f); + } + } + + public ProxyClient getProxyClient() { + return proxyClient; + } + + private final class ProxyClientHandler implements ProxyCallback, Runnable { + + private int tries; + + private final long timeout; + private final int maxAttempts; + private final HttpServerExchange exchange; + private ProxyClient.ProxyTarget target; + + ProxyClientHandler(HttpServerExchange exchange, ProxyClient.ProxyTarget target, long timeout, int maxAttempts) { + this.exchange = exchange; + this.timeout = timeout; + this.maxAttempts = maxAttempts; + this.target = target; + } + + @Override + public void run() { + proxyClient.getConnection(target, exchange, this, -1, TimeUnit.MILLISECONDS); + } + + @Override + public void completed(final HttpServerExchange exchange, final ProxyConnection connection) { + exchange.putAttachment(CONNECTION, connection); + exchange.dispatch(SameThreadExecutor.INSTANCE, new ProxyAction(connection, exchange, requestHeaders, rewriteHostHeader, reuseXForwarded)); + } + + @Override + public void failed(final HttpServerExchange exchange) { + final long time = System.currentTimeMillis(); + if (timeout > 0 && timeout > time) { + cancel(exchange); + } else if (tries++ < maxAttempts) { + target = proxyClient.findTarget(exchange); + if (target != null) { + final long remaining = timeout > 0 ? timeout - time : -1; + proxyClient.getConnection(target, exchange, this, remaining, TimeUnit.MILLISECONDS); + } else { + couldNotResolveBackend(exchange); // The context was registered when we started, so return 503 + } + } else { + couldNotResolveBackend(exchange); + } + } + + @Override + public void queuedRequestFailed(HttpServerExchange exchange) { + failed(exchange); + } + + @Override + public void couldNotResolveBackend(HttpServerExchange exchange) { + if (exchange.isResponseStarted()) { + IoUtils.safeClose(exchange.getConnection()); + } else { + exchange.setResponseCode(503); + exchange.endExchange(); + } + } + + void cancel(final HttpServerExchange exchange) { + final ProxyConnection connectionAttachment = exchange.getAttachment(CONNECTION); + if (connectionAttachment != null) { + ClientConnection clientConnection = connectionAttachment.getConnection(); + UndertowLogger.REQUEST_LOGGER.timingOutRequest(clientConnection.getPeerAddress() + "" + exchange.getRequestURI()); + IoUtils.safeClose(clientConnection); + } else { + UndertowLogger.REQUEST_LOGGER.timingOutRequest(exchange.getRequestURI()); + } + if (exchange.isResponseStarted()) { + IoUtils.safeClose(exchange.getConnection()); + } else { + exchange.setResponseCode(503); + exchange.endExchange(); + } + } + + } + + private static class ProxyAction implements Runnable { + private final ProxyConnection clientConnection; + private final HttpServerExchange exchange; + private final Map requestHeaders; + private final boolean rewriteHostHeader; + private final boolean reuseXForwarded; + + public ProxyAction(final ProxyConnection clientConnection, final HttpServerExchange exchange, Map requestHeaders, + boolean rewriteHostHeader, boolean reuseXForwarded) { + this.clientConnection = clientConnection; + this.exchange = exchange; + this.requestHeaders = requestHeaders; + this.rewriteHostHeader = rewriteHostHeader; + this.reuseXForwarded = reuseXForwarded; + } + + @Override + public void run() { + final ClientRequest request = new ClientRequest(); + + StringBuilder requestURI = new StringBuilder(); + try { + if (exchange.getRelativePath().isEmpty()) { + requestURI.append(encodeUrlPart(clientConnection.getTargetPath())); + } else { + if (clientConnection.getTargetPath().endsWith("/")) { + requestURI.append(clientConnection.getTargetPath().substring(0, clientConnection.getTargetPath().length() - 1)); + requestURI.append(encodeUrlPart(exchange.getRelativePath())); + } else { + requestURI = requestURI.append(clientConnection.getTargetPath()); + requestURI.append(encodeUrlPart(exchange.getRelativePath())); + } + } + boolean first = true; + if (!exchange.getPathParameters().isEmpty()) { + requestURI.append(';'); + for (Map.Entry> entry : exchange.getPathParameters().entrySet()) { + if (first) { + first = false; + } else { + requestURI.append('&'); + } + for (String val : entry.getValue()) { + requestURI.append(URLEncoder.encode(entry.getKey(), UTF_8)); + requestURI.append('='); + requestURI.append(URLEncoder.encode(val, UTF_8)); + } + } + } + + String qs = exchange.getQueryString(); + if (qs != null && !qs.isEmpty()) { + requestURI.append('?'); + requestURI.append(qs); + } + } catch (UnsupportedEncodingException e) { + //impossible + exchange.setResponseCode(500); + exchange.endExchange(); + return; + } + request.setPath(requestURI.toString()) + .setMethod(exchange.getRequestMethod()); + final HeaderMap inboundRequestHeaders = exchange.getRequestHeaders(); + final HeaderMap outboundRequestHeaders = request.getRequestHeaders(); + copyHeaders(outboundRequestHeaders, inboundRequestHeaders); + + if (!exchange.isPersistent()) { + //just because the client side is non-persistent + //we don't want to close the connection to the backend + outboundRequestHeaders.put(Headers.CONNECTION, "keep-alive"); + } + + for (Map.Entry entry : requestHeaders.entrySet()) { + String headerValue = entry.getValue().readAttribute(exchange); + if (headerValue == null || headerValue.isEmpty()) { + outboundRequestHeaders.remove(entry.getKey()); + } else { + outboundRequestHeaders.put(entry.getKey(), headerValue.replace('\n', ' ')); + } + } + + final SocketAddress address = exchange.getConnection().getPeerAddress(); + final String remoteHost = (address != null && address instanceof InetSocketAddress) ? ((InetSocketAddress) address).getHostString() : "localhost"; + request.putAttachment(ProxiedRequestAttachments.REMOTE_HOST, remoteHost); + + if (reuseXForwarded && request.getRequestHeaders().contains(Headers.X_FORWARDED_FOR)) { + // We have an existing header so we shall simply append the host to the existing list + final String current = request.getRequestHeaders().getFirst(Headers.X_FORWARDED_FOR); + if (current == null || current.isEmpty()) { + // It was empty so just add it + request.getRequestHeaders().put(Headers.X_FORWARDED_FOR, remoteHost); + } + else { + // Add the new entry and reset the existing header + request.getRequestHeaders().put(Headers.X_FORWARDED_FOR, current + "," + remoteHost); + } + } + else { + // No existing header or not allowed to reuse the header so set it here + request.getRequestHeaders().put(Headers.X_FORWARDED_FOR, remoteHost); + } + + // Set the protocol header and attachment + final String proto = exchange.getRequestScheme().equals("https") ? "https" : "http"; + request.getRequestHeaders().put(Headers.X_FORWARDED_PROTO, proto); + request.putAttachment(ProxiedRequestAttachments.IS_SSL, proto.equals("https")); + + // Set the server name + final String hostName = exchange.getHostName(); + request.getRequestHeaders().put(Headers.X_FORWARDED_HOST, hostName); + request.putAttachment(ProxiedRequestAttachments.SERVER_NAME, hostName); + + // Set the port + int port = exchange.getConnection().getLocalAddress(InetSocketAddress.class).getPort(); + request.getRequestHeaders().put(Headers.X_FORWARDED_PORT, port); + request.putAttachment(ProxiedRequestAttachments.SERVER_PORT, port); + + SSLSessionInfo sslSessionInfo = exchange.getConnection().getSslSessionInfo(); + if (sslSessionInfo != null) { + X509Certificate[] peerCertificates; + try { + peerCertificates = sslSessionInfo.getPeerCertificateChain(); + if (peerCertificates.length > 0) { + request.putAttachment(ProxiedRequestAttachments.SSL_CERT, Certificates.toPem(peerCertificates[0])); + } + } catch (SSLPeerUnverifiedException e) { + //ignore + } catch (CertificateEncodingException e) { + //ignore + } catch (RenegotiationRequiredException e) { + //ignore + } + request.putAttachment(ProxiedRequestAttachments.SSL_CYPHER, sslSessionInfo.getCipherSuite()); + request.putAttachment(ProxiedRequestAttachments.SSL_SESSION_ID, sslSessionInfo.getSessionId()); + } + + if(rewriteHostHeader) { + InetSocketAddress targetAddress = clientConnection.getConnection().getPeerAddress(InetSocketAddress.class); + request.getRequestHeaders().put(Headers.HOST, targetAddress.getHostString() + ":" + targetAddress.getPort()); + request.getRequestHeaders().put(Headers.X_FORWARDED_HOST, exchange.getRequestHeaders().getFirst(Headers.HOST)); + } + + clientConnection.getConnection().sendRequest(request, new ClientCallback() { + @Override + public void completed(final ClientExchange result) { + + result.putAttachment(EXCHANGE, exchange); + + boolean requiresContinueResponse = HttpContinue.requiresContinueResponse(exchange); + if (requiresContinueResponse) { + result.setContinueHandler(new ContinueNotification() { + @Override + public void handleContinue(final ClientExchange clientExchange) { + HttpContinue.sendContinueResponse(exchange, new IoCallback() { + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + //don't care + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + IoUtils.safeClose(clientConnection.getConnection()); + } + }); + } + }); + + } + + result.setResponseListener(new ResponseCallback(exchange)); + final IoExceptionHandler handler = new IoExceptionHandler(exchange, clientConnection.getConnection()); + if(requiresContinueResponse) { + try { + if(!result.getRequestChannel().flush()) { + result.getRequestChannel().getWriteSetter().set(ChannelListeners.flushingChannelListener(new ChannelListener() { + @Override + public void handleEvent(StreamSinkChannel channel) { + ChannelListeners.initiateTransfer(Long.MAX_VALUE, exchange.getRequestChannel(), result.getRequestChannel(), ChannelListeners.closingChannelListener(), new HTTPTrailerChannelListener(exchange, result), handler, handler, exchange.getConnection().getBufferPool()); + + } + }, handler)); + result.getRequestChannel().resumeWrites(); + return; + } + } catch (IOException e) { + handler.handleException(result.getRequestChannel(), e); + } + } + ChannelListeners.initiateTransfer(Long.MAX_VALUE, exchange.getRequestChannel(), result.getRequestChannel(), ChannelListeners.closingChannelListener(), new HTTPTrailerChannelListener(exchange, result), handler, handler, exchange.getConnection().getBufferPool()); + + } + + @Override + public void failed(IOException e) { + UndertowLogger.PROXY_REQUEST_LOGGER.proxyRequestFailed(exchange.getRequestURI(), e); + if (!exchange.isResponseStarted()) { + exchange.setResponseCode(503); + exchange.endExchange(); + } else { + IoUtils.safeClose(exchange.getConnection()); + } + } + }); + + + } + } + + private static final class ResponseCallback implements ClientCallback { + + private final HttpServerExchange exchange; + + private ResponseCallback(HttpServerExchange exchange) { + this.exchange = exchange; + } + + @Override + public void completed(final ClientExchange result) { + HttpServerExchange exchange = result.getAttachment(EXCHANGE); + final ClientResponse response = result.getResponse(); + final HeaderMap inboundResponseHeaders = response.getResponseHeaders(); + final HeaderMap outboundResponseHeaders = exchange.getResponseHeaders(); + exchange.setResponseCode(response.getResponseCode()); + copyHeaders(outboundResponseHeaders, inboundResponseHeaders); + + if (exchange.isUpgrade()) { + exchange.upgradeChannel(new HttpUpgradeListener() { + @Override + public void handleUpgrade(StreamConnection streamConnection, HttpServerExchange exchange) { + StreamConnection clientChannel = null; + try { + clientChannel = result.getConnection().performUpgrade(); + + ChannelListeners.initiateTransfer(Long.MAX_VALUE, clientChannel.getSourceChannel(), streamConnection.getSinkChannel(), ChannelListeners.closingChannelListener(), ChannelListeners.writeShutdownChannelListener(ChannelListeners.flushingChannelListener(ChannelListeners.closingChannelListener(), ChannelListeners.closingChannelExceptionHandler()), ChannelListeners.closingChannelExceptionHandler()), ChannelListeners.closingChannelExceptionHandler(), ChannelListeners.closingChannelExceptionHandler(), result.getConnection().getBufferPool()); + ChannelListeners.initiateTransfer(Long.MAX_VALUE, streamConnection.getSourceChannel(), clientChannel.getSinkChannel(), ChannelListeners.closingChannelListener(), ChannelListeners.writeShutdownChannelListener(ChannelListeners.flushingChannelListener(ChannelListeners.closingChannelListener(), ChannelListeners.closingChannelExceptionHandler()), ChannelListeners.closingChannelExceptionHandler()), ChannelListeners.closingChannelExceptionHandler(), ChannelListeners.closingChannelExceptionHandler(), result.getConnection().getBufferPool()); + + } catch (IOException e) { + IoUtils.safeClose(streamConnection, clientChannel); + } + } + }); + } + IoExceptionHandler handler = new IoExceptionHandler(exchange, result.getConnection()); + ChannelListeners.initiateTransfer(Long.MAX_VALUE, result.getResponseChannel(), exchange.getResponseChannel(), ChannelListeners.closingChannelListener(), new HTTPTrailerChannelListener(result, exchange), handler, handler, exchange.getConnection().getBufferPool()); + + } + + @Override + public void failed(IOException e) { + UndertowLogger.PROXY_REQUEST_LOGGER.proxyRequestFailed(exchange.getRequestURI(), e); + if (!exchange.isResponseStarted()) { + exchange.setResponseCode(500); + exchange.endExchange(); + } else { + IoUtils.safeClose(exchange.getConnection()); + } + } + } + + private static final class HTTPTrailerChannelListener implements ChannelListener { + + private final Attachable source; + private final Attachable target; + + private HTTPTrailerChannelListener(final Attachable source, final Attachable target) { + this.source = source; + this.target = target; + } + + @Override + public void handleEvent(final StreamSinkChannel channel) { + HeaderMap trailers = source.getAttachment(HttpAttachments.REQUEST_TRAILERS); + if (trailers != null) { + target.putAttachment(HttpAttachments.RESPONSE_TRAILERS, trailers); + } + try { + channel.shutdownWrites(); + if (!channel.flush()) { + channel.getWriteSetter().set(ChannelListeners.flushingChannelListener(new ChannelListener() { + @Override + public void handleEvent(StreamSinkChannel channel) { + channel.suspendWrites(); + channel.getWriteSetter().set(null); + } + }, ChannelListeners.closingChannelExceptionHandler())); + channel.resumeWrites(); + } else { + channel.getWriteSetter().set(null); + channel.shutdownWrites(); + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } + + } + } + + private static final class IoExceptionHandler implements ChannelExceptionHandler { + + private final HttpServerExchange exchange; + private final ClientConnection clientConnection; + + private IoExceptionHandler(HttpServerExchange exchange, ClientConnection clientConnection) { + this.exchange = exchange; + this.clientConnection = clientConnection; + } + + @Override + public void handleException(Channel channel, IOException exception) { + IoUtils.safeClose(channel); + if (exchange.isResponseStarted()) { + IoUtils.safeClose(clientConnection); + UndertowLogger.REQUEST_IO_LOGGER.debug("Exception reading from target server", exception); + if (!exchange.isResponseStarted()) { + exchange.setResponseCode(500); + exchange.endExchange(); + } else { + IoUtils.safeClose(exchange.getConnection()); + } + } else { + exchange.setResponseCode(500); + exchange.endExchange(); + } + } + } + + /** + * perform URL encoding + *

+ * TODO: this whole thing is kinda crappy. + * + * @return + */ + private static String encodeUrlPart(final String part) throws UnsupportedEncodingException { + //we need to go through and check part by part that a section does not need encoding + + int pos = 0; + for (int i = 0; i < part.length(); ++i) { + char c = part.charAt(i); + if (c == '/') { + if (pos != i) { + String original = part.substring(pos, i - 1); + String encoded = URLEncoder.encode(original, UTF_8); + if (!encoded.equals(original)) { + return realEncode(part, pos); + } + } + pos = i + 1; + } else if (c == ' ') { + return realEncode(part, pos); + } + } + if (pos != part.length()) { + String original = part.substring(pos); + String encoded = URLEncoder.encode(original, UTF_8); + if (!encoded.equals(original)) { + return realEncode(part, pos); + } + } + return part; + } + + private static String realEncode(String part, int startPos) throws UnsupportedEncodingException { + StringBuilder sb = new StringBuilder(); + sb.append(part.substring(0, startPos)); + int pos = startPos; + for (int i = startPos; i < part.length(); ++i) { + char c = part.charAt(i); + if (c == '/') { + if (pos != i) { + String original = part.substring(pos, i - 1); + String encoded = URLEncoder.encode(original, UTF_8); + sb.append(encoded); + sb.append('/'); + pos = i + 1; + } + } + } + + String original = part.substring(pos); + String encoded = URLEncoder.encode(original, UTF_8); + sb.append(encoded); + return sb.toString(); + } + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "reverse-proxy"; + } + + @Override + public Map> parameters() { + return Collections.>singletonMap("hosts", String[].class); + } + + @Override + public Set requiredParameters() { + return Collections.singleton("hosts"); + } + + @Override + public String defaultParameter() { + return "hosts"; + } + + @Override + public HandlerWrapper build(Map config) { + String[] hosts = (String[]) config.get("hosts"); + List uris = new ArrayList<>(); + for(String host : hosts) { + try { + uris.add(new URI(host)); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + return new Wrapper(uris); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final List uris; + + private Wrapper(List uris) { + this.uris = uris; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + + LoadBalancingProxyClient loadBalancingProxyClient = new LoadBalancingProxyClient(); + for(URI url : uris) { + loadBalancingProxyClient.addHost(url); + } + return new ProxyHandler(loadBalancingProxyClient, handler); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/SimpleProxyClientProvider.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/SimpleProxyClientProvider.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/SimpleProxyClientProvider.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,111 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy; + +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.UndertowClient; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.ServerConnection; +import io.undertow.util.AttachmentKey; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + +import java.io.IOException; +import java.net.URI; +import java.nio.channels.Channel; +import java.util.concurrent.TimeUnit; + +/** + * Simple proxy client provider. This provider simply proxies to another server, using a a one to one + * connection strategy. + * + * @author Stuart Douglas + */ +public class SimpleProxyClientProvider implements ProxyClient { + + private final URI uri; + private final AttachmentKey clientAttachmentKey = AttachmentKey.create(ClientConnection.class); + private final UndertowClient client; + + private static final ProxyTarget TARGET = new ProxyTarget() {}; + + public SimpleProxyClientProvider(URI uri) { + this.uri = uri; + client = UndertowClient.getInstance(); + } + + @Override + public ProxyTarget findTarget(HttpServerExchange exchange) { + return TARGET; + } + + @Override + public void getConnection(ProxyTarget target, HttpServerExchange exchange, ProxyCallback callback, long timeout, TimeUnit timeUnit) { + ClientConnection existing = exchange.getConnection().getAttachment(clientAttachmentKey); + if (existing != null) { + if (existing.isOpen()) { + //this connection already has a client, re-use it + callback.completed(exchange, new ProxyConnection(existing, uri.getPath() == null ? "/" : uri.getPath())); + return; + } else { + exchange.getConnection().removeAttachment(clientAttachmentKey); + } + } + client.connect(new ConnectNotifier(callback, exchange), uri, exchange.getIoThread(), exchange.getConnection().getBufferPool(), OptionMap.EMPTY); + } + + private final class ConnectNotifier implements ClientCallback { + private final ProxyCallback callback; + private final HttpServerExchange exchange; + + private ConnectNotifier(ProxyCallback callback, HttpServerExchange exchange) { + this.callback = callback; + this.exchange = exchange; + } + + @Override + public void completed(final ClientConnection connection) { + final ServerConnection serverConnection = exchange.getConnection(); + //we attach to the connection so it can be re-used + serverConnection.putAttachment(clientAttachmentKey, connection); + serverConnection.addCloseListener(new ServerConnection.CloseListener() { + @Override + public void closed(ServerConnection serverConnection) { + IoUtils.safeClose(connection); + } + }); + connection.getCloseSetter().set(new ChannelListener() { + @Override + public void handleEvent(Channel channel) { + serverConnection.removeAttachment(clientAttachmentKey); + } + }); + callback.completed(exchange, new ProxyConnection(connection, uri.getPath() == null ? "/" : uri.getPath())); + } + + @Override + public void failed(IOException e) { + callback.failed(exchange); + } + } + + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Balancer.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Balancer.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Balancer.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,270 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.util.concurrent.atomic.AtomicInteger; + +/** + * The mod_cluster balancer config. + * + * @author Stuart Douglas + * @author Emanuel Muckenhuber + */ +public class Balancer { + + /** + * Name of the balancer. max size: 40, Default: "mycluster" + */ + private final String name; + + /** + * {@code true}: use JVMRoute to stick a request to a node, {@code false}: ignore JVMRoute. Default: {@code true} + */ + private final boolean stickySession; + + /** + * Name of the cookie containing the session-id. Max size: 30 Default: "JSESSIONID" + */ + private final String stickySessionCookie; + + /** + * Name of the parameter containing the session-id. Max size: 30. Default: "jsessionid" + */ + private final String stickySessionPath; + + /** + * {@code true}: remove the session-id (cookie or parameter) when the request can't be + * routed to the right node. {@code false}: send it anyway. Default: {@code false} + */ + private final boolean stickySessionRemove; + + /** + * {@code true}: Return an error if the request can't be routed according to + * JVMRoute, {@code false}: Route it to another node. Default: {@code true} + */ + private final boolean stickySessionForce; + + /** + * value in seconds: time to wait for an available worker. Default: "0" no wait. + */ + private final int waitWorker; + + /** + * value: number of attempts to send the request to the backend server. Default: "1" + */ + private final int maxattempts; + + private final int id; + private static final AtomicInteger idGen = new AtomicInteger(); + + Balancer(BalancerBuilder b) { + this.id = idGen.incrementAndGet(); + this.name = b.getName(); + this.stickySession = b.isStickySession(); + this.stickySessionCookie = b.getStickySessionCookie(); + this.stickySessionPath = b.getStickySessionPath(); + this.stickySessionRemove = b.isStickySessionRemove(); + this.stickySessionForce = b.isStickySessionForce(); + this.waitWorker = b.getWaitWorker(); + this.maxattempts = b.getMaxattempts(); + } + + public int getId() { + return id; + } + + /** + * Getter for name + * + * @return the name + */ + public String getName() { + return this.name; + } + + /** + * Getter for stickySession + * + * @return the stickySession + */ + public boolean isStickySession() { + return this.stickySession; + } + + /** + * Getter for stickySessionCookie + * + * @return the stickySessionCookie + */ + public String getStickySessionCookie() { + return this.stickySessionCookie; + } + + /** + * Getter for stickySessionPath + * + * @return the stickySessionPath + */ + public String getStickySessionPath() { + return this.stickySessionPath; + } + + /** + * Getter for stickySessionRemove + * + * @return the stickySessionRemove + */ + public boolean isStickySessionRemove() { + return this.stickySessionRemove; + } + + /** + * Getter for stickySessionForce + * + * @return the stickySessionForce + */ + public boolean isStickySessionForce() { + return this.stickySessionForce; + } + + /** + * Getter for waitWorker + * + * @return the waitWorker + */ + public int getWaitWorker() { + return this.waitWorker; + } + + /** + * Getter for maxattempts + * + * @return the maxattempts + */ + public int getMaxattempts() { + return this.maxattempts; + } + + @Override + public String toString() { + return new StringBuilder("balancer: Name: ") + .append(this.name).append(", Sticky: ").append(this.stickySession ? 1 : 0) + .append(" [").append(this.stickySessionCookie).append("]/[") + .append(this.stickySessionPath).append("], remove: ") + .append(this.stickySessionRemove ? 1 : 0).append(", force: ") + .append(this.stickySessionForce ? 1 : 0).append(", Timeout: ") + .append(this.waitWorker).append(", Maxtry: ").append(this.maxattempts).toString(); + } + + static final BalancerBuilder builder() { + return new BalancerBuilder(); + } + + public static final class BalancerBuilder { + + private String name = "mycluster"; + private boolean stickySession = true; + private String stickySessionCookie = "JSESSIONID"; + private String stickySessionPath = "jsessionid"; + private boolean stickySessionRemove = false; + private boolean stickySessionForce = true; + private int waitWorker = 0; + private int maxattempts = 1; + + public String getName() { + return name; + } + + public BalancerBuilder setName(String name) { + this.name = name; + return this; + } + + public boolean isStickySession() { + return stickySession; + } + + public BalancerBuilder setStickySession(boolean stickySession) { + this.stickySession = stickySession; + return this; + } + + public String getStickySessionCookie() { + return stickySessionCookie; + } + + public BalancerBuilder setStickySessionCookie(String stickySessionCookie) { + if (stickySessionCookie != null && stickySessionCookie.length() > 30) { + this.stickySessionCookie = stickySessionCookie.substring(0, 30); + } else { + this.stickySessionCookie = stickySessionCookie; + } + return this; + } + + public String getStickySessionPath() { + return stickySessionPath; + } + + public BalancerBuilder setStickySessionPath(String stickySessionPath) { + this.stickySessionPath = stickySessionPath; + return this; + } + + public boolean isStickySessionRemove() { + return stickySessionRemove; + } + + public BalancerBuilder setStickySessionRemove(boolean stickySessionRemove) { + this.stickySessionRemove = stickySessionRemove; + return this; + } + + public boolean isStickySessionForce() { + return stickySessionForce; + } + + public BalancerBuilder setStickySessionForce(boolean stickySessionForce) { + this.stickySessionForce = stickySessionForce; + return this; + } + + public int getWaitWorker() { + return waitWorker; + } + + public BalancerBuilder setWaitWorker(int waitWorker) { + this.waitWorker = waitWorker; + return this; + } + + public int getMaxattempts() { + return maxattempts; + } + + public BalancerBuilder setMaxattempts(int maxattempts) { + this.maxattempts = maxattempts; + return this; + } + + public Balancer build() { + return new Balancer(this); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Context.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Context.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Context.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,216 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; + +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.proxy.ProxyCallback; +import io.undertow.server.handlers.proxy.ProxyConnection; + +/** + * + * @author Emanuel Muckenhuber + */ +class Context { + + private static final AtomicInteger idGen = new AtomicInteger(); + + static enum Status { + + ENABLED, + DISABLED, + STOPPED, + ; + + } + + private final int id; + private final Node node; + private final String path; + private final Node.VHostMapping vhost; + + private static final int STOPPED = (1 << 31); + private static final int DISABLED = (1 << 30); + private static final int REQUEST_MASK = ((1 << 30) - 1); + + private volatile int state = STOPPED; + private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater.newUpdater(Context.class, "state"); + + Context(final String path, final Node.VHostMapping vHost, final Node node) { + id = idGen.incrementAndGet(); + this.path = path; + this.node = node; + this.vhost = vHost; + } + + public int getId() { + return id; + } + + public String getJVMRoute() { + return node.getJvmRoute(); + } + + public String getPath() { + return path; + } + + public List getVirtualHosts() { + return vhost.getAliases(); + } + + public int getActiveRequests() { + return state & REQUEST_MASK; + } + + public Status getStatus() { + final int state = this.state; + if ((state & STOPPED) == STOPPED) { + return Status.STOPPED; + } else if ((state & DISABLED) == DISABLED) { + return Status.DISABLED; + } + return Status.ENABLED; + } + + public boolean isEnabled() { + return allAreClear(state, DISABLED | STOPPED); + } + + public boolean isStopped() { + return allAreSet(state, STOPPED); + } + + public boolean isDisabled() { + return allAreSet(state, DISABLED); + } + + Node getNode() { + return node; + } + + Node.VHostMapping getVhost() { + return vhost; + } + + boolean checkAvailable(boolean existingSession) { + if (node.checkAvailable(existingSession)) { + return existingSession ? !isStopped() : isEnabled(); + } + return false; + } + + void enable() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState & ~(STOPPED | DISABLED); + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return; + } + } + } + + void disable() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState | DISABLED; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return; + } + } + } + + void stop() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState | STOPPED; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return; + } + } + } + + /** + * Handle a proxy request for this context. + * + * @param target the proxy target + * @param exchange the http server exchange + * @param callback the proxy callback + * @param timeout the timeout + * @param timeUnit the time unit + * @param exclusive whether this connection is exclusive + */ + void handleRequest(final ModClusterProxyTarget target, final HttpServerExchange exchange, final ProxyCallback callback, long timeout, TimeUnit timeUnit, boolean exclusive) { + if (addRequest()) { + exchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + requestDone(); + nextListener.proceed(); + } + }); + node.getConnectionPool().connect(target, exchange, callback, timeout, timeUnit, exclusive); + } else { + callback.failed(exchange); + } + } + + boolean addRequest() { + int oldState, newState; + for (;;) { + oldState = this.state; + if ((oldState & STOPPED) != 0) { + return false; + } + newState = oldState + 1; + if ((newState & REQUEST_MASK) == REQUEST_MASK) { + return false; + } + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return true; + } + } + } + + void requestDone() { + int oldState, newState; + for (;;) { + oldState = this.state; + if ((oldState & REQUEST_MASK) == 0) { + return; + } + newState = oldState - 1; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return; + } + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPAdvertiseTask.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPAdvertiseTask.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPAdvertiseTask.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,181 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.io.IOException; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import org.xnio.ChannelListener; +import org.xnio.OptionMap; +import org.xnio.XnioWorker; +import org.xnio.channels.MulticastMessageChannel; + +/** + * @author Emanuel Muckenhuber + */ +class MCMPAdvertiseTask implements Runnable { + + public static final String RFC_822_FMT = "EEE, d MMM yyyy HH:mm:ss Z"; + private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat(RFC_822_FMT); + private static final boolean linuxLike; + + static { + String value = System.getProperty("os.name"); + linuxLike = (value != null) && (value.toLowerCase().startsWith("linux") || value.toLowerCase().startsWith("mac") || value.toLowerCase().startsWith("hp")); + } + + private volatile int seq = 0; + + private final String protocol; + private final String host; + private final int port; + private final String path; + private final byte[] ssalt; + private final MessageDigest md; + private final InetSocketAddress address; + private final ModClusterContainer container; + private final MulticastMessageChannel channel; + + static void advertise(final ModClusterContainer container, final MCMPConfig.AdvertiseConfig config, final XnioWorker worker) throws IOException { + final InetSocketAddress bindAddress; + final InetAddress group = InetAddress.getByName(config.getAdvertiseGroup()); + if (group == null || linuxLike) { + bindAddress = new InetSocketAddress(config.getAdvertisePort()); + } else { + bindAddress = new InetSocketAddress(group, config.getAdvertisePort()); + } + final MulticastMessageChannel channel = worker.createUdpServer(bindAddress, new ChannelListener() { + @Override + public void handleEvent(MulticastMessageChannel channel) { + channel.resumeWrites(); + } + }, OptionMap.EMPTY); + final MCMPAdvertiseTask task = new MCMPAdvertiseTask(container, config, channel); + channel.getIoThread().executeAtInterval(task, config.getAdvertiseFrequency(), TimeUnit.MILLISECONDS); + } + + MCMPAdvertiseTask(final ModClusterContainer container, final MCMPConfig.AdvertiseConfig config, final MulticastMessageChannel channel) throws IOException { + + this.container = container; + this.protocol = config.getProtocol(); + this.host = config.getManagementHost(); + this.port = config.getManagementPort(); + this.path = config.getPath(); + this.channel = channel; + + final InetAddress group = InetAddress.getByName(config.getAdvertiseGroup()); + address = new InetSocketAddress(group, config.getAdvertisePort()); + try { + md = MessageDigest.getInstance("MD5"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + final String securityKey = config.getSecurityKey(); + if (securityKey == null) { + // Security key is not configured, so the result hash was zero bytes + ssalt = new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; + } else { + md.reset(); + digestString(md, securityKey); + ssalt = md.digest(); + } + } + + private static final String CRLF = "\r\n"; + + /* + * the messages to send are something like: + * + * HTTP/1.0 200 OK + * Date: Thu, 13 Sep 2012 09:24:02 GMT + * Sequence: 5 + * Digest: ae8e7feb7cd85be346134657de3b0661 + * Server: b58743ba-fd84-11e1-bd12-ad866be2b4cc + * X-Manager-Address: 127.0.0.1:6666 + * X-Manager-Url: /b58743ba-fd84-11e1-bd12-ad866be2b4cc + * X-Manager-Protocol: http + * X-Manager-Host: 10.33.144.3 + * + */ + @Override + public void run() { + try { + /* + * apr_uuid_get(&magd->suuid); + * magd->srvid[0] = '/'; + * apr_uuid_format(&magd->srvid[1], &magd->suuid); + * In fact we use the srvid on the 2 second byte [1] + */ + final byte[] ssalt = this.ssalt; + final String server = container.getServerID(); + final String date = DATE_FORMAT.format(new Date(System.currentTimeMillis())); + final String seq = "" + this.seq++; + + final byte[] digest; + synchronized (md) { + md.reset(); + md.update(ssalt); + digestString(md, date); + digestString(md, seq); + digestString(md, server); + digest = md.digest(); + } + final String digestString = bytesToHexString(digest); + + final StringBuilder builder = new StringBuilder(); + builder.append("HTTP/1.0 200 OK").append(CRLF) + .append("Date: ").append(date).append(CRLF) + .append("Sequence: ").append(seq).append(CRLF) + .append("Digest: ").append(digestString).append(CRLF) + .append("Server: ").append(server).append(CRLF) + .append("X-Manager-Address: ").append(host).append(":").append(port).append(CRLF) + .append("X-Manager-Url: ").append(path).append(CRLF) + .append("X-Manager-Protocol: ").append(protocol).append(CRLF) + .append("X-Manager-Host: ").append(host).append(CRLF); + + final String payload = builder.toString(); + final ByteBuffer byteBuffer = ByteBuffer.wrap(payload.getBytes()); + channel.sendTo(address, byteBuffer); + } catch (Exception Ex) { + Ex.printStackTrace(); + } + } + + private void digestString(MessageDigest md, String securityKey) { + byte[] buf = securityKey.getBytes(); + md.update(buf); + } + + private static final char[] TABLE = "0123456789abcdef".toCharArray(); + static String bytesToHexString(final byte[] bytes) { + final StringBuilder builder = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + builder.append(TABLE[b >> 4 & 0x0f]).append(TABLE[b & 0x0f]); + } + return builder.toString(); + } + +} \ No newline at end of file Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,309 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.net.InetAddress; +import java.net.UnknownHostException; + +import io.undertow.server.HttpHandler; + +/** + * @author Emanuel Muckenhuber + */ +public class MCMPConfig { + + public static Builder builder() { + return new Builder(); + } + + public static WebBuilder webBuilder() { + return new WebBuilder(); + } + + private final String managementHost; + private final String managementHostIp; + private final int managementPort; + private final AdvertiseConfig advertiseConfig; + + public MCMPConfig(Builder builder) { + this.managementHost = builder.managementHost; + this.managementPort = builder.managementPort; + if (builder.advertiseBuilder != null) { + this.advertiseConfig = new AdvertiseConfig(builder.advertiseBuilder, this); + } else { + this.advertiseConfig = null; + } + String mhip = managementHost; + try { + mhip = InetAddress.getByName(managementHost).getHostAddress(); + } catch (UnknownHostException e) { + + } + this.managementHostIp = mhip; + } + + public String getManagementHost() { + return managementHost; + } + + public int getManagementPort() { + return managementPort; + } + + public String getManagementHostIp() { + return managementHostIp; + } + + AdvertiseConfig getAdvertiseConfig() { + return advertiseConfig; + } + + public HttpHandler create(final ModCluster modCluster, final HttpHandler next) { + return new MCMPHandler(this, modCluster, next); + } + + static class MCMPWebManagerConfig extends MCMPConfig { + + private final boolean allowCmd; + private final boolean checkNonce; + private final boolean reduceDisplay; + private final boolean displaySessionids; + + MCMPWebManagerConfig(WebBuilder builder) { + super(builder); + this.allowCmd = builder.allowCmd; + this.checkNonce = builder.checkNonce; + this.reduceDisplay = builder.reduceDisplay; + this.displaySessionids = builder.displaySessionids; + } + + public boolean isAllowCmd() { + return allowCmd; + } + + public boolean isCheckNonce() { + return checkNonce; + } + + public boolean isReduceDisplay() { + return reduceDisplay; + } + + public boolean isDisplaySessionids() { + return displaySessionids; + } + + @Override + public HttpHandler create(ModCluster modCluster, HttpHandler next) { + return new MCMPWebManager(this, modCluster, next); + } + } + + static class AdvertiseConfig { + + private final String advertiseGroup; + private final String advertiseAddress; + private final int advertisePort; + + private final String securityKey; + private final String protocol; + private final String path; + + private final int advertiseFrequency; + + private final String managementHost; + private final int managementPort; + + AdvertiseConfig(AdvertiseBuilder builder, MCMPConfig config) { + this.advertiseGroup = builder.advertiseGroup; + this.advertiseAddress = builder.advertiseAddress; + this.advertiseFrequency = builder.advertiseFrequency; + this.advertisePort = builder.advertisePort; + this.securityKey = builder.securityKey; + this.protocol = builder.protocol; + this.path = builder.path; + this.managementHost = config.getManagementHost(); + this.managementPort = config.getManagementPort(); + } + + public String getAdvertiseGroup() { + return advertiseGroup; + } + + public String getAdvertiseAddress() { + return advertiseAddress; + } + + public int getAdvertisePort() { + return advertisePort; + } + + public String getSecurityKey() { + return securityKey; + } + + public String getProtocol() { + return protocol; + } + + public String getPath() { + return path; + } + + public int getAdvertiseFrequency() { + return advertiseFrequency; + } + + public String getManagementHost() { + return managementHost; + } + + public int getManagementPort() { + return managementPort; + } + } + + public static class Builder { + + private String managementHost; + private int managementPort; + private AdvertiseBuilder advertiseBuilder; + + public Builder setManagementHost(String managementHost) { + this.managementHost = managementHost; + return this; + } + + public Builder setManagementPort(int managementPort) { + this.managementPort = managementPort; + return this; + } + + public AdvertiseBuilder enableAdvertise() { + this.advertiseBuilder = new AdvertiseBuilder(this); + return advertiseBuilder; + } + + public MCMPConfig build() { + return new MCMPConfig(this); + } + + public HttpHandler create(final ModCluster modCluster, final HttpHandler next) { + final MCMPConfig config = build(); + return config.create(modCluster, next); + } + + } + + public static class WebBuilder extends Builder { + + boolean checkNonce = true; + boolean reduceDisplay = false; + boolean allowCmd = true; + boolean displaySessionids = false; + + public WebBuilder setCheckNonce(boolean checkNonce) { + this.checkNonce = checkNonce; + return this; + } + + public WebBuilder setReduceDisplay(boolean reduceDisplay) { + this.reduceDisplay = reduceDisplay; + return this; + } + + public WebBuilder setAllowCmd(boolean allowCmd) { + this.allowCmd = allowCmd; + return this; + } + + public WebBuilder setDisplaySessionids(boolean displaySessionids) { + this.displaySessionids = displaySessionids; + return this; + } + + @Override + public MCMPConfig build() { + return new MCMPWebManagerConfig(this); + } + + } + + public static class AdvertiseBuilder { + + String advertiseGroup = "224.0.1.105"; + String advertiseAddress = "127.0.0.1"; + int advertisePort = 23364; + + String securityKey; + String protocol = "http"; + String path = "/"; + + int advertiseFrequency = 10000; + + private final Builder parent; + public AdvertiseBuilder(Builder parent) { + this.parent = parent; + } + + public AdvertiseBuilder setAdvertiseGroup(String advertiseGroup) { + this.advertiseGroup = advertiseGroup; + return this; + } + + public AdvertiseBuilder setAdvertiseAddress(String advertiseAddress) { + this.advertiseAddress = advertiseAddress; + return this; + } + + public AdvertiseBuilder setAdvertisePort(int advertisePort) { + this.advertisePort = advertisePort; + return this; + } + + public AdvertiseBuilder setSecurityKey(String securityKey) { + this.securityKey = securityKey; + return this; + } + + public AdvertiseBuilder setProtocol(String protocol) { + this.protocol = protocol; + return this; + } + + public AdvertiseBuilder setPath(String path) { + if (path.startsWith("/")) { + this.path = path; + } else { + this.path = "/" + path; + } + return this; + } + + public AdvertiseBuilder setAdvertiseFrequency(int advertiseFrequency) { + this.advertiseFrequency = advertiseFrequency; + return this; + } + + public Builder getParent() { + return parent; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPConstants.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPConstants.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPConstants.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,81 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import io.undertow.util.HttpString; + +/** + * @author Emanuel Muckenhuber + */ +interface MCMPConstants { + + String ALIAS_STRING = "Alias"; + String BALANCER_STRING = "Balancer"; + String CONTEXT_STRING = "Context"; + String DOMAIN_STRING = "Domain"; + String FLUSH_PACKET_STRING = "flushpacket"; + String FLUSH_WAIT_STRING = "flushwait"; + String HOST_STRING = "Host"; + String JVMROUTE_STRING = "JVMRoute"; + String LOAD_STRING = "Load"; + String MAXATTEMPTS_STRING = "Maxattempts"; + String PING_STRING = "ping"; + String PORT_STRING = "Port"; + String REVERSED_STRING = "Reversed"; + String SCHEME_STRING = "Scheme"; + String SMAX_STRING = "smax"; + String STICKYSESSION_STRING = "StickySession"; + String STICKYSESSIONCOOKIE_STRING = "StickySessionCookie"; + String STICKYSESSIONPATH_STRING = "StickySessionPath"; + String STICKYSESSIONREMOVE_STRING = "StickySessionRemove"; + String STICKYSESSIONFORCE_STRING = "StickySessionForce"; + String TIMEOUT_STRING = "Timeout"; + String TTL_STRING = "ttl"; + String TYPE_STRING = "Type"; + String WAITWORKER_STRING = "WaitWorker"; + + HttpString ALIAS = new HttpString(ALIAS_STRING); + HttpString BALANCER = new HttpString(BALANCER_STRING); + HttpString CONTEXT = new HttpString(CONTEXT_STRING); + HttpString DOMAIN = new HttpString(DOMAIN_STRING); + HttpString FLUSH_PACKET = new HttpString(FLUSH_PACKET_STRING); + HttpString FLUSH_WAIT = new HttpString(FLUSH_WAIT_STRING); + HttpString HOST = new HttpString(HOST_STRING); + HttpString JVMROUTE = new HttpString(JVMROUTE_STRING); + HttpString LOAD = new HttpString(LOAD_STRING); + HttpString MAXATTEMPTS = new HttpString(MAXATTEMPTS_STRING); + HttpString PING = new HttpString(PING_STRING); + HttpString PORT = new HttpString(PORT_STRING); + HttpString REVERSED = new HttpString(REVERSED_STRING); + HttpString SCHEME = new HttpString(SCHEME_STRING); + HttpString SMAX = new HttpString(SMAX_STRING); + HttpString STICKYSESSION = new HttpString(STICKYSESSION_STRING); + HttpString STICKYSESSIONCOOKIE = new HttpString(STICKYSESSIONCOOKIE_STRING); + HttpString STICKYSESSIONPATH = new HttpString(STICKYSESSIONPATH_STRING); + HttpString STICKYSESSIONREMOVE = new HttpString(STICKYSESSIONREMOVE_STRING); + HttpString STICKYSESSIONFORCE = new HttpString(STICKYSESSIONFORCE_STRING); + HttpString TIMEOUT = new HttpString(TIMEOUT_STRING); + HttpString TTL = new HttpString(TTL_STRING); + HttpString TYPE = new HttpString(TYPE_STRING); + HttpString WAITWORKER = new HttpString(WAITWORKER_STRING); + + String TYPESYNTAX = "SYNTAX"; + String TYPEMEM = "MEM"; + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPErrorCode.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPErrorCode.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPErrorCode.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TYPEMEM; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TYPESYNTAX; + +/** + * @author Emanuel Muckenhuber + */ +enum MCMPErrorCode { + + CANT_READ_NODE(TYPEMEM, "MEM: Can't read node"), + CANT_UPDATE_NODE(TYPEMEM, "MEM: Can't update or insert node"), + CANT_UPDATE_CONTEXT(TYPEMEM, "MEM: Can't update or insert context"), + NODE_STILL_EXISTS(TYPESYNTAX, "MEM: Old node still exist"), + ; + + private final String type; + private final String message; + MCMPErrorCode(String type, String message) { + this.type = type; + this.message = message; + } + + public String getType() { + return type; + } + + public String getMessage() { + return message; + } + + /** the syntax error messages + String SMESPAR = "SYNTAX: Can't parse message"; + String SBALBIG = "SYNTAX: Balancer field too big"; + String SBAFBIG = "SYNTAX: A field is too big"; + String SROUBIG = "SYNTAX: JVMRoute field too big"; + String SROUBAD = "SYNTAX: JVMRoute can't be empty"; + String SDOMBIG = "SYNTAX: LBGroup field too big"; + String SHOSBIG = "SYNTAX: Host field too big"; + String SPORBIG = "SYNTAX: Port field too big"; + String STYPBIG = "SYNTAX: Type field too big"; + String SALIBAD = "SYNTAX: Alias without Context"; + String SCONBAD = "SYNTAX: Context without Alias"; + String SBADFLD = "SYNTAX: Invalid field "; + String SBADFLD1 = " in message"; + String SMISFLD = "SYNTAX: Mandatory field(s) missing in message"; + String SCMDUNS = "SYNTAX: Command is not supported"; + String SMULALB = "SYNTAX: Only one Alias in APP command"; + String SMULCTB = "SYNTAX: Only one Context in APP command"; + String SREADER = "SYNTAX: %s can't read POST data"; + + String MBALAUI = "MEM: Can't update or insert balancer"; + String MHOSTRD = "MEM: Can't read host alias"; + String MHOSTUI = "MEM: Can't update or insert host alias"; + */ + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,789 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.ALIAS; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.BALANCER; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.CONTEXT; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.DOMAIN; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.FLUSH_PACKET; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.FLUSH_WAIT; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.HOST; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.JVMROUTE; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.LOAD; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.MAXATTEMPTS; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.PORT; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.REVERSED; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.SCHEME; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.SMAX; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSION; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONCOOKIE; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONFORCE; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONPATH; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.STICKYSESSIONREMOVE; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TIMEOUT; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TTL; +import static io.undertow.server.handlers.proxy.mod_cluster.MCMPConstants.TYPE; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import io.undertow.UndertowLogger; +import io.undertow.Version; +import io.undertow.io.Sender; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.form.FormData; +import io.undertow.server.handlers.form.FormDataParser; +import io.undertow.server.handlers.form.FormEncodedDataDefinition; +import io.undertow.server.handlers.form.FormParserFactory; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import org.xnio.OptionMap; +import org.xnio.Options; +import org.xnio.ssl.XnioSsl; + +/** + * The mod cluster management protocol http handler. + * + * @author Emanuel Muckenhuber + */ +class MCMPHandler implements HttpHandler { + + static enum MCMPAction { + + ENABLE, + DISABLE, + STOP, + REMOVE, + ; + + } + + public static final HttpString CONFIG = new HttpString("CONFIG"); + public static final HttpString ENABLE_APP = new HttpString("ENABLE-APP"); + public static final HttpString DISABLE_APP = new HttpString("DISABLE-APP"); + public static final HttpString STOP_APP = new HttpString("STOP-APP"); + public static final HttpString REMOVE_APP = new HttpString("REMOVE-APP"); + public static final HttpString STATUS = new HttpString("STATUS"); + public static final HttpString DUMP = new HttpString("DUMP"); + public static final HttpString INFO = new HttpString("INFO"); + public static final HttpString PING = new HttpString("PING"); + + protected static final String VERSION_PROTOCOL = "0.2.1"; + protected static final String MOD_CLUSTER_EXPOSED_VERSION = "mod_cluster_undertow/" + Version.getVersionString(); + + private static final String CONTENT_TYPE = "text/plain; charset=ISO-8859-1"; + + /* the syntax error messages */ + private static final String TYPESYNTAX = MCMPConstants.TYPESYNTAX; + private static final String SCONBAD = "SYNTAX: Context without Alias"; + private static final String SBADFLD = "SYNTAX: Invalid field "; + private static final String SBADFLD1 = " in message"; + private static final String SMISFLD = "SYNTAX: Mandatory field(s) missing in message"; + + private final FormParserFactory parserFactory; + private final MCMPConfig config; + private final HttpHandler next; + private final long creationTime = System.currentTimeMillis(); // This should change with each restart + private final ModCluster modCluster; + protected final ModClusterContainer container; + + public MCMPHandler(MCMPConfig config, ModCluster modCluster, HttpHandler next) { + this.config = config; + this.next = next; + this.modCluster = modCluster; + this.container = modCluster.getContainer(); + this.parserFactory = FormParserFactory.builder(false).addParser(new FormEncodedDataDefinition().setForceCreation(true)).build(); + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + /* + * Proxy the request that needs to be proxied and process others + */ + // TODO maybe this should be handled outside here? + final InetSocketAddress addr = exchange.getDestinationAddress(); + //we use getHostString to avoid a reverse lookup + if (addr.getPort() != config.getManagementPort() || (!addr.getHostString().equals(config.getManagementHost()) && !addr.getHostString().equals(config.getManagementHostIp()))) { + next.handleRequest(exchange); + return; + } + + if(exchange.isInIoThread()) { + //for now just do all the management stuff in a worker, as it uses blocking IO + exchange.dispatch(this); + return; + } + + final HttpString method = exchange.getRequestMethod(); + try { + handleRequest(method, exchange); + } catch (Exception e) { + UndertowLogger.ROOT_LOGGER.errorf(e, "failed to process management request"); + exchange.setResponseCode(500); + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE); + final Sender sender = exchange.getResponseSender(); + sender.send("failed to process management request"); + } + } + + /** + * Handle a management+ request. + * + * @param method the http method + * @param exchange the http server exchange + * @throws Exception + */ + protected void handleRequest(final HttpString method, HttpServerExchange exchange) throws Exception { + final RequestData requestData = parseFormData(exchange); + if (CONFIG.equals(method)) { + processConfig(exchange, requestData); + } else if (ENABLE_APP.equals(method)) { + processCommand(exchange, requestData, MCMPAction.ENABLE); + } else if (DISABLE_APP.equals(method)) { + processCommand(exchange, requestData, MCMPAction.DISABLE); + } else if (STOP_APP.equals(method)) { + processCommand(exchange, requestData, MCMPAction.STOP); + } else if (REMOVE_APP.equals(method)) { + processCommand(exchange, requestData, MCMPAction.REMOVE); + } else if (STATUS.equals(method)) { + processStatus(exchange, requestData); + } else if (INFO.equals(method)) { + processInfo(exchange); + } else if (DUMP.equals(method)) { + processDump(exchange); + } else if (PING.equals(method)) { + processPing(exchange, requestData); + } else { + next.handleRequest(exchange); + } + } + + /** + * Process the node config. + * + * @param exchange the http server exchange + * @param requestData the request data + * @throws IOException + */ + private void processConfig(final HttpServerExchange exchange, final RequestData requestData) throws IOException { + + // Get the node builder + List hosts = null; + List contexts = null; + final Balancer.BalancerBuilder balancer = Balancer.builder(); + final NodeConfig.NodeBuilder node = NodeConfig.builder(modCluster); + final Iterator i = requestData.iterator(); + while (i.hasNext()) { + final HttpString name = i.next(); + final String value = requestData.getFirst(name); + + if (!checkString(value)) { + processError(TYPESYNTAX, SBADFLD + name + SBADFLD1, exchange); + return; + } + + if (BALANCER.equals(name)) { + node.setBalancer(value); + balancer.setName(value); + } else if (MAXATTEMPTS.equals(name)) { + balancer.setMaxattempts(Integer.valueOf(value)); + } else if (STICKYSESSION.equals(name)) { + if ("No".equalsIgnoreCase(value)) { + balancer.setStickySession(false); + } + } else if (STICKYSESSIONCOOKIE.equals(name)) { + balancer.setStickySessionCookie(value); + } else if (STICKYSESSIONPATH.equals(name)) { + balancer.setStickySessionPath(value); + } else if (STICKYSESSIONREMOVE.equals(name)) { + if ("Yes".equalsIgnoreCase(value)) { + balancer.setStickySessionRemove(true); + } + } else if (STICKYSESSIONFORCE.equals(name)) { + if ("no".equalsIgnoreCase(value)) { + balancer.setStickySessionForce(false); + } + } else if (JVMROUTE.equals(name)) { + node.setJvmRoute(value); + } else if (DOMAIN.equals(name)) { + node.setDomain(value); + } else if (HOST.equals(name)) { + node.setHostname(value); + } else if (PORT.equals(name)) { + node.setPort(Integer.parseInt(value)); + } else if (TYPE.equals(name)) { + node.setType(value); + } else if (REVERSED.equals(name)) { + continue; // ignore + } else if (FLUSH_PACKET.equals(name)) { + if ("on".equalsIgnoreCase(value)) { + node.setFlushPackets(true); + } else if ("auto".equalsIgnoreCase(value)) { + node.setFlushPackets(true); + } + } else if (FLUSH_WAIT.equals(name)) { + node.setFlushwait(Integer.valueOf(value)); + } else if (MCMPConstants.PING.equals(name)) { + node.setPing(Integer.valueOf(value)); + } else if (SMAX.equals(name)) { + node.setSmax(Integer.valueOf(value)); + } else if (TTL.equals(name)) { + node.setTtl(Integer.valueOf(value)); + } else if (TIMEOUT.equals(name)) { + node.setTimeout(Integer.valueOf(value)); + } else if (CONTEXT.equals(name)) { + final String[] context = value.split(","); + contexts = Arrays.asList(context); + } else if (ALIAS.equals(name)) { + final String[] alias = value.split(","); + hosts = Arrays.asList(alias); + } else { + processError(TYPESYNTAX, SBADFLD + name + SBADFLD1, exchange); + return; + } + } + + final NodeConfig config; + try { + // Build the config + config = node.build(); + if (container.addNode(config, balancer, exchange.getIoThread(), exchange.getConnection().getBufferPool())) { + // Apparently this is hard to do in the C part, so maybe we should just remove this + if (contexts != null && hosts != null) { + for (final String context : contexts) { + container.enableContext(context, config.getJvmRoute(), hosts); + } + } + processOK(exchange); + } else { + processError(MCMPErrorCode.NODE_STILL_EXISTS, exchange); + } + } catch (Exception e) { + processError(MCMPErrorCode.CANT_UPDATE_NODE, exchange); + } + } + + /** + * Process a mod_cluster mgmt command. + * + * @param exchange the http server exchange + * @param requestData the request data + * @param action the mgmt action + * @throws IOException + */ + void processCommand(final HttpServerExchange exchange, final RequestData requestData, final MCMPAction action) throws IOException { + if (exchange.getRequestPath().equals("*") || exchange.getRequestPath().endsWith("/*")) { + processNodeCommand(exchange, requestData, action); + } else { + processAppCommand(exchange, requestData, action); + } + } + + /** + * Process a mgmt command targeting a node. + * + * @param exchange the http server exchange + * @param requestData the request data + * @param action the mgmt action + * @throws IOException + */ + void processNodeCommand(final HttpServerExchange exchange, final RequestData requestData, final MCMPAction action) throws IOException { + final String jvmRoute = requestData.getFirst(JVMROUTE); + if (jvmRoute == null) { + processError(TYPESYNTAX, SMISFLD, exchange); + return; + } + if (processNodeCommand(jvmRoute, action)) { + processOK(exchange); + } else { + processError(MCMPErrorCode.CANT_UPDATE_NODE, exchange); + } + } + + boolean processNodeCommand(final String jvmRoute, final MCMPAction action) throws IOException { + switch (action) { + case ENABLE: + return container.enableNode(jvmRoute); + case DISABLE: + return container.disableNode(jvmRoute); + case STOP: + return container.stopNode(jvmRoute); + case REMOVE: + return container.removeNode(jvmRoute) != null; + } + return false; + } + + /** + * Process a command targeting an application. + * + * @param exchange the http server exchange + * @param requestData the request data + * @param action the mgmt action + * @return + * @throws IOException + */ + void processAppCommand(final HttpServerExchange exchange, final RequestData requestData, final MCMPAction action) throws IOException { + + final String contextPath = requestData.getFirst(CONTEXT); + final String jvmRoute = requestData.getFirst(JVMROUTE); + final String aliases = requestData.getFirst(ALIAS); + + if (contextPath == null || jvmRoute == null || aliases == null) { + processError(TYPESYNTAX, SMISFLD, exchange); + return; + } + final List virtualHosts = aliases != null ? Arrays.asList(aliases.split(",")) : null; + if (virtualHosts == null || virtualHosts.isEmpty()) { + processError(TYPESYNTAX, SCONBAD, exchange); + return; + } + + String response = null; + switch (action) { + case ENABLE: + if (!container.enableContext(contextPath, jvmRoute, virtualHosts)) { + processError(MCMPErrorCode.CANT_UPDATE_CONTEXT, exchange); + return; + } + break; + case DISABLE: + if (!container.disableContext(contextPath, jvmRoute, virtualHosts)) { + processError(MCMPErrorCode.CANT_UPDATE_CONTEXT, exchange); + return; + } + break; + case STOP: + int i = container.stopContext(contextPath, jvmRoute, virtualHosts); + final StringBuilder builder = new StringBuilder(); + builder.append("Type=STOP-APP-RSP,JvmRoute=").append(jvmRoute); + builder.append("Alias=").append(aliases); + builder.append("Context=").append(contextPath); + builder.append("Requests=").append(i); + response = builder.toString(); + break; + case REMOVE: + if (!container.removeContext(contextPath, jvmRoute, virtualHosts)) { + processError(MCMPErrorCode.CANT_UPDATE_CONTEXT, exchange); + return; + } + break; + default: { + processError(TYPESYNTAX, SMISFLD, exchange); + return; + } + } + if (response != null) { + sendResponse(exchange, response); + } else { + processOK(exchange); + } + } + + /** + * Process the status request. + * + * @param exchange the http server exchange + * @param requestData the request data + * @throws IOException + */ + void processStatus(final HttpServerExchange exchange, final RequestData requestData) throws IOException { + + final String jvmRoute = requestData.getFirst(JVMROUTE); + final String loadValue = requestData.getFirst(LOAD); + + if (loadValue == null || jvmRoute == null) { + processError(TYPESYNTAX, SMISFLD, exchange); + return; + } + + final int load = Integer.valueOf(loadValue); + if (load > 0 || load == -2) { + + final Node node = container.getNode(jvmRoute); + if (node == null) { + final String response = "Type=STATUS-RSP&State=NOTOK&JVMRoute=" + jvmRoute + "&id=" + creationTime; + sendResponse(exchange, response); + return; + } + + final NodePingUtil.PingCallback callback = new NodePingUtil.PingCallback() { + @Override + public void completed() { + final String response = "Type=STATUS-RSP&State=OK&JVMRoute=" + jvmRoute + "&id=" + creationTime; + try { + if (load > 0) { + node.updateLoad(load); + } + sendResponse(exchange, response); + } catch (Exception e) { + UndertowLogger.ROOT_LOGGER.debugf(e, "failed to send ping response"); + } + } + + @Override + public void failed() { + final String response = "Type=STATUS-RSP&State=NOTOK&JVMRoute=" + jvmRoute + "&id=" + creationTime; + try { + node.markInError(); + sendResponse(exchange, response); + } catch (Exception e) { + UndertowLogger.ROOT_LOGGER.debugf(e, "failed to send ping response"); + } + } + }; + + // Ping the node + node.ping(exchange, callback); + + } else if (load == 0) { + final Node node = container.getNode(jvmRoute); + if (node != null) { + node.hotStandby(); + sendResponse(exchange, "Type=STATUS-RSP&State=OK&JVMRoute=" + jvmRoute + "&id=" + creationTime); + } else { + processError(MCMPErrorCode.CANT_READ_NODE, exchange); + } + } else if (load == -1) { + // Error, disable node + final Node node = container.getNode(jvmRoute); + if (node != null) { + node.markInError(); + sendResponse(exchange, "Type=STATUS-RSP&State=NOTOK&JVMRoute=" + jvmRoute + "&id=" + creationTime); + } else { + processError(MCMPErrorCode.CANT_READ_NODE, exchange); + } + } else { + processError(TYPESYNTAX, SMISFLD, exchange); + } + } + + /** + * Process the ping request. + * + * @param exchange the http server exchange + * @param requestData the request data + * @throws IOException + */ + void processPing(final HttpServerExchange exchange, final RequestData requestData) throws IOException { + + final String jvmRoute = requestData.getFirst(JVMROUTE); + final String scheme = requestData.getFirst(SCHEME); + final String host = requestData.getFirst(HOST); + final String port = requestData.getFirst(PORT); + + final String OK = "Type=PING-RSP&State=OK&id=" + creationTime; + final String NOTOK = "Type=PING-RSP&State=NOTOK&id=" + creationTime; + + if (jvmRoute != null) { + // ping the corresponding node. + final Node nodeConfig = container.getNode(jvmRoute); + if (nodeConfig == null) { + sendResponse(exchange, NOTOK); + return; + } + final NodePingUtil.PingCallback callback = new NodePingUtil.PingCallback() { + @Override + public void completed() { + try { + sendResponse(exchange, OK); + } catch (Exception e) { + e.printStackTrace(); + } + } + + @Override + public void failed() { + try { + nodeConfig.markInError(); + sendResponse(exchange, NOTOK); + } catch (Exception e) { + e.printStackTrace(); + } + } + }; + nodeConfig.ping(exchange, callback); + } else { + if (scheme == null && host == null && port == null) { + sendResponse(exchange, OK); + return; + } else { + if (host == null || port == null) { + processError(TYPESYNTAX, SMISFLD, exchange); + return; + } + // Check whether we can reach the host + checkHostUp(scheme, host, Integer.valueOf(port), exchange, new NodePingUtil.PingCallback() { + @Override + public void completed() { + sendResponse(exchange, OK); + } + + @Override + public void failed() { + sendResponse(exchange, NOTOK); + } + }); + return; + } + } + } + + /* + * Something like: + * + * Node: [1],Name: 368e2e5c-d3f7-3812-9fc6-f96d124dcf79,Balancer: + * cluster-prod-01,LBGroup: ,Host: 127.0.0.1,Port: 8443,Type: + * https,Flushpackets: Off,Flushwait: 10,Ping: 10,Smax: 21,Ttl: 60,Elected: + * 0,Read: 0,Transfered: 0,Connected: 0,Load: 1 Vhost: [1:1:1], Alias: + * default-host Vhost: [1:1:2], Alias: localhost Vhost: [1:1:3], Alias: + * example.com Context: [1:1:1], Context: /myapp, Status: ENABLED + */ + + /** + * Process INFO request + * + * @throws Exception + */ + protected void processInfo(HttpServerExchange exchange) throws IOException { + final String data = processInfoString(); + exchange.getResponseHeaders().add(Headers.SERVER, MOD_CLUSTER_EXPOSED_VERSION); + sendResponse(exchange, data); + } + + protected String processInfoString() { + final StringBuilder builder = new StringBuilder(); + final List vHosts = new ArrayList<>(); + final List contexts = new ArrayList<>(); + final Collection nodes = container.getNodes(); + for (final Node node : nodes) { + MCMPInfoUtil.printInfo(node, builder); + vHosts.addAll(node.getVHosts()); + contexts.addAll(node.getContexts()); + } + for (final Node.VHostMapping vHost : vHosts) { + MCMPInfoUtil.printInfo(vHost, builder); + } + for (final Context context : contexts) { + MCMPInfoUtil.printInfo(context, builder); + } + return builder.toString(); + } + + /* + * something like: + * + * balancer: [1] Name: cluster-prod-01 Sticky: 1 [JSESSIONID]/[jsessionid] + * remove: 0 force: 0 Timeout: 0 maxAttempts: 1 node: [1:1],Balancer: + * cluster-prod-01,JVMRoute: 368e2e5c-d3f7-3812-9fc6-f96d124dcf79,LBGroup: + * [],Host: 127.0.0.1,Port: 8443,Type: https,flushpackets: 0,flushwait: + * 10,ping: 10,smax: 21,ttl: 60,timeout: 0 host: 1 [default-host] vhost: 1 + * node: 1 host: 2 [localhost] vhost: 1 node: 1 host: 3 [example.com] vhost: + * 1 node: 1 context: 1 [/myapp] vhost: 1 node: 1 status: 1 + */ + + /** + * Process DUMP request + * + * @param exchange + * @throws java.io.IOException + */ + protected void processDump(HttpServerExchange exchange) throws IOException { + final String data = processDumpString(); + exchange.getResponseHeaders().add(Headers.SERVER, MOD_CLUSTER_EXPOSED_VERSION); + sendResponse(exchange, data); + } + + protected String processDumpString() { + + final StringBuilder builder = new StringBuilder(); + final Collection balancers = container.getBalancers(); + for (final Balancer balancer : balancers) { + MCMPInfoUtil.printDump(balancer, builder); + } + + final List vHosts = new ArrayList<>(); + final List contexts = new ArrayList<>(); + final Collection nodes = container.getNodes(); + for (final Node node : nodes) { + MCMPInfoUtil.printDump(node, builder); + vHosts.addAll(node.getVHosts()); + contexts.addAll(node.getContexts()); + } + for (final Node.VHostMapping vHost : vHosts) { + MCMPInfoUtil.printDump(vHost, builder); + } + for (final Context context : contexts) { + MCMPInfoUtil.printDump(context, builder); + } + return builder.toString(); + } + + /** + * Check whether a host is up. + * + * @param scheme the scheme + * @param host the host + * @param port the port + * @param exchange the http server exchange + * @param callback the ping callback + */ + protected void checkHostUp(final String scheme, final String host, final int port, final HttpServerExchange exchange, final NodePingUtil.PingCallback callback) { + + final XnioSsl xnioSsl = null; // TODO + final OptionMap options = OptionMap.builder() + .set(Options.TCP_NODELAY, true) + .getMap(); + + try { + // http, ajp and maybe more in future + if ("ajp".equalsIgnoreCase(scheme) || "http".equalsIgnoreCase(scheme)) { + final URI uri = new URI(scheme, null, host, port, "/", null, null); + NodePingUtil.pingHttpClient(uri, callback, exchange, container.getClient(), xnioSsl, options); + } else { + final InetSocketAddress address = new InetSocketAddress(host, port); + NodePingUtil.pingHost(address, exchange, callback, options); + } + } catch (URISyntaxException e) { + callback.failed(); + } + } + + /** + * Send a simple response string. + * + * @param exchange the http server exchange + * @param response the response string + */ + static void sendResponse(final HttpServerExchange exchange, final String response) { + exchange.setResponseCode(200); + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE); + final Sender sender = exchange.getResponseSender(); + sender.send(response); + } + + /** + * If the process is OK, then add 200 HTTP status and its "OK" phrase + * + * @throws Exception + */ + static void processOK(HttpServerExchange exchange) throws IOException { + exchange.setResponseCode(200); + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE); + exchange.endExchange(); + } + + static void processError(MCMPErrorCode errorCode, HttpServerExchange exchange) { + processError(errorCode.getType(), errorCode.getMessage(), exchange); + } + + /** + * Send an error message. + * + * @param type the error type + * @param errString the error string + * @param exchange the http server exchange + */ + static void processError(String type, String errString, HttpServerExchange exchange) { + exchange.setResponseCode(500); + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, CONTENT_TYPE); + exchange.getResponseHeaders().add(new HttpString("Version"), VERSION_PROTOCOL); + exchange.getResponseHeaders().add(new HttpString("Type"), type); + exchange.getResponseHeaders().add(new HttpString("Mess"), errString); + exchange.endExchange(); + } + + /** + * Transform the form data into an intermediate request data which can me used + * by the web manager + * + * @param exchange the http server exchange + * @return + * @throws IOException + */ + RequestData parseFormData(final HttpServerExchange exchange) throws IOException { + // Read post parameters + final FormDataParser parser = parserFactory.createParser(exchange); + final FormData formData = parser.parseBlocking(); + final RequestData data = new RequestData(); + final Iterator i = formData.iterator(); + while (i.hasNext()) { + final String name = i.next(); + final HttpString key = new HttpString(name); + data.add(key, formData.get(name)); + } + return data; + } + + static class RequestData { + + private final Map> values = new LinkedHashMap<>(); + + Iterator iterator() { + return values.keySet().iterator(); + } + + void add(final HttpString name, Deque values) { + for (final FormData.FormValue value : values) { + add(name, value); + } + } + + void addValues(final HttpString name, Deque value) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, value); + } else { + values.addAll(value); + } + } + + void add(final HttpString name, final FormData.FormValue value) { + Deque values = this.values.get(name); + if (values == null) { + this.values.put(name, values = new ArrayDeque<>(1)); + } + values.add(value.getValue()); + } + + String getFirst(HttpString name) { + final Deque deque = values.get(name); + return deque == null ? null : deque.peekFirst(); + } + + } + + static boolean checkString(final String value) { + return value != null && value.length() > 0; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPInfoUtil.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPInfoUtil.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPInfoUtil.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,130 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +/** + * @author Emanuel Muckenhuber + */ +class MCMPInfoUtil { + + private static final String NEWLINE = "\n"; + + static void printDump(final Balancer balancer, final StringBuilder builder) { + builder.append("balancer: [").append(balancer.getId()).append("],") + .append(" Name: ").append(balancer.getName()) + .append(" Sticky: ").append(formatBoolean(balancer.isStickySession())) + .append(" [").append(balancer.getStickySessionCookie()).append("]/[").append(balancer.getStickySessionPath()).append("]") + .append(" remove: ").append(formatBoolean(balancer.isStickySessionRemove())) + .append("force: ").append(formatBoolean(balancer.isStickySessionForce())) + .append("Timeout: ").append(balancer.getWaitWorker()) + .append("maxAttempts: ").append(balancer.getMaxattempts()) + .append(NEWLINE); + } + + static void printInfo(final Node.VHostMapping host, final StringBuilder builder) { + builder.append("Vhost: [") + // .append(host.getNode().getBalancer().getId()).append(":") // apparently no balancer + .append(host.getNode().getId()).append(":") + .append(host.getId()).append(":") + .append(-1) // id[i] id in the table!? does not exist + .append("], Alias: ").append(host.getAliases()) + .append(NEWLINE); + } + + static void printDump(final Node.VHostMapping host, final StringBuilder builder) { + final int hostID = host.getId(); + final int nodeID = host.getNode().getId(); + for (final String alias : host.getAliases()) { + builder.append("host: ").append(hostID).append(" [") + .append(alias).append("] vhost: ").append(host.getId()) + .append(" node: ").append(nodeID) + .append(NEWLINE); + } + } + + static void printInfo(final Context context, final StringBuilder builder) { + builder.append("Context: ").append("[") + .append(context.getNode().getId()).append(":") + .append(context.getVhost().getId()).append(":") + .append(context.getId()).append("]") + .append("],Context: ").append(context.getPath()) + .append(",Status: ").append(context.getStatus()) + .append(NEWLINE); + } + + static void printDump(final Context context, final StringBuilder builder) { + builder.append("context: ").append("[").append(context.getId()).append("]") + .append(" [").append(context.getPath()) + .append("] vhost: ").append(context.getVhost().getId()) + .append("node: ").append(context.getNode().getId()) + .append("status: ").append(context.getStatus()) + .append(NEWLINE); + } + + static void printInfo(final Node node, final StringBuilder builder) { + builder.append("Node: [") + // .append(node.getBalancer().getId()).append(":") + .append(node.getId()).append("]") + .append(",Name: ").append(node.getJvmRoute()) + .append(",Balancer: ").append(node.getNodeConfig().getBalancer()) + .append(",JVMRoute: ").append(node.getJvmRoute()) + .append(",LBGroup: ").append(node.getNodeConfig().getDomain()) + .append(",Host: ").append(node.getNodeConfig().getConnectionURI().getHost()) + .append(",Port: ").append(node.getNodeConfig().getConnectionURI().getPort()) + .append(",Type: ").append(node.getNodeConfig().getConnectionURI().getScheme()) + .append(",flushpackets: ").append(formatBoolean(node.getNodeConfig().isFlushPackets())) + .append(",flushwait: ").append(node.getNodeConfig().getFlushwait()) + .append(",ping: ").append(node.getNodeConfig().getPing()) + .append(",smax: ").append(node.getNodeConfig().getSmax()) + .append(",ttl: ").append(node.getNodeConfig().getTtl()) + .append(",timeout: ").append(node.getNodeConfig().getTimeout()) + // + .append(",Elected: ").append(node.getElected()) + .append(",Read: ").append(node.getStats().getRead()) + .append(",Transferred: ").append(node.getStats().getTransferred()) + .append(",Connected: ").append(node.getStats().getOpenConnections()) + .append(",Load: ").append(node.getLoad()) + + .append(NEWLINE); + } + + static void printDump(final Node node, final StringBuilder builder) { + builder.append("node: [") + .append(node.getBalancer().getId()).append(":") + .append(node.getId()).append("]") + .append(",Balancer: ").append(node.getNodeConfig().getBalancer()) + .append(",JVMRoute: ").append(node.getJvmRoute()) + .append(",LBGroup: ").append(node.getNodeConfig().getDomain()) + .append(",Host: ").append(node.getNodeConfig().getConnectionURI().getHost()) + .append(",Port: ").append(node.getNodeConfig().getConnectionURI().getPort()) + .append(",Type: ").append(node.getNodeConfig().getConnectionURI().getScheme()) + .append(",flushpackets: ").append(formatBoolean(node.getNodeConfig().isFlushPackets())) + .append(",flushwait: ").append(node.getNodeConfig().getFlushwait()) + .append(",ping: ").append(node.getNodeConfig().getPing()) + .append(",smax: ").append(node.getNodeConfig().getSmax()) + .append(",ttl: ").append(node.getNodeConfig().getTtl()) + .append(",timeout: ").append(node.getNodeConfig().getTimeout()) + .append(NEWLINE); + } + + static String formatBoolean(boolean value) { + return value ? "1" : "0"; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPWebManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPWebManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/MCMPWebManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,390 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.io.IOException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; + +import io.undertow.io.Sender; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; + +/** + * The mod cluster manager web frontend. + * + * @author Emanuel Muckenhuber + */ +class MCMPWebManager extends MCMPHandler { + + private final boolean checkNonce; + private final boolean reduceDisplay; + private final boolean allowCmd; + private final boolean displaySessionIds; + + private final Random r = new SecureRandom(); + private String nonce = null; + + public MCMPWebManager(MCMPConfig.MCMPWebManagerConfig config, ModCluster modCluster, HttpHandler next) { + super(config, modCluster, next); + this.checkNonce = config.isCheckNonce(); + this.reduceDisplay = config.isReduceDisplay(); + this.allowCmd = config.isAllowCmd(); + this.displaySessionIds = config.isDisplaySessionids(); + } + + String getNonce() { + return "nonce=" + getRawNonce(); + } + + synchronized String getRawNonce() { + if (this.nonce == null) { + byte[] nonce = new byte[16]; + r.nextBytes(nonce); + this.nonce = ""; + for (int i = 0; i < 16; i = i + 2) { + this.nonce = this.nonce.concat(Integer.toHexString(0xFF & nonce[i] * 16 + 0xFF & nonce[i + 1])); + } + } + return nonce; + } + + @Override + protected void handleRequest(HttpString method, HttpServerExchange exchange) throws Exception { + if (!Methods.GET.equals(method)) { + super.handleRequest(method, exchange); + return; + } + + // Process the request + processRequest(exchange); + } + + private void processRequest(HttpServerExchange exchange) throws IOException { + Map> params = exchange.getQueryParameters(); + boolean hasNonce = params.containsKey("nonce"); + int refreshTime = 0; + if (checkNonce) { + /* Check the nonce */ + if (hasNonce) { + String receivedNonce = params.get("nonce").getFirst(); + if (receivedNonce.equals(getRawNonce())) { + boolean refresh = params.containsKey("refresh"); + if (refresh) { + String sval = params.get("refresh").getFirst(); + refreshTime = Integer.parseInt(sval); + if (refreshTime < 10) + refreshTime = 10; + exchange.getResponseHeaders().add(new HttpString("Refresh"), Integer.toString(refreshTime)); + } + boolean cmd = params.containsKey("Cmd"); + boolean range = params.containsKey("Range"); + if (cmd) { + String scmd = params.get("Cmd").getFirst(); + if (scmd.equals("INFO")) { + processInfo(exchange); + return; + } else if (scmd.equals("DUMP")) { + processDump(exchange); + return; + } else if (scmd.equals("ENABLE-APP") && range) { + String srange = params.get("Range").getFirst(); + final RequestData data = buildRequestData(exchange, params); + if (srange.equals("NODE")) { + processNodeCommand(exchange, data, MCMPAction.DISABLE); + } + if (srange.equals("DOMAIN")) { + boolean domain = params.containsKey("Domain"); + if (domain) { + String sdomain = params.get("Domain").getFirst(); + processDomainCmd(exchange, sdomain, MCMPAction.ENABLE); + } + } + if (srange.equals("CONTEXT")) { + processAppCommand(exchange, data, MCMPAction.ENABLE); + } + } else if (scmd.equals("DISABLE-APP") && range) { + final String srange = params.get("Range").getFirst(); + final RequestData data = buildRequestData(exchange, params); + if (srange.equals("NODE")) { + processNodeCommand(exchange, data, MCMPAction.DISABLE); + } + if (srange.equals("DOMAIN")) { + boolean domain = params.containsKey("Domain"); + if (domain) { + String sdomain = params.get("Domain").getFirst(); + processDomainCmd(exchange, sdomain, MCMPAction.DISABLE); + } + } + if (srange.equals("CONTEXT")) { + processAppCommand(exchange, data, MCMPAction.DISABLE); + } + } + return; + } + } + } + } + + exchange.setResponseCode(200); + exchange.getResponseHeaders().add(Headers.CONTENT_TYPE, "text/html; charset=ISO-8859-1"); + final Sender resp = exchange.getResponseSender(); + + final StringBuilder buf = new StringBuilder(); + buf.append("\nMod_cluster Status\n\n"); + buf.append("

" + MOD_CLUSTER_EXPOSED_VERSION + "

"); + + final String uri = exchange.getRequestPath(); + final String nonce = getNonce(); + if (refreshTime <= 0) { + buf.append("Auto Refresh"); + } + buf.append(" show DUMP output"); + buf.append(" show INFO output"); + buf.append("\n"); + + // Show load balancing groups + final Map> nodes = new LinkedHashMap<>(); + for (final Node node : container.getNodes()) { + final String domain = node.getNodeConfig().getDomain() != null ? node.getNodeConfig().getDomain() : ""; + List list = nodes.get(domain); + if (list == null) { + list = new ArrayList<>(); + nodes.put(domain, list); + } + list.add(node); + } + + for (Map.Entry> entry : nodes.entrySet()) { + final String groupName = entry.getKey(); + if (reduceDisplay) { + buf.append("

LBGroup " + groupName + ": "); + } else { + buf.append("

LBGroup " + groupName + ": "); + } + if (allowCmd) { + domainCommandString(buf, uri, MCMPAction.ENABLE, groupName); + domainCommandString(buf, uri, MCMPAction.DISABLE, groupName); + } + + for (final Node node : entry.getValue()) { + final NodeConfig nodeConfig = node.getNodeConfig(); + if (reduceDisplay) { + buf.append("

Node " + nodeConfig.getJvmRoute()); + printProxyStat(buf, node, reduceDisplay); + } else { + buf.append("

Node " + nodeConfig.getJvmRoute() + " (" + nodeConfig.getConnectionURI() + "):

\n"); + } + + if (allowCmd) { + nodeCommandString(buf, uri, MCMPAction.ENABLE, nodeConfig.getJvmRoute()); + nodeCommandString(buf, uri, MCMPAction.DISABLE, nodeConfig.getJvmRoute()); + } + if (!reduceDisplay) { + buf.append("
\n"); + buf.append("Balancer: " + nodeConfig.getBalancer() + ",LBGroup: " + nodeConfig.getDomain()); + String flushpackets = "off"; + if (nodeConfig.isFlushPackets()) { + flushpackets = "Auto"; + } + buf.append(",Flushpackets: " + flushpackets + ",Flushwait: " + nodeConfig.getFlushwait() + ",Ping: " + nodeConfig.getPing() + " ,Smax: " + nodeConfig.getPing() + ",Ttl: " + nodeConfig.getTtl()); + printProxyStat(buf, node, reduceDisplay); + } else { + buf.append("
\n"); + } + // the sessionid list is mostly for demos. + if (displaySessionIds) { + // buf.append(",Num sessions: " + container.getJVMRouteSessionCount(nodeConfig.getJvmRoute())); + } + buf.append("\n"); + + // Process the virtual-host of the node + printInfoHost(buf, uri, reduceDisplay, allowCmd, node); + } + } + + // Display the all the actives sessions + if (displaySessionIds) { + printInfoSessions(buf, Collections.emptyList()); + } + + buf.append("\n"); + resp.send(buf.toString()); + } + + void nodeCommandString(StringBuilder buf, String uri, MCMPAction status, String jvmRoute) { + switch (status) { + case ENABLE: + buf.append("Enable Contexts "); + break; + case DISABLE: + buf.append("Disable Contexts "); + break; + } + } + + static void printProxyStat(StringBuilder buf, Node node, boolean reduceDisplay) { + String status = "NOTOK"; + if (node.getStatus() == Node.Status.NODE_UP) + status = "OK"; + if (reduceDisplay) { + buf.append(" " + status + " "); + } else { + buf.append(",Status: " + status + ",Elected: " + node.getElected() + ",Read: " + node.getStats().getRead() + ",Transferred: " + node.getStats().getTransferred() + ",Connected: " + + node.getStats().getOpenConnections() + ",Load: " + node.getLoad()); + } + } + + /* based on domain_command_string */ + void domainCommandString(StringBuilder buf, String uri, MCMPAction status, String lbgroup) { + switch (status) { + case ENABLE: + buf.append("Enable Nodes "); + break; + case DISABLE: + buf.append("Disable Nodes"); + break; + } + } + + void processDomainCmd(HttpServerExchange exchange, String domain, MCMPAction action) throws IOException { + if (domain != null) { + for (final Node node : container.getNodes()) { + if (domain.equals(node.getNodeConfig().getDomain())) { + processNodeCommand(node.getJvmRoute(), action); + } + } + } + processOK(exchange); + } + + /* + * list the session information. + */ + static void printInfoSessions(StringBuilder buf, List sessionids) { + buf.append("

SessionIDs:

"); + buf.append("
");
+        for (SessionId s : sessionids) {
+            buf.append("id: " + s.getSessionId() + " route: " + s.getJmvRoute() + "\n");
+        }
+        buf.append("
"); + } + + /* based on manager_info_hosts */ + private void printInfoHost(StringBuilder buf, String uri, boolean reduceDisplay, boolean allowCmd, final Node node) { + final String jvmRoute = node.getJvmRoute(); + for (Node.VHostMapping host : node.getVHosts()) { + if (!reduceDisplay) { + buf.append("

Virtual Host " + host.getId() + ":

"); + } + printInfoContexts(buf, uri, reduceDisplay, allowCmd, host.getId(), host, node); + if (reduceDisplay) { + buf.append("Aliases: "); + for (String alias : host.getAliases()) { + buf.append(alias + " "); + } + } else { + buf.append("

Aliases:

"); + buf.append("
");
+                for (String alias : host.getAliases()) {
+                    buf.append(alias + "\n");
+                }
+                buf.append("
"); + } + } + } + + /* based on manager_info_contexts */ + private void printInfoContexts(StringBuilder buf, String uri, boolean reduceDisplay, boolean allowCmd, long host, Node.VHostMapping vhost, Node node) { + if (!reduceDisplay) + buf.append("

Contexts:

"); + buf.append("
");
+        for (Context context : node.getContexts()) {
+            if (context.getVhost() == vhost) {
+                String status = "REMOVED";
+                switch (context.getStatus()) {
+                    case ENABLED:
+                        status = "ENABLED";
+                        break;
+                    case DISABLED:
+                        status = "DISABLED";
+                        break;
+                    case STOPPED:
+                        status = "STOPPED";
+                        break;
+                }
+                buf.append(context.getPath() + " , Status: " + status + " Request: " + context.getActiveRequests() + " ");
+                if (allowCmd) {
+                    contextCommandString(buf, uri, context.getStatus(), context.getPath(), vhost.getAliases(), node.getJvmRoute());
+                }
+                buf.append("\n");
+            }
+        }
+        buf.append("
"); + } + + /* generate a command URL for the context */ + void contextCommandString(StringBuilder buf, String uri, Context.Status status, String path, List alias, String jvmRoute) { + switch (status) { + case DISABLED: + buf.append("Enable "); + break; + case ENABLED: + buf.append("Disable "); + break; + } + } + + static void contextString(StringBuilder buf, String path, List alias, String jvmRoute) { + buf.append("JVMRoute=" + jvmRoute + "&Alias="); + boolean first = true; + for (String a : alias) { + if (first) { + first = false; + } else { + buf.append(","); + } + buf.append(a); + } + buf.append("&Context=" + path); + } + + static RequestData buildRequestData(final HttpServerExchange exchange, Map> params) { + final RequestData data = new RequestData(); + for (final String key : params.keySet()) { + final HttpString name = new HttpString(key); + data.addValues(name, params.get(key)); + } + return data; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModCluster.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModCluster.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModCluster.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,233 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.io.IOException; +import java.util.UUID; +import java.util.concurrent.Executors; +import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.TimeUnit; + +import io.undertow.client.UndertowClient; +import io.undertow.server.HttpHandler; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.server.handlers.proxy.ProxyHandler; +import org.xnio.XnioWorker; +import org.xnio.ssl.XnioSsl; + +/** + * @author Emanuel Muckenhuber + */ +public class ModCluster { + + private static final HttpHandler NEXT_HANDLER = ResponseCodeHandler.HANDLE_404; + + // Health check intervals + private final long healthCheckInterval; + private final long removeBrokenNodes; + private final NodeHealthChecker healthChecker; + + // Proxy connection pool defaults + private final int maxConnections; + private final int cacheConnections; + private final int requestQueueSize; + private final boolean queueNewRequests; + + private final XnioWorker xnioWorker; + private final ModClusterContainer container; + private final HttpHandler proxyHandler; + private final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); + + private final String serverID = UUID.randomUUID().toString(); // TODO + + ModCluster(Builder builder) { + this.xnioWorker = builder.xnioWorker; + this.maxConnections = builder.maxConnections; + this.cacheConnections = builder.cacheConnections; + this.requestQueueSize = builder.requestQueueSize; + this.queueNewRequests = builder.queueNewRequests; + this.healthCheckInterval = builder.healthCheckInterval; + this.removeBrokenNodes = builder.removeBrokenNodes; + this.healthChecker = builder.healthChecker; + this.container = new ModClusterContainer(this, builder.xnioSsl, builder.client); + this.proxyHandler = new ProxyHandler(container.getProxyClient(), builder.maxRequestTime, NEXT_HANDLER); + } + + protected String getServerID() { + return serverID; + } + + protected ModClusterContainer getContainer() { + return container; + } + + public int getMaxConnections() { + return maxConnections; + } + + public int getCacheConnections() { + return cacheConnections; + } + + public int getRequestQueueSize() { + return requestQueueSize; + } + + public boolean isQueueNewRequests() { + return queueNewRequests; + } + + public long getHealthCheckInterval() { + return healthCheckInterval; + } + + public long getRemoveBrokenNodes() { + return removeBrokenNodes; + } + + public NodeHealthChecker getHealthChecker() { + return healthChecker; + } + + /** + * Get the handler proxying the requests. + * + * @return the proxy handler + */ + public HttpHandler getProxyHandler() { + return proxyHandler; + } + + /** + * Start + */ + public synchronized void start() { + if (healthCheckInterval > 0) { + executorService.scheduleAtFixedRate(new Runnable() { + @Override + public void run() { + container.checkHealth(); + } + }, healthCheckInterval, healthCheckInterval, TimeUnit.MILLISECONDS); + } + } + + /** + * Start advertising a mcmp handler. + * + * @param config the mcmp handler config + * @throws IOException + */ + public synchronized void advertise(MCMPConfig config) throws IOException { + final MCMPConfig.AdvertiseConfig advertiseConfig = config.getAdvertiseConfig(); + if (advertiseConfig == null) { + throw new IllegalArgumentException("advertise not enabled"); + } + MCMPAdvertiseTask.advertise(container, advertiseConfig, xnioWorker); + } + + /** + * Stop + */ + public synchronized void stop() { + executorService.shutdownNow(); + } + + public static Builder builder(final XnioWorker worker) { + return builder(worker, UndertowClient.getInstance(), null); + } + + public static Builder builder(final XnioWorker worker, final UndertowClient client) { + return builder(worker, client, null); + } + + public static Builder builder(final XnioWorker worker, final UndertowClient client, final XnioSsl xnioSsl) { + return new Builder(worker, client, xnioSsl); + } + + public static class Builder { + + private final XnioSsl xnioSsl; + private final UndertowClient client; + private final XnioWorker xnioWorker; + + // Fairly restrictive connection pool defaults + private int maxConnections = 16; + private int cacheConnections = 8; + private int requestQueueSize = 0; + private boolean queueNewRequests = false; + + private int maxRequestTime = -1; + + private NodeHealthChecker healthChecker = NodeHealthChecker.OK; + private long healthCheckInterval = TimeUnit.SECONDS.toMillis(10); + private long removeBrokenNodes = TimeUnit.MINUTES.toMillis(1); + + private Builder(XnioWorker xnioWorker, UndertowClient client, XnioSsl xnioSsl) { + this.xnioSsl = xnioSsl; + this.client = client; + this.xnioWorker = xnioWorker; + } + + public ModCluster build() { + return new ModCluster(this); + } + + public Builder setMaxRequestTime(int maxRequestTime) { + this.maxRequestTime = maxRequestTime; + return this; + } + + public Builder setHealthCheckInterval(long healthCheckInterval) { + this.healthCheckInterval = healthCheckInterval; + return this; + } + + public Builder setRemoveBrokenNodes(long removeBrokenNodes) { + this.removeBrokenNodes = removeBrokenNodes; + return this; + } + + public Builder setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + return this; + } + + public Builder setCacheConnections(int cacheConnections) { + this.cacheConnections = cacheConnections; + return this; + } + + public Builder setRequestQueueSize(int requestQueueSize) { + this.requestQueueSize = requestQueueSize; + return this; + } + + public Builder setQueueNewRequests(boolean queueNewRequests) { + this.queueNewRequests = queueNewRequests; + return this; + } + + public Builder setHealthChecker(NodeHealthChecker healthChecker) { + this.healthChecker = healthChecker; + return this; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterContainer.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterContainer.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterContainer.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,510 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentMap; + +import io.undertow.UndertowLogger; +import io.undertow.client.UndertowClient; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.cache.LRUCache; +import io.undertow.server.handlers.proxy.ProxyClient; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.Headers; +import io.undertow.util.PathMatcher; +import org.xnio.Pool; +import org.xnio.XnioIoThread; +import org.xnio.ssl.XnioSsl; + +/** + * @author Stuart Douglas + * @author Emanuel Muckenhuber + */ +class ModClusterContainer { + + // The configured balancers + private final ConcurrentMap balancers = new CopyOnWriteMap<>(); + + // The available nodes + private final ConcurrentMap nodes = new CopyOnWriteMap<>(); + + // virtual-host > per context balancing table + private final ConcurrentMap hosts = new CopyOnWriteMap<>(); + + // Map of removed jvmRoutes to failover domain + private final LRUCache failoverDomains = new LRUCache<>(100, 5 * 60 * 1000); + + private final XnioSsl xnioSsl; + private final UndertowClient client; + private final ProxyClient proxyClient; + private final ModCluster modCluster; + private final NodeHealthChecker healthChecker; + private final long removeBrokenNodesThreshold; + + ModClusterContainer(final ModCluster modCluster, final XnioSsl xnioSsl, final UndertowClient client) { + this.xnioSsl = xnioSsl; + this.client = client; + this.modCluster = modCluster; + this.healthChecker = modCluster.getHealthChecker(); + this.proxyClient = new ModClusterProxyClient(null, this); + this.removeBrokenNodesThreshold = removeThreshold(modCluster.getHealthCheckInterval(), modCluster.getRemoveBrokenNodes()); + } + + String getServerID() { + return modCluster.getServerID(); + } + + UndertowClient getClient() { + return client; + } + + XnioSsl getXnioSsl() { + return xnioSsl; + } + + /** + * Get the proxy client. + * + * @return the proxy client + */ + public ProxyClient getProxyClient() { + return proxyClient; + } + + Collection getBalancers() { + return Collections.unmodifiableCollection(balancers.values()); + } + + Collection getNodes() { + return Collections.unmodifiableCollection(nodes.values()); + } + + Node getNode(final String jvmRoute) { + return nodes.get(jvmRoute); + } + + /** + * Get the mod cluster proxy target. + * + * @param exchange the http exchange + * @return + */ + public ModClusterProxyTarget findTarget(final HttpServerExchange exchange) { + // There is an option to disable the virtual host check, probably a default virtual host + final PathMatcher.PathMatch entry = mapVirtualHost(exchange); + if (entry == null) { + return null; + } + for (final Balancer balancer : balancers.values()) { + final Map cookies = exchange.getRequestCookies(); + if (balancer.isStickySession()) { + if (cookies.containsKey(balancer.getStickySessionCookie())) { + final String jvmRoute = getJVMRoute(cookies.get(balancer.getStickySessionCookie()).getValue()); + if (jvmRoute != null) { + return new ModClusterProxyTarget.ExistingSessionTarget(jvmRoute, entry.getValue(), this, balancer.isStickySessionForce()); + } + } + if (exchange.getPathParameters().containsKey(balancer.getStickySessionPath())) { + final String id = exchange.getPathParameters().get(balancer.getStickySessionPath()).getFirst(); + final String jvmRoute = getJVMRoute(id); + if (jvmRoute != null) { + return new ModClusterProxyTarget.ExistingSessionTarget(jvmRoute, entry.getValue(), this, balancer.isStickySessionForce()); + } + } + } + } + return new ModClusterProxyTarget.BasicTarget(entry.getValue(), this); + } + + /** + * Register a new node. + * + * @param config the node configuration + * @param balancerConfig the balancer configuration + * @param ioThread the associated I/O thread + * @param bufferPool the buffer pool + * @return whether the node could be created or not + */ + public synchronized boolean addNode(final NodeConfig config, final Balancer.BalancerBuilder balancerConfig, final XnioIoThread ioThread, final Pool bufferPool) { + + final String jvmRoute = config.getJvmRoute(); + final Node existing = nodes.get(jvmRoute); + if (existing != null) { + if (config.getConnectionURI().equals(existing.getNodeConfig().getConnectionURI())) { + // TODO better check if they are the same + existing.resetState(); + return true; + } else { + existing.markRemoved(); + removeNode(existing); + if (!existing.isInErrorState()) { + return false; // replies with MNODERM error + } + } + } + + final String balancerRef = config.getBalancer(); + Balancer balancer = balancers.get(balancerRef); + if (balancer == null) { + // TODO compare balancer configs, if they are not equal log a warning? + balancer = balancerConfig.build(); + balancers.put(balancerRef, balancer); + } + final Node node = new Node(config, balancer, ioThread, bufferPool, this); + nodes.put(jvmRoute, node); + // Remove from the failover groups + failoverDomains.remove(node.getJvmRoute()); + UndertowLogger.ROOT_LOGGER.infof("registering node %s, connection: %s", jvmRoute, config.getConnectionURI()); + return true; + } + + /** + * Management command enabling all contexts on the given node. + * + * @param jvmRoute the jvmRoute + * @return + */ + public synchronized boolean enableNode(final String jvmRoute) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + for (final Context context : node.getContexts()) { + context.enable(); + } + return true; + } + return false; + } + + /** + * Management command disabling all contexts on the given node. + * + * @param jvmRoute the jvmRoute + * @return + */ + public synchronized boolean disableNode(final String jvmRoute) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + for (final Context context : node.getContexts()) { + context.disable(); + } + return true; + } + return false; + } + + /** + * Management command stopping all contexts on the given node. + * + * @param jvmRoute the jvmRoute + * @return + */ + public synchronized boolean stopNode(final String jvmRoute) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + for (final Context context : node.getContexts()) { + context.stop(); + } + return true; + } + return false; + } + + /** + * Remove a node. + * + * @param jvmRoute the jvmRoute + * @return the removed node + */ + public synchronized Node removeNode(final String jvmRoute) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + removeNode(node); + } + return node; + } + + protected synchronized void removeNode(final Node node) { + final String jvmRoute = node.getJvmRoute(); + node.markRemoved(); + if (nodes.remove(jvmRoute, node)) { + UndertowLogger.ROOT_LOGGER.infof("removing node %s", jvmRoute); + node.markRemoved(); + for (final Context context : node.getContexts()) { + removeContext(context.getPath(), node, context.getVirtualHosts()); + } + final String domain = node.getNodeConfig().getDomain(); + if (domain != null) { + failoverDomains.add(node.getJvmRoute(), domain); + } + final String balancerName = node.getBalancer().getName(); + for (final Node other : nodes.values()) { + if (other.getBalancer().getName().equals(balancerName)) { + return; + } + } + balancers.remove(balancerName); + } + } + + /** + * Register a web context. If the web context already exists, just enable it. + * + * @param contextPath the context path + * @param jvmRoute the jvmRoute + * @param aliases the virtual host aliases + */ + public synchronized boolean enableContext(final String contextPath, final String jvmRoute, final List aliases) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + Context context = node.getContext(contextPath, aliases); + if (context == null) { + context = node.registerContext(contextPath, aliases); + UndertowLogger.ROOT_LOGGER.infof("registering context %s, for node %s, with aliases %s", contextPath, jvmRoute, aliases); + for (final String alias : aliases) { + VirtualHost virtualHost = hosts.get(alias); + if (virtualHost == null) { + virtualHost = new VirtualHost(); + hosts.put(alias, virtualHost); + } + virtualHost.registerContext(contextPath, jvmRoute, context); + } + } + context.enable(); + return true; + } + return false; + } + + synchronized boolean disableContext(final String contextPath, final String jvmRoute, List aliases) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + node.disableContext(contextPath, aliases); + return true; + } + return false; + } + + synchronized int stopContext(final String contextPath, final String jvmRoute, List aliases) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + return node.stopContext(contextPath, aliases); + } + return -1; + } + + synchronized boolean removeContext(final String contextPath, final String jvmRoute, List aliases) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + return removeContext(contextPath, node, aliases); + } + return false; + } + + public synchronized boolean removeContext(final String contextPath, final Node node, List aliases) { + if (node == null) { + return false; + } + final String jvmRoute = node.getJvmRoute(); + UndertowLogger.ROOT_LOGGER.infof("unregistering context '%s' from node '%s'", contextPath, jvmRoute); + final Context context = node.removeContext(contextPath, aliases); + if (context == null) { + return false; + } + context.stop(); + for (final String alias : context.getVirtualHosts()) { + final VirtualHost virtualHost = hosts.get(alias); + if (virtualHost != null) { + virtualHost.removeContext(contextPath, jvmRoute, context); + if (virtualHost.isEmpty()) { + hosts.remove(alias); + } + } + } + return true; + } + + /** + * Check the health of all registered nodes + */ + void checkHealth() { + for (final Node node : nodes.values()) { + node.checkHealth(removeBrokenNodesThreshold, healthChecker); + } + } + + /** + * Find a new node handling this request. + * + * @param entry the resolved virtual host entry + * @return the context, {@code null} if not found + */ + Context findNewNode(final VirtualHost.HostEntry entry) { + return electNode(entry.getContexts(), false, null); + } + + /** + * Try to find a failover node within the same load balancing group. + * + * @oaram entry the resolved virtual host entry + * @param domain the load balancing domain, if known + * @param jvmRoute the original jvmRoute + * @return the context, {@code null} if not found + */ + Context findFailoverNode(final VirtualHost.HostEntry entry, final String domain, final String jvmRoute, final boolean forceStickySession) { + String failOverDomain = null; + if (domain == null) { + final Node node = nodes.get(jvmRoute); + if (node != null) { + failOverDomain = node.getNodeConfig().getDomain(); + } + if (failOverDomain == null) { + failOverDomain = failoverDomains.get(jvmRoute); + } + } else { + failOverDomain = domain; + } + final Collection contexts = entry.getContexts(); + if (failOverDomain != null) { + final Context context = electNode(contexts, true, failOverDomain); + if (context != null) { + return context; + } + } + if (forceStickySession) { + return null; + } else { + return electNode(contexts, false, null); + } + } + + /** + * Map a request to virtual host. + * + * @param exchange the http exchange + * @return + */ + private PathMatcher.PathMatch mapVirtualHost(final HttpServerExchange exchange) { + final String hostName = exchange.getRequestHeaders().getFirst(Headers.HOST); + if (hostName != null) { + final String context = exchange.getRelativePath(); + // Remove the port from the host + int i = hostName.indexOf(":"); + VirtualHost host; + if (i > 0) { + host = hosts.get(hostName.substring(0, i)); + if (host == null) { + host = hosts.get(hostName); + } + } else { + host = hosts.get(hostName); + } + if (host == null) { + return null; + } + return host.match(context); + } + return null; + } + + static String getJVMRoute(final String sessionId) { + int index = sessionId.indexOf('.'); + if (index == -1) { + return null; + } + String route = sessionId.substring(index + 1); + index = route.indexOf('.'); + if (index != -1) { + route = route.substring(0, index); + } + return route; + } + + static Context electNode(final Iterable contexts, final boolean existingSession, final String domain) { + Context elected = null; + Node candidate = null; + boolean candidateHotStandby = false; + for (Context context : contexts) { + // Skip disabled contexts + if (context.checkAvailable(existingSession)) { + final Node node = context.getNode(); + final boolean hotStandby = node.isHotStandby(); + // Check that we only failover in the domain + if (domain != null && !domain.equals(node.getNodeConfig().getDomain())) { + continue; + } + if (candidate != null) { + // Check if the nodes are in hot-standby + if (candidateHotStandby) { + if (hotStandby) { + if (candidate.getElectedDiff() > node.getElectedDiff()) { + candidate = node; + elected = context; + } + } else { + candidate = node; + elected = context; + candidateHotStandby = hotStandby; + } + } else if (hotStandby) { + continue; + } else { + // Normal election process + final int lbStatus1 = candidate.getLoadStatus(); + final int lbStatus2 = node.getLoadStatus(); + if (lbStatus1 > lbStatus2) { + candidate = node; + elected = context; + candidateHotStandby = false; + } + } + } else { + candidate = node; + elected = context; + candidateHotStandby = hotStandby; + } + } + } + if (candidate != null) { + candidate.elected(); // We have a winner! + } + return elected; + } + + static long removeThreshold(final long healthChecks, final long removeBrokenNodes) { + if (healthChecks > 0 && removeBrokenNodes > 0) { + final long threshold = removeBrokenNodes / healthChecks; + if (threshold > 1000) { + return 1000; + } else if (threshold < 1) { + return 1; + } else { + return threshold; + } + } else { + return -1; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterProxyClient.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterProxyClient.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterProxyClient.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,134 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import static org.xnio.IoUtils.safeClose; + +import java.util.concurrent.TimeUnit; + +import io.undertow.client.ClientConnection; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.ServerConnection; +import io.undertow.server.handlers.proxy.ExclusivityChecker; +import io.undertow.server.handlers.proxy.ProxyCallback; +import io.undertow.server.handlers.proxy.ProxyClient; +import io.undertow.server.handlers.proxy.ProxyConnection; +import io.undertow.util.AttachmentKey; + +class ModClusterProxyClient implements ProxyClient { + + /** + * The attachment key that is used to attach the proxy connection to the exchange. + *

+ * This cannot be static as otherwise a connection from a different client could be re-used. + */ + private final AttachmentKey exclusiveConnectionKey = AttachmentKey + .create(ExclusiveConnectionHolder.class); + + private final ExclusivityChecker exclusivityChecker; + private final ModClusterContainer container; + + protected ModClusterProxyClient(ExclusivityChecker exclusivityChecker, ModClusterContainer container) { + this.exclusivityChecker = exclusivityChecker; + this.container = container; + } + + @Override + public ProxyTarget findTarget(HttpServerExchange exchange) { + return container.findTarget(exchange); + } + + public void getConnection(final ProxyTarget target, final HttpServerExchange exchange, + final ProxyCallback callback, final long timeout, final TimeUnit timeUnit) { + final ExclusiveConnectionHolder holder = exchange.getConnection().getAttachment(exclusiveConnectionKey); + if (holder != null && holder.connection.getConnection().isOpen()) { + // Something has already caused an exclusive connection to be + // allocated so keep using it. + callback.completed(exchange, holder.connection); + return; + } + if (! (target instanceof ModClusterProxyTarget)) { + callback.couldNotResolveBackend(exchange); + } + + // Resolve the node + final ModClusterProxyTarget proxyTarget = (ModClusterProxyTarget) target; + final Context context = proxyTarget.resolveContext(exchange); + if (context == null) { + callback.couldNotResolveBackend(exchange); + } else { + if (holder != null || (exclusivityChecker != null && exclusivityChecker.isExclusivityRequired(exchange))) { + // If we have a holder, even if the connection was closed we now + // exclusivity was already requested so our client + // may be assuming it still exists. + final ProxyCallback wrappedCallback = new ProxyCallback() { + + @Override + public void completed(HttpServerExchange exchange, ProxyConnection result) { + if (holder != null) { + holder.connection = result; + } else { + final ExclusiveConnectionHolder newHolder = new ExclusiveConnectionHolder(); + newHolder.connection = result; + ServerConnection connection = exchange.getConnection(); + connection.putAttachment(exclusiveConnectionKey, newHolder); + connection.addCloseListener(new ServerConnection.CloseListener() { + + @Override + public void closed(ServerConnection connection) { + ClientConnection clientConnection = newHolder.connection.getConnection(); + if (clientConnection.isOpen()) { + safeClose(clientConnection); + } + } + }); + } + callback.completed(exchange, result); + } + + @Override + public void queuedRequestFailed(HttpServerExchange exchange) { + callback.queuedRequestFailed(exchange); + } + + @Override + public void failed(HttpServerExchange exchange) { + callback.failed(exchange); + } + + @Override + public void couldNotResolveBackend(HttpServerExchange exchange) { + callback.couldNotResolveBackend(exchange); + } + }; + + context.handleRequest(proxyTarget, exchange, wrappedCallback, timeout, timeUnit, true); + } else { + context.handleRequest(proxyTarget, exchange, callback, timeout, timeUnit, true); + } + } + } + + private static class ExclusiveConnectionHolder { + + private ProxyConnection connection; + + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterProxyTarget.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterProxyTarget.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/ModClusterProxyTarget.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,80 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.proxy.ProxyClient; + +/** + * @author Emanuel Muckenhuber + */ +public interface ModClusterProxyTarget extends ProxyClient.ProxyTarget { + + /** + * Resolve the responsible context handling this request. + * + * @param exchange the http server exchange + * @return the context + */ + Context resolveContext(HttpServerExchange exchange); + + class ExistingSessionTarget implements ModClusterProxyTarget { + + private final String jvmRoute; + private final VirtualHost.HostEntry entry; + private final boolean forceStickySession; + private final ModClusterContainer container; + + public ExistingSessionTarget(String jvmRoute, VirtualHost.HostEntry entry, ModClusterContainer container, boolean forceStickySession) { + this.jvmRoute = jvmRoute; + this.entry = entry; + this.container = container; + this.forceStickySession = forceStickySession; + } + + @Override + public Context resolveContext(HttpServerExchange exchange) { + final Context context = entry.getContextForNode(jvmRoute); + if (context != null && context.checkAvailable(true)) { + final Node node = context.getNode(); + node.elected(); // Maybe move this to context#handleRequest + return context; + } + final String domain = context != null ? context.getNode().getNodeConfig().getDomain() : null; + return container.findFailoverNode(entry, domain, jvmRoute, forceStickySession); + } + } + + class BasicTarget implements ModClusterProxyTarget { + + private final VirtualHost.HostEntry entry; + private final ModClusterContainer container; + + public BasicTarget(VirtualHost.HostEntry entry, ModClusterContainer container) { + this.entry = entry; + this.container = container; + } + + @Override + public Context resolveContext(HttpServerExchange exchange) { + return container.findNewNode(entry); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Node.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Node.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/Node.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,520 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.proxy.ConnectionPoolManager; +import io.undertow.server.handlers.proxy.ProxyConnectionPool; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.XnioIoThread; + +/** + * @author Stuart Douglas + * @author Emanuel Muckenhuber + */ +class Node { + + enum Status { + /** + * The node is up + */ + NODE_UP, + /** + * The node is down + */ + NODE_DOWN, + /** + * The node is paused + */ + NODE_HOT_STANDBY; + } + + private final int id; + private final String jvmRoute; + private final ConnectionPoolManager connectionPoolManager; + private final NodeConfig nodeConfig; + private final Balancer balancerConfig; + private final ProxyConnectionPool connectionPool; + private final NodeStats stats = new NodeStats(); + private final NodeLbStatus lbStatus = new NodeLbStatus(); + private final ModClusterContainer container; + private final List vHosts = new CopyOnWriteArrayList<>(); + private final List contexts = new CopyOnWriteArrayList<>(); + + private final XnioIoThread ioThread; + private final Pool bufferPool; + + private volatile int state = ERROR; // This gets cleared with the first status report + + private static final int ERROR = 1 << 31; + private static final int REMOVED = 1 << 30; + private static final int HOT_STANDBY = 1 << 29; + private static final int ACTIVE_PING = 1 << 28; + private static final int ERROR_MASK = (1 << 10) - 1; + + private static final AtomicInteger idGen = new AtomicInteger(); + private static final AtomicIntegerFieldUpdater stateUpdater = AtomicIntegerFieldUpdater.newUpdater(Node.class, "state"); + + protected Node(NodeConfig nodeConfig, Balancer balancerConfig, XnioIoThread ioThread, Pool bufferPool, ModClusterContainer container) { + this.id = idGen.incrementAndGet(); + this.jvmRoute = nodeConfig.getJvmRoute(); + this.nodeConfig = nodeConfig; + this.ioThread = ioThread; + this.bufferPool = bufferPool; + this.balancerConfig = balancerConfig; + this.container = container; + this.connectionPoolManager = new NodeConnectionPoolManager(); + this.connectionPool = new ProxyConnectionPool(connectionPoolManager, nodeConfig.getConnectionURI(), container.getXnioSsl(), container.getClient(), OptionMap.EMPTY); + } + + public int getId() { + return id; + } + + /** + * Get the JVM route. + * + * @return the jvmRoute + */ + public String getJvmRoute() { + return jvmRoute; + } + + public Balancer getBalancer() { + return balancerConfig; + } + + public NodeConfig getNodeConfig() { + return nodeConfig; + } + + public NodeStats getStats() { + return stats; + } + + /** + * Get or create the connection pool for this node. + * + * @return the connection pool + */ + public ProxyConnectionPool getConnectionPool() { + return connectionPool; + } + + public Status getStatus() { + final int status = this.state; + if (anyAreSet(status, ERROR)) { + return Status.NODE_DOWN; + } else if (anyAreSet(status, HOT_STANDBY)) { + return Status.NODE_HOT_STANDBY; + } else { + return Status.NODE_UP; + } + } + + public int getElected() { + return lbStatus.getElected(); + } + + int getElectedDiff() { + return lbStatus.getElectedDiff(); + } + + /** + * Get the load information. Add the error information for clients. + * + * @return the node load + */ + public int getLoad() { + final int status = this.state; + if (anyAreSet(status, ERROR)) { + return -1; + } else if (anyAreSet(status, HOT_STANDBY)) { + return 0; + } else { + return lbStatus.getLbFactor(); + } + } + + /** + * Get the current load status, based on the number of elections and the current load; + * + * @return the load status + */ + public int getLoadStatus() { + return lbStatus.getLbStatus(); + } + + /** + * This node got elected to serve a request! + */ + void elected() { + lbStatus.elected(); + } + + List getVHosts() { + return Collections.unmodifiableList(vHosts); + } + + Collection getContexts() { + return Collections.unmodifiableCollection(contexts); + } + + /** + * Check the health of the node and try to ping it if necessary. + * + * @param threshold the threshold after which the node should be removed + * @param healthChecker the node health checker + */ + protected void checkHealth(long threshold, NodeHealthChecker healthChecker) { + final int state = this.state; + if (anyAreSet(state, REMOVED | ACTIVE_PING)) { + return; + } + if (allAreClear(state, ERROR)) { + if (lbStatus.update()) { + return; + } + } + healthCheckPing(threshold, healthChecker); + } + + void healthCheckPing(final long threshold, NodeHealthChecker healthChecker) { + int oldState, newState; + for (;;) { + oldState = this.state; + if ((oldState & ACTIVE_PING) != 0) { + // There is already a ping active + return; + } + newState = oldState | ACTIVE_PING; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + break; + } + } + + NodePingUtil.internalPingNode(this, new NodePingUtil.PingCallback() { + @Override + public void completed() { + clearActivePing(); + } + + @Override + public void failed() { + try { + if (healthCheckFailed() == threshold) { + container.removeNode(Node.this); + } + } finally { + clearActivePing(); + } + } + }, healthChecker, ioThread, bufferPool, container.getClient(), container.getXnioSsl(), OptionMap.EMPTY); + } + + /** + * Async ping from the user + * + * @param exchange the http server exchange + * @param callback the ping callback + */ + void ping(final HttpServerExchange exchange, final NodePingUtil.PingCallback callback) { + NodePingUtil.pingNode(this, exchange, callback); + } + + /** + * Register a context. + * + * @param path the context path + * @return the created context + */ + Context registerContext(final String path, final List virtualHosts) { + VHostMapping host = null; + for (final VHostMapping vhost : vHosts) { + if (virtualHosts.equals(vhost.getAliases())) { + host = vhost; + break; + } + } + if (host == null) { + host = new VHostMapping(this, virtualHosts); + vHosts.add(host); + } + final Context context = new Context(path, host, this); + contexts.add(context); + return context; + } + + /** + * Get a context. + * + * @param path the context path + * @param aliases the aliases + * @return the context, {@code null} if there is no matching context + */ + Context getContext(final String path, List aliases) { + VHostMapping host = null; + for (final VHostMapping vhost : vHosts) { + if (aliases.equals(vhost.getAliases())) { + host = vhost; + break; + } + } + if (host == null) { + return null; + } + for (final Context context : contexts) { + if (context.getPath().equals(path) && context.getVhost() == host) { + return context; + } + } + return null; + } + + boolean disableContext(final String path, final List aliases) { + final Context context = getContext(path, aliases); + if (context != null) { + context.disable(); + return true; + } + return false; + } + + int stopContext(final String path, final List aliases) { + final Context context = getContext(path, aliases); + if (context != null) { + context.stop(); + return context.getActiveRequests(); + } + return -1; + } + + Context removeContext(final String path, final List aliases) { + final Context context = getContext(path, aliases); + if (context != null) { + context.stop(); + contexts.remove(context); + return context; + } + return null; + } + + protected void updateLoad(final int i) { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState & ~(ERROR | HOT_STANDBY | ERROR_MASK); + if (stateUpdater.compareAndSet(this, oldState, newState)) { + lbStatus.updateLoad(i); + return; + } + } + } + + protected void hotStandby() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState | HOT_STANDBY; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + lbStatus.updateLoad(0); + return; + } + } + } + + protected void markRemoved() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState | REMOVED; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + connectionPool.close(); + return; + } + } + } + + protected void markInError() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState | ERROR; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + UndertowLogger.ROOT_LOGGER.debugf("Node '%s' in error", jvmRoute); + return; + } + } + } + + private void clearActivePing() { + int oldState, newState; + for (;;) { + oldState = this.state; + newState = oldState & ~ACTIVE_PING; + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return; + } + } + } + + /** + * Mark a node in error. Mod_cluster has a threshold after which broken nodes get removed. + * + * @return + */ + private int healthCheckFailed() { + int oldState, newState; + for (;;) { + oldState = this.state; + if ((oldState & ERROR) != ERROR) { + newState = oldState | ERROR; + UndertowLogger.ROOT_LOGGER.debugf("Node '%s' in error", jvmRoute); + } else if ((oldState & ERROR_MASK) == ERROR_MASK) { + return ERROR_MASK; + } else { + newState = oldState +1; + } + if (stateUpdater.compareAndSet(this, oldState, newState)) { + return newState & ERROR_MASK; + } + } + } + + protected void resetState() { + state = ERROR; + lbStatus.updateLoad(0); + } + + protected boolean isInErrorState() { + return (state & ERROR) == ERROR; + } + + boolean isHotStandby() { + return anyAreSet(state, HOT_STANDBY); + } + + protected boolean checkAvailable(final boolean existingSession) { + if (allAreClear(state, ERROR | REMOVED)) { + // Check the state of the queue on the connection pool + final ProxyConnectionPool.AvailabilityType availability = connectionPool.available(); + if (availability == ProxyConnectionPool.AvailabilityType.AVAILABLE) { + return true; + } else if (availability == ProxyConnectionPool.AvailabilityType.FULL) { + if (existingSession) { + return true; + } else if (!existingSession && nodeConfig.isQueueNewRequests()) { + return true; + } + } + } + return false; + } + + private class NodeConnectionPoolManager implements ConnectionPoolManager { + + @Override + public boolean isAvailable() { + return allAreClear(state, ERROR | REMOVED); + } + + @Override + public boolean handleError() { + markInError(); + return false; + } + + @Override + public boolean clearError() { + // This needs to be cleared through the status update + return isAvailable(); + } + + @Override + public int getMaxConnections() { + return nodeConfig.getMaxConnections(); + } + + @Override + public int getMaxCachedConnections() { + return nodeConfig.getCacheConnections(); + } + + @Override + public int getSMaxConnections() { + return nodeConfig.getSmax(); + } + + @Override + public long getTtl() { + return nodeConfig.getTtl(); + } + + @Override + public int getMaxQueueSize() { + return nodeConfig.getRequestQueueSize(); + } + + @Override + public int getProblemServerRetry() { + return -1; // Disable ping from the pool, this is handled through the health-check + } + } + + // Simple host mapping for the mod cluster management protocol + static final AtomicInteger vHostIdGen = new AtomicInteger(); + static class VHostMapping { + + private final int id; + private final List aliases; + private final Node node; + + VHostMapping(Node node, List aliases) { + this.id = vHostIdGen.incrementAndGet(); + this.aliases = aliases; + this.node = node; + } + + public int getId() { + return id; + } + + public List getAliases() { + return aliases; + } + + Node getNode() { + return node; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,339 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.net.URI; +import java.net.URISyntaxException; + +/** + * The node configuration. + * + * @author Nabil Benothman + * @author Emanuel Muckenhuber + */ +public class NodeConfig { + + /** + * The JVM Route. + */ + private final String jvmRoute; + + /** + * The connection URI. + */ + private final URI connectionURI; + + /** + * The balancer configuration to use. + */ + private final String balancer; + + /** + * The failover domain. + */ + private final String domain; + + /** + * Tell how to flush the packets. On: Send immediately, Auto wait for flushwait time before sending, Off don't flush. + * Default: "Off" + */ + private boolean flushPackets; + + /** + * Time to wait before flushing. Value in milliseconds. Default: 10 + */ + private final int flushwait; + + /** + * Time to wait for a pong answer to a ping. 0 means we don't try to ping before sending. Value in seconds Default: 10 + * (10_000 in milliseconds) + */ + private final int ping; + + /** + * max time in seconds to life for connection above smax. Default 60 seconds (60_000 in milliseconds). + */ + private final int ttl; + + /** + * Max time the proxy will wait for the backend connection. Default 0 no timeout value in seconds. + */ + private final int timeout; + + // Proxy connection pool defaults + private final int maxConnections; + private final int cacheConnections; + private final int requestQueueSize; + private final boolean queueNewRequests; + + NodeConfig(NodeBuilder b, final URI connectionURI) { + this.connectionURI = connectionURI; + balancer = b.balancer; + domain = b.domain; + jvmRoute = b.jvmRoute; + flushPackets = b.flushPackets; + flushwait = b.flushwait; + ping = b.ping; + ttl = b.ttl; + timeout = b.timeout; + maxConnections = b.maxConnections; + cacheConnections = b.cacheConnections; + requestQueueSize = b.requestQueueSize; + queueNewRequests = b.queueNewRequests; + } + + /** + * Get the connection URI. + * + * @return the connection URI + */ + public URI getConnectionURI() { + return connectionURI; + } + + /** + * Getter for domain + * + * @return the domain + */ + public String getDomain() { + return this.domain; + } + + /** + * Getter for flushwait + * + * @return the flushwait + */ + public int getFlushwait() { + return this.flushwait; + } + + /** + * Getter for ping + * + * @return the ping + */ + public int getPing() { + return this.ping; + } + + /** + * Getter for smax + * + * @return the smax + */ + public int getSmax() { + return this.cacheConnections; + } + + /** + * Getter for ttl + * + * @return the ttl + */ + public int getTtl() { + return this.ttl; + } + + /** + * Getter for timeout + * + * @return the timeout + */ + public int getTimeout() { + return this.timeout; + } + + /** + * Getter for balancer + * + * @return the balancer + */ + public String getBalancer() { + return this.balancer; + } + + public boolean isFlushPackets() { + return flushPackets; + } + + public void setFlushPackets(boolean flushPackets) { + this.flushPackets = flushPackets; + } + + public String getJvmRoute() { + return jvmRoute; + } + + /** + * Get the maximum connection limit for a nodes thread-pool. + * + * @return the max connections limit + */ + public int getMaxConnections() { + return maxConnections; + } + + /** + * Get the amount of connections which should be kept alive in the connection pool. + * + * @return the number of cached connections + */ + public int getCacheConnections() { + return cacheConnections; + } + + /** + * Get the max queue size for requests. + * + * @return the queue size for requests + */ + public int getRequestQueueSize() { + return requestQueueSize; + } + + /** + * Flag indicating whether requests without a session can be queued. + * + * @return true if requests without a session id can be queued + */ + public boolean isQueueNewRequests() { + return queueNewRequests; + } + + public static NodeBuilder builder(ModCluster modCluster) { + return new NodeBuilder(modCluster); + } + + public static class NodeBuilder { + + private String jvmRoute; + private String balancer = "mycluster"; + private String domain = null; + + private String type = "http"; + private String hostname; + private int port; + + private boolean flushPackets = false; + private int flushwait = 10; + private int ping = 10000; + + private int maxConnections; + private int cacheConnections; + private int requestQueueSize; + private boolean queueNewRequests = false; + + private int ttl = 60000; + private int timeout = 0; + + NodeBuilder(final ModCluster modCluster) { + this.maxConnections = modCluster.getMaxConnections(); + this.cacheConnections = modCluster.getCacheConnections(); + this.requestQueueSize = modCluster.getRequestQueueSize(); + this.queueNewRequests = modCluster.isQueueNewRequests(); + } + + public NodeBuilder setHostname(String hostname) { + this.hostname = hostname; + return this; + } + + public NodeBuilder setPort(int port) { + this.port = port; + return this; + } + + public NodeBuilder setType(String type) { + this.type = type; + return this; + } + + public NodeBuilder setBalancer(String balancer) { + this.balancer = balancer; + return this; + } + + public NodeBuilder setDomain(String domain) { + this.domain = domain; + return this; + } + + public NodeBuilder setJvmRoute(String jvmRoute) { + this.jvmRoute = jvmRoute; + return this; + } + + public NodeBuilder setFlushPackets(boolean flushPackets) { + this.flushPackets = flushPackets; + return this; + } + + public NodeBuilder setFlushwait(int flushwait) { + this.flushwait = flushwait; + return this; + } + + public NodeBuilder setPing(int ping) { + this.ping = ping; + return this; + } + + public NodeBuilder setSmax(int smax) { + this.cacheConnections = smax; + return this; + } + + public NodeBuilder setMaxConnections(int maxConnections) { + this.maxConnections = maxConnections; + return this; + } + + public NodeBuilder setCacheConnections(int cacheConnections) { + this.cacheConnections = cacheConnections; + return this; + } + + public NodeBuilder setRequestQueueSize(int requestQueueSize) { + this.requestQueueSize = requestQueueSize; + return this; + } + + public NodeBuilder setQueueNewRequests(boolean queueNewRequests) { + this.queueNewRequests = queueNewRequests; + return this; + } + + public NodeBuilder setTtl(int ttl) { + this.ttl = ttl; + return this; + } + + public NodeBuilder setTimeout(int timeout) { + this.timeout = timeout; + return this; + } + + public NodeConfig build() throws URISyntaxException { + final URI uri = new URI(type, null, hostname, port, "/", "", ""); + return new NodeConfig(this, uri); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeHealthChecker.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeHealthChecker.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeHealthChecker.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,57 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import io.undertow.client.ClientResponse; + +/** + * @author Emanuel Muckenhuber + */ +public interface NodeHealthChecker { + + /** + * Check the response of a health check. + * + * @param response the client response + * @return true if the response from the node is healthy + */ + boolean checkResponse(final ClientResponse response); + + /** + * Receiving a response is a success. + */ + NodeHealthChecker NO_CHECK = new NodeHealthChecker() { + @Override + public boolean checkResponse(ClientResponse response) { + return true; + } + }; + + /** + * Check that the response code is 2xx to 3xx. + */ + NodeHealthChecker OK = new NodeHealthChecker() { + @Override + public boolean checkResponse(final ClientResponse response) { + final int code = response.getResponseCode(); + return code >= 200 && code < 400; + } + }; + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeLbStatus.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeLbStatus.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeLbStatus.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +/** + * The load-balancing information of a node. + * + * @author Emanuel Muckenhuber + */ +// move this back to Node +class NodeLbStatus { + + private volatile int oldelected; + private volatile int lbfactor; + private volatile int lbstatus; + private volatile int elected; + + public int getLbFactor() { + return lbfactor; + } + + public int getElected() { + return elected; + } + + synchronized int getElectedDiff() { + return elected - oldelected; + } + + /** + * Update the load balancing status. + * + * @return + */ + synchronized boolean update() { + int elected = this.elected; + int oldelected = this.oldelected; + int lbfactor = this.lbfactor; + if (lbfactor > 0) { + this.lbstatus = ((elected - oldelected) * 1000) / lbfactor; + } + this.oldelected = elected; + return elected != oldelected; // ping if they are equal + } + + synchronized void elected() { + if (elected == Integer.MAX_VALUE) { + oldelected = (elected - oldelected); + elected = 1; + } else { + elected++; + } + } + + void updateLoad(int load) { + lbfactor = load; + } + + /** + * Get the load balancing status. + * + * @return + */ + synchronized int getLbStatus() { + int lbfactor = this.lbfactor; + if (lbfactor > 0) { + return (((elected - oldelected) * 1000) / lbfactor) + lbstatus; + } else { + return -1; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodePingUtil.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodePingUtil.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodePingUtil.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,491 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.concurrent.TimeUnit; + +import io.undertow.client.ClientCallback; +import io.undertow.client.ClientConnection; +import io.undertow.client.ClientExchange; +import io.undertow.client.ClientRequest; +import io.undertow.client.UndertowClient; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.proxy.ProxyCallback; +import io.undertow.server.handlers.proxy.ProxyConnection; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import io.undertow.util.SameThreadExecutor; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoFuture; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.ssl.XnioSsl; + +/** + * Utilities to ping a remote node. + * + * @author Emanuel Muckenhuber + */ +class NodePingUtil { + + interface PingCallback { + + /** + * Ping completed. + */ + void completed(); + + /** + * Ping failed. + */ + void failed(); + + } + + private static final ClientRequest PING_REQUEST; + + static { + final ClientRequest request = new ClientRequest(); + request.setMethod(Methods.OPTIONS); + request.setPath("*"); + request.getRequestHeaders().add(Headers.USER_AGENT, "mod_cluster ping"); + PING_REQUEST = request; + } + + /** + * Try to open a socket connection to given address. + * + * @param address the socket address + * @param exchange the http servers exchange + * @param callback the ping callback + * @param options the options + */ + static void pingHost(InetSocketAddress address, HttpServerExchange exchange, PingCallback callback, OptionMap options) { + + final XnioIoThread thread = exchange.getIoThread(); + final XnioWorker worker = thread.getWorker(); + final HostPingTask r = new HostPingTask(address, worker, callback, options); + // Schedule timeout task + scheduleCancelTask(exchange.getIoThread(), r, 5, TimeUnit.SECONDS); + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : thread, r); + } + + /** + * Try to ping a server using the undertow client. + * + * @param connection the connection URI + * @param callback the ping callback + * @param exchange the http servers exchange + * @param client the undertow client + * @param xnioSsl the ssl setup + * @param options the options + */ + static void pingHttpClient(URI connection, PingCallback callback, HttpServerExchange exchange, UndertowClient client, XnioSsl xnioSsl, OptionMap options) { + + final XnioIoThread thread = exchange.getIoThread(); + final RequestExchangeListener exchangeListener = new RequestExchangeListener(callback, NodeHealthChecker.NO_CHECK, true); + final Runnable r = new HttpClientPingTask(connection, exchangeListener, thread, client, xnioSsl, exchange.getConnection().getBufferPool(), options); + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : thread, r); + // Schedule timeout task + scheduleCancelTask(exchange.getIoThread(), exchangeListener, 5, TimeUnit.SECONDS); + } + + /** + * Try to ping a node using it's connection pool. + * + * @param node the node + * @param exchange the http servers exchange + * @param callback the ping callback + */ + static void pingNode(final Node node, final HttpServerExchange exchange, final PingCallback callback) { + if (node == null) { + callback.failed(); + return; + } + + final int timeout = node.getNodeConfig().getPing(); + exchange.dispatch(exchange.isInIoThread() ? SameThreadExecutor.INSTANCE : exchange.getIoThread(), new Runnable() { + @Override + public void run() { + node.getConnectionPool().connect(null, exchange, new ProxyCallback() { + @Override + public void completed(final HttpServerExchange exchange, ProxyConnection result) { + final RequestExchangeListener exchangeListener = new RequestExchangeListener(callback, NodeHealthChecker.NO_CHECK, false); + exchange.dispatch(SameThreadExecutor.INSTANCE, new ConnectionPoolPingTask(result, exchangeListener)); + // Schedule timeout task + scheduleCancelTask(exchange.getIoThread(), exchangeListener, timeout, TimeUnit.SECONDS); + } + + @Override + public void failed(HttpServerExchange exchange) { + callback.failed(); + } + + @Override + public void queuedRequestFailed(HttpServerExchange exchange) { + callback.failed(); + } + + @Override + public void couldNotResolveBackend(HttpServerExchange exchange) { + callback.failed(); + } + + }, timeout, TimeUnit.SECONDS, false); + } + }); + } + + /** + * Internally ping a node. This should probably use the connections from the nodes pool, if there are any available. + * + * @param node the node + * @param callback the ping callback + * @param ioThread the xnio i/o thread + * @param bufferPool the xnio buffer pool + * @param client the undertow client + * @param xnioSsl the ssl setup + * @param options the options + */ + static void internalPingNode(Node node, PingCallback callback, NodeHealthChecker healthChecker, XnioIoThread ioThread, Pool bufferPool, UndertowClient client, XnioSsl xnioSsl, OptionMap options) { + + final URI uri = node.getNodeConfig().getConnectionURI(); + final long timeout = node.getNodeConfig().getPing(); + final RequestExchangeListener exchangeListener = new RequestExchangeListener(callback, healthChecker, true); + final HttpClientPingTask r = new HttpClientPingTask(uri, exchangeListener, ioThread, client, xnioSsl, bufferPool, options); + // Schedule timeout task + scheduleCancelTask(ioThread, exchangeListener, timeout, TimeUnit.SECONDS); + ioThread.execute(r); + } + + static class ConnectionPoolPingTask implements Runnable { + + private final RequestExchangeListener exchangeListener; + private final ProxyConnection proxyConnection; + + ConnectionPoolPingTask(ProxyConnection proxyConnection, RequestExchangeListener exchangeListener) { + this.proxyConnection = proxyConnection; + this.exchangeListener = exchangeListener; + } + + @Override + public void run() { + + // TODO AJP has a special ping thing + proxyConnection.getConnection().sendRequest(PING_REQUEST, new ClientCallback() { + @Override + public void completed(final ClientExchange result) { + if (exchangeListener.isDone()) { + IoUtils.safeClose(proxyConnection.getConnection()); + return; + } + exchangeListener.exchange = result; + result.setResponseListener(exchangeListener); + try { + result.getRequestChannel().shutdownWrites(); + if (!result.getRequestChannel().flush()) { + result.getRequestChannel().getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkChannel channel, IOException exception) { + IoUtils.safeClose(proxyConnection.getConnection()); + exchangeListener.taskFailed(); + } + })); + result.getRequestChannel().resumeWrites(); + } + } catch (IOException e) { + IoUtils.safeClose(proxyConnection.getConnection()); + exchangeListener.taskFailed(); + } + } + + @Override + public void failed(IOException e) { + exchangeListener.taskFailed(); + } + }); + } + + } + + static class HostPingTask extends CancellableTask implements Runnable { + + private final InetSocketAddress address; + private final XnioWorker worker; + private final OptionMap options; + + HostPingTask(InetSocketAddress address, XnioWorker worker, PingCallback callback, OptionMap options) { + super(callback); + this.address = address; + this.worker = worker; + this.options = options; + } + + @Override + public void run() { + try { + final IoFuture future = worker.openStreamConnection(address, new ChannelListener() { + @Override + public void handleEvent(StreamConnection channel) { + IoUtils.safeClose(channel); // Close the channel right away + } + }, options); + + future.addNotifier(new IoFuture.HandlingNotifier() { + + @Override + public void handleCancelled(Void attachment) { + cancel(); + } + + @Override + public void handleFailed(IOException exception, Void attachment) { + taskFailed(); + } + + @Override + public void handleDone(StreamConnection data, Void attachment) { + taskCompleted(); + } + }, null); + + } catch (Exception e) { + taskFailed(); + } + } + + } + + static class HttpClientPingTask implements Runnable { + + private final URI connection; + private final XnioIoThread thread; + private final UndertowClient client; + private final XnioSsl xnioSsl; + private final Pool bufferPool; + private final OptionMap options; + private final RequestExchangeListener exchangeListener; + + HttpClientPingTask(URI connection, RequestExchangeListener exchangeListener, XnioIoThread thread, UndertowClient client, XnioSsl xnioSsl, Pool bufferPool, OptionMap options) { + this.connection = connection; + this.thread = thread; + this.client = client; + this.xnioSsl = xnioSsl; + this.bufferPool = bufferPool; + this.options = options; + this.exchangeListener = exchangeListener; + } + + @Override + public void run() { + + // TODO AJP has a special ping thing + client.connect(new ClientCallback() { + @Override + public void completed(final ClientConnection clientConnection) { + if (exchangeListener.isDone()) { + IoUtils.safeClose(clientConnection); + return; + } + clientConnection.sendRequest(PING_REQUEST, new ClientCallback() { + + @Override + public void completed(ClientExchange result) { + exchangeListener.exchange = result; + if (exchangeListener.isDone()) { + return; + } + result.setResponseListener(exchangeListener); + try { + result.getRequestChannel().shutdownWrites(); + if (!result.getRequestChannel().flush()) { + result.getRequestChannel().getWriteSetter().set(ChannelListeners.flushingChannelListener(null, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkChannel channel, IOException exception) { + IoUtils.safeClose(clientConnection); + exchangeListener.taskFailed(); + } + })); + result.getRequestChannel().resumeWrites(); + } + } catch (IOException e) { + IoUtils.safeClose(clientConnection); + exchangeListener.taskFailed(); + } + } + + @Override + public void failed(IOException e) { + exchangeListener.taskFailed(); + IoUtils.safeClose(clientConnection); + } + }); + } + + @Override + public void failed(IOException e) { + exchangeListener.taskFailed(); + } + }, connection, thread, xnioSsl, bufferPool, options); + + } + } + + static class RequestExchangeListener extends CancellableTask implements ClientCallback { + + private ClientExchange exchange; + private final boolean closeConnection; + private final NodeHealthChecker healthChecker; + + RequestExchangeListener(PingCallback callback, NodeHealthChecker healthChecker, boolean closeConnection) { + super(callback); + assert healthChecker != null; + this.closeConnection = closeConnection; + this.healthChecker = healthChecker; + } + + @Override + public void completed(final ClientExchange result) { + if (isDone()) { + IoUtils.safeClose(result.getConnection()); + return; + } + final ChannelListener listener = ChannelListeners.drainListener(Long.MAX_VALUE, new ChannelListener() { + @Override + public void handleEvent(StreamSourceChannel channel) { + try { + if (healthChecker.checkResponse(result.getResponse())) { + taskCompleted(); + } else { + taskFailed(); + } + } finally { + if (closeConnection) { + if (exchange != null) { + IoUtils.safeClose(exchange.getConnection()); + } + } + } + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSourceChannel channel, IOException exception) { + taskFailed(); + if (exception != null) { + IoUtils.safeClose(exchange.getConnection()); + } + } + }); + StreamSourceChannel responseChannel = result.getResponseChannel(); + responseChannel.getReadSetter().set(listener); + responseChannel.resumeReads(); + listener.handleEvent(responseChannel); + } + + @Override + public void failed(IOException e) { + taskFailed(); + if (exchange != null) { + IoUtils.safeClose(exchange.getConnection()); + } + } + } + + static enum State { + WAITING, DONE, CANCELLED; + } + + static class CancellableTask { + + private final PingCallback delegate; + private volatile State state = State.WAITING; + private volatile XnioExecutor.Key cancelKey; + + CancellableTask(PingCallback callback) { + this.delegate = callback; + } + + boolean isDone() { + return state != State.WAITING; + } + + void setCancelKey(XnioExecutor.Key cancelKey) { + if (state == State.WAITING) { + this.cancelKey = cancelKey; + } else { + cancelKey.remove(); + } + } + + void taskCompleted() { + if (state == State.WAITING) { + state = State.DONE; + if (cancelKey != null) { + cancelKey.remove(); + } + delegate.completed(); + } + } + + void taskFailed() { + if (state == State.WAITING) { + state = State.DONE; + if (cancelKey != null) { + cancelKey.remove(); + } + delegate.failed(); + } + } + + void cancel() { + if (state == State.WAITING) { + state = State.CANCELLED; + if (cancelKey != null) { + cancelKey.remove(); + } + delegate.failed(); + } + } + + } + + static void scheduleCancelTask(final XnioIoThread ioThread, final CancellableTask cancellable, final long timeout, final TimeUnit timeUnit ) { + final XnioExecutor.Key key = ioThread.executeAfter(new Runnable() { + @Override + public void run() { + cancellable.cancel(); + } + }, timeout, timeUnit); + cancellable.setCancelKey(key); + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeStats.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeStats.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/NodeStats.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +/** + * @author Emanuel Muckenhuber + */ +class NodeStats { + + int getRead() { + return -1; + } + + int getTransferred() { + return -1; + } + + int getOpenConnections() { + return -1; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/SessionId.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/SessionId.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/SessionId.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.io.Serializable; + +/** + * {@code SessionId} + * + * @author Jean-Frederic Clere + */ +public class SessionId implements Serializable { + + /** + * SessionId + */ + private final String sessionId; + + /** + * JVMRoute + */ + private final String jmvRoute; + + /** + * Date last updated. + */ + private volatile long updateTime; + + public SessionId(String sessionId, String jmvRoute) { + this.sessionId = sessionId; + this.jmvRoute = jmvRoute; + } + + public String getSessionId() { + return sessionId; + } + + public String getJmvRoute() { + return jmvRoute; + } + + public long getUpdateTime() { + return updateTime; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/VirtualHost.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/VirtualHost.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/proxy/mod_cluster/VirtualHost.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,222 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.proxy.mod_cluster; + +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentMap; + +import io.undertow.UndertowMessages; +import io.undertow.util.CopyOnWriteMap; +import io.undertow.util.PathMatcher; + +/** + * The virtual host handler. + * + * @author Emanuel Muckenhuber + */ +public class VirtualHost { + + private static final char PATH_SEPARATOR = '/'; + private static final String STRING_PATH_SEPARATOR = "/"; + + private final HostEntry defaultHandler = new HostEntry(STRING_PATH_SEPARATOR); + private final ConcurrentMap contexts = new CopyOnWriteMap<>(); + + /** + * lengths of all registered contexts + */ + private volatile int[] lengths = {}; + + protected VirtualHost() { + // + } + + /** + * Matches a path against the registered handlers. + * + * @param path The relative path to match + * @return The match match. This will never be null, however if none matched its value field will be + */ + PathMatcher.PathMatch match(String path){ + int length = path.length(); + final int[] lengths = this.lengths; + for (int i = 0; i < lengths.length; ++i) { + int pathLength = lengths[i]; + if (pathLength == length) { + HostEntry next = contexts.get(path); + if (next != null) { + return new PathMatcher.PathMatch<>(path.substring(pathLength), next); + } + } else if (pathLength < length) { + char c = path.charAt(pathLength); + if (c == '/') { + String part = path.substring(0, pathLength); + HostEntry next = contexts.get(part); + if (next != null) { + return new PathMatcher.PathMatch<>(path.substring(pathLength), next); + } + } + } + } + return new PathMatcher.PathMatch<>(path, defaultHandler); + } + + public synchronized void registerContext(final String path, final String jvmRoute, final Context context) { + if (path.isEmpty()) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + + final String normalizedPath = this.normalizeSlashes(path); + if (STRING_PATH_SEPARATOR.equals(normalizedPath)) { + defaultHandler.contexts.put(jvmRoute, context); + return; + } + + boolean rebuild = false; + HostEntry hostEntry = contexts.get(normalizedPath); + if (hostEntry == null) { + rebuild = true; + hostEntry = new HostEntry(normalizedPath); + contexts.put(normalizedPath, hostEntry); + } + assert !hostEntry.contexts.containsKey(jvmRoute); + hostEntry.contexts.put(jvmRoute, context); + if (rebuild) { + buildLengths(); + } + } + + public synchronized void removeContext(final String path, final String jvmRoute, final Context context) { + if (path == null || path.isEmpty()) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + + final String normalizedPath = this.normalizeSlashes(path); + if (STRING_PATH_SEPARATOR.equals(normalizedPath)) { + defaultHandler.contexts.remove(jvmRoute, context); + } + + final HostEntry hostEntry = contexts.get(normalizedPath); + if (hostEntry != null) { + if (hostEntry.contexts.remove(jvmRoute, context)) { + if (hostEntry.contexts.isEmpty()) { + contexts.remove(normalizedPath); + buildLengths(); + } + } + } + } + + boolean isEmpty() { + return contexts.isEmpty() && defaultHandler.contexts.isEmpty(); + } + + private void buildLengths() { + final Set lengths = new TreeSet<>(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return -o1.compareTo(o2); + } + }); + for (String p : contexts.keySet()) { + lengths.add(p.length()); + } + + int[] lengthArray = new int[lengths.size()]; + int pos = 0; + for (int i : lengths) { + lengthArray[pos++] = i; + } + this.lengths = lengthArray; + } + + + /** + * Adds a '/' prefix to the beginning of a path if one isn't present + * and removes trailing slashes if any are present. + * + * @param path the path to normalize + * @return a normalized (with respect to slashes) result + */ + private String normalizeSlashes(final String path) { + // prepare + final StringBuilder builder = new StringBuilder(path); + boolean modified = false; + + // remove all trailing '/'s except the first one + while (builder.length() > 0 && builder.length() != 1 && PATH_SEPARATOR == builder.charAt(builder.length() - 1)) { + builder.deleteCharAt(builder.length() - 1); + modified = true; + } + + // add a slash at the beginning if one isn't present + if (builder.length() == 0 || PATH_SEPARATOR != builder.charAt(0)) { + builder.insert(0, PATH_SEPARATOR); + modified = true; + } + + // only create string when it was modified + if (modified) { + return builder.toString(); + } + + return path; + } + + static class HostEntry { + + // node > context + private final ConcurrentMap contexts = new CopyOnWriteMap<>(); + private final String contextPath; + + HostEntry(String contextPath) { + this.contextPath = contextPath; + } + + protected String getContextPath() { + return contextPath; + } + + /** + * Get a context for a jvmRoute. + * + * @param jvmRoute the jvm route + * @return + */ + protected Context getContextForNode(final String jvmRoute) { + return contexts.get(jvmRoute); + } + + /** + * Get all registered contexts. + * + * @return + */ + protected Collection getContexts() { + return Collections.unmodifiableCollection(contexts.values()); + } + + } + + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/CachedResource.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/CachedResource.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/CachedResource.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,287 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.List; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.cache.DirectBufferCache; +import io.undertow.server.handlers.cache.LimitedBufferSlicePool; +import io.undertow.server.handlers.cache.ResponseCachingSender; +import io.undertow.util.DateUtils; +import io.undertow.util.ETag; +import io.undertow.util.MimeMappings; + +/** + * @author Stuart Douglas + */ +public class CachedResource implements Resource { + + private final CacheKey cacheKey; + private final CachingResourceManager cachingResourceManager; + private final Resource underlyingResource; + private final boolean directory; + private final Date lastModifiedDate; + private final String lastModifiedDateString; + private final ETag eTag; + private final String name; + private volatile long nextMaxAgeCheck; + + public CachedResource(final CachingResourceManager cachingResourceManager, final Resource underlyingResource, final String path) { + this.cachingResourceManager = cachingResourceManager; + this.underlyingResource = underlyingResource; + this.directory = underlyingResource.isDirectory(); + this.lastModifiedDate = underlyingResource.getLastModified(); + if (lastModifiedDate != null) { + this.lastModifiedDateString = DateUtils.toDateString(lastModifiedDate); + } else { + this.lastModifiedDateString = null; + } + this.eTag = underlyingResource.getETag(); + this.name = underlyingResource.getName(); + this.cacheKey = new CacheKey(cachingResourceManager, underlyingResource.getCacheKey()); + if (cachingResourceManager.getMaxAge() > 0) { + nextMaxAgeCheck = System.currentTimeMillis() + cachingResourceManager.getMaxAge(); + } else { + nextMaxAgeCheck = -1; + } + } + + @Override + public String getPath() { + return underlyingResource.getPath(); + } + + @Override + public Date getLastModified() { + return lastModifiedDate; + } + + @Override + public String getLastModifiedString() { + return lastModifiedDateString; + } + + @Override + public ETag getETag() { + return eTag; + } + + @Override + public String getName() { + return name; + } + + @Override + public boolean isDirectory() { + return directory; + } + + @Override + public List list() { + return underlyingResource.list(); + } + + @Override + public String getContentType(final MimeMappings mimeMappings) { + return underlyingResource.getContentType(mimeMappings); + } + + public void invalidate() { + final DirectBufferCache dataCache = cachingResourceManager.getDataCache(); + if(dataCache != null) { + dataCache.remove(cacheKey); + } + } + + public boolean checkStillValid() { + if (nextMaxAgeCheck > 0) { + long time = System.currentTimeMillis(); + if (time > nextMaxAgeCheck) { + nextMaxAgeCheck = time + cachingResourceManager.getMaxAge(); + if (!underlyingResource.getLastModified().equals(lastModifiedDate)) { + return false; + } + } + } + return true; + } + + @Override + public void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback completionCallback) { + final DirectBufferCache dataCache = cachingResourceManager.getDataCache(); + if(dataCache == null) { + underlyingResource.serve(sender, exchange, completionCallback); + return; + } + + final DirectBufferCache.CacheEntry existing = dataCache.get(cacheKey); + final Long length = getContentLength(); + //if it is not eligible to be served from the cache + if (length == null || length > cachingResourceManager.getMaxFileSize()) { + underlyingResource.serve(sender, exchange, completionCallback); + return; + } + //it is not cached yet, install a wrapper to grab the data + if (existing == null || !existing.enabled() || !existing.reference()) { + Sender newSender = sender; + + final DirectBufferCache.CacheEntry entry; + if (existing == null) { + entry = dataCache.add(cacheKey, length.intValue(), cachingResourceManager.getMaxAge()); + } else { + entry = existing; + } + + if (entry != null && entry.buffers().length != 0 && entry.claimEnable()) { + if (entry.reference()) { + newSender = new ResponseCachingSender(sender, entry, length); + } else { + entry.disable(); + } + } + underlyingResource.serve(newSender, exchange, completionCallback); + } else { + //serve straight from the cache + ByteBuffer[] buffers; + boolean ok = false; + try { + LimitedBufferSlicePool.PooledByteBuffer[] pooled = existing.buffers(); + buffers = new ByteBuffer[pooled.length]; + for (int i = 0; i < buffers.length; i++) { + // Keep position from mutating + buffers[i] = pooled[i].getResource().duplicate(); + } + ok = true; + } finally { + if (!ok) { + existing.dereference(); + } + } + sender.send(buffers, new DereferenceCallback(existing, completionCallback)); + } + } + + @Override + public Long getContentLength() { + //we always use the underlying size unless the data is cached in the buffer cache + //to prevent a mis-match between size on disk and cached size + final DirectBufferCache dataCache = cachingResourceManager.getDataCache(); + if(dataCache == null) { + return underlyingResource.getContentLength(); + } + final DirectBufferCache.CacheEntry existing = dataCache.get(cacheKey); + if(existing == null || !existing.enabled()) { + return underlyingResource.getContentLength(); + } + //we only return the + return (long)existing.size(); + } + + @Override + public String getCacheKey() { + return cacheKey.cacheKey; + } + + @Override + public File getFile() { + return underlyingResource.getFile(); + } + + @Override + public File getResourceManagerRoot() { + return underlyingResource.getResourceManagerRoot(); + } + + @Override + public URL getUrl() { + return underlyingResource.getUrl(); + } + + + private static class DereferenceCallback implements IoCallback { + + private final DirectBufferCache.CacheEntry cache; + private final IoCallback callback; + + public DereferenceCallback(DirectBufferCache.CacheEntry cache, final IoCallback callback) { + this.cache = cache; + this.callback = callback; + } + + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + try { + cache.dereference(); + } finally { + callback.onComplete(exchange, sender); + } + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + try { + cache.dereference(); + } finally { + callback.onException(exchange, sender, exception); + } + } + } + + + static final class CacheKey { + final CachingResourceManager manager; + final String cacheKey; + + CacheKey(CachingResourceManager manager, String cacheKey) { + this.manager = manager; + this.cacheKey = cacheKey; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + CacheKey cacheKey1 = (CacheKey) o; + + if (cacheKey != null ? !cacheKey.equals(cacheKey1.cacheKey) : cacheKey1.cacheKey != null) return false; + if (manager != null ? !manager.equals(cacheKey1.manager) : cacheKey1.manager != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = manager != null ? manager.hashCode() : 0; + result = 31 * result + (cacheKey != null ? cacheKey.hashCode() : 0); + return result; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/CachingResourceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/CachingResourceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/CachingResourceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,186 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.IOException; +import java.util.Collection; +import java.util.Set; + +import io.undertow.UndertowLogger; +import io.undertow.server.handlers.cache.DirectBufferCache; +import io.undertow.server.handlers.cache.LRUCache; + +/** + * @author Stuart Douglas + */ +public class CachingResourceManager implements ResourceManager { + + /** + * The biggest file size we cache + */ + private final long maxFileSize; + + /** + * The underlying resource manager + */ + private final ResourceManager underlyingResourceManager; + + /** + * A cache of byte buffers + */ + private final DirectBufferCache dataCache; + + /** + * A cache of file metadata, such as if a file exists or not + */ + private final LRUCache cache; + + private final int maxAge; + + public CachingResourceManager(final int metadataCacheSize, final long maxFileSize, final DirectBufferCache dataCache, final ResourceManager underlyingResourceManager, final int maxAge) { + this.maxFileSize = maxFileSize; + this.underlyingResourceManager = underlyingResourceManager; + this.dataCache = dataCache; + this.cache = new LRUCache<>(metadataCacheSize, maxAge); + this.maxAge = maxAge; + if(underlyingResourceManager.isResourceChangeListenerSupported()) { + try { + underlyingResourceManager.registerResourceChangeListener(new ResourceChangeListener() { + @Override + public void handleChanges(Collection changes) { + for(ResourceChangeEvent change : changes) { + invalidate(change.getResource()); + } + } + }); + } catch (Exception e) { + UndertowLogger.ROOT_LOGGER.couldNotRegisterChangeListener(e); + } + } + } + + @Override + public CachedResource getResource(final String path) throws IOException { + Object res = cache.get(path); + if (res instanceof NoResourceMarker) { + NoResourceMarker marker = (NoResourceMarker) res; + long nextCheck = marker.getNextCheckTime(); + if(nextCheck > 0) { + long time = System.currentTimeMillis(); + if(time > nextCheck) { + marker.setNextCheckTime(time + maxAge); + if(underlyingResourceManager.getResource(path) != null) { + cache.remove(path); + } else { + return null; + } + } else { + return null; + } + } else { + return null; + } + } else if (res != null) { + CachedResource resource = (CachedResource) res; + if (resource.checkStillValid()) { + return resource; + } else { + invalidate(path); + } + } + final Resource underlying = underlyingResourceManager.getResource(path); + if (underlying == null) { + cache.add(path, new NoResourceMarker(maxAge > 0 ? System.currentTimeMillis() + maxAge : -1)); + return null; + } + final CachedResource resource = new CachedResource(this, underlying, path); + cache.add(path, resource); + return resource; + } + + @Override + public boolean isResourceChangeListenerSupported() { + return underlyingResourceManager.isResourceChangeListenerSupported(); + } + + @Override + public void registerResourceChangeListener(ResourceChangeListener listener) { + underlyingResourceManager.registerResourceChangeListener(listener); + } + + @Override + public void removeResourceChangeListener(ResourceChangeListener listener) { + underlyingResourceManager.removeResourceChangeListener(listener); + } + + public void invalidate(final String path) { + Object entry = cache.remove(path); + if (entry instanceof CachedResource) { + ((CachedResource) entry).invalidate(); + } + } + + DirectBufferCache getDataCache() { + return dataCache; + } + + public long getMaxFileSize() { + return maxFileSize; + } + + public int getMaxAge() { + return maxAge; + } + + @Override + public void close() throws IOException { + try { + //clear all cached data on close + if(dataCache != null) { + Set keys = dataCache.getAllKeys(); + for(final Object key : keys) { + if(key instanceof CachedResource.CacheKey) { + if(((CachedResource.CacheKey) key).manager == this) { + dataCache.remove(key); + } + } + } + } + } finally { + underlyingResourceManager.close(); + } + } + + private static final class NoResourceMarker { + + volatile long nextCheckTime; + + private NoResourceMarker(long nextCheckTime) { + this.nextCheckTime = nextCheckTime; + } + + public long getNextCheckTime() { + return nextCheckTime; + } + + public void setNextCheckTime(long nextCheckTime) { + this.nextCheckTime = nextCheckTime; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ClassPathResourceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ClassPathResourceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ClassPathResourceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,94 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import io.undertow.UndertowMessages; + +import java.io.IOException; +import java.net.URL; + +/** + * @author Stuart Douglas + */ +public class ClassPathResourceManager implements ResourceManager { + + /** + * The class loader that is used to load resources + */ + private final ClassLoader classLoader; + /** + * The prefix that is appended to resources that are to be loaded. + */ + private final String prefix; + + public ClassPathResourceManager(final ClassLoader loader, final Package p) { + this(loader, p.getName().replace(".", "/")); + } + + public ClassPathResourceManager(final ClassLoader classLoader, final String prefix) { + this.classLoader = classLoader; + if (prefix.equals("")) { + this.prefix = ""; + } else if (prefix.endsWith("/")) { + this.prefix = prefix; + } else { + this.prefix = prefix + "/"; + } + } + + public ClassPathResourceManager(final ClassLoader classLoader) { + this(classLoader, ""); + } + + @Override + public Resource getResource(final String path) throws IOException { + String modPath = path; + if(modPath.startsWith("/")) { + modPath = path.substring(1); + } + final String realPath = prefix + modPath; + final URL resource = classLoader.getResource(realPath); + if(resource == null) { + return null; + } else { + return new URLResource(resource, resource.openConnection(), path); + } + + } + + @Override + public boolean isResourceChangeListenerSupported() { + return false; + } + + @Override + public void registerResourceChangeListener(ResourceChangeListener listener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported(); + } + + @Override + public void removeResourceChangeListener(ResourceChangeListener listener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported(); + } + + + @Override + public void close() throws IOException { + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/DirectoryUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/DirectoryUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/DirectoryUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,358 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.text.SimpleDateFormat; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import io.undertow.util.RedirectBuilder; +import org.xnio.channels.Channels; + +/** + * @author Stuart Douglas + */ +public class DirectoryUtils { + + /** + * Serve static resource for the directory listing + * + * @param exchange The exchange + * @return true if resources were served + */ + public static boolean sendRequestedBlobs(HttpServerExchange exchange) { + ByteBuffer buffer = null; + String type = null; + if ("css".equals(exchange.getQueryString())) { + buffer = Blobs.FILE_CSS_BUFFER.duplicate(); + type = "text/css"; + } else if ("js".equals(exchange.getQueryString())) { + buffer = Blobs.FILE_JS_BUFFER.duplicate(); + type = "application/javascript"; + } + + if (buffer != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, String.valueOf(buffer.limit())); + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, type); + if (Methods.HEAD.equals(exchange.getRequestMethod())) { + exchange.endExchange(); + return true; + } + exchange.getResponseSender().send(buffer); + + return true; + } + + return false; + } + + public static StringBuilder renderDirectoryListing(String path, Resource resource) { + if (!path.endsWith("/")){ + path += "/"; + } + StringBuilder builder = new StringBuilder(); + builder.append("\n\n\n") + .append("\n\n"); + builder.append("\n\n\n"); + builder.append("\n") + .append("\n\n") + .append("\n\n\n\n"); + + int state = 0; + String parent = null; + for (int i = path.length() - 1; i >= 0; i--) { + if (state == 1) { + if (path.charAt(i) == '/') { + state = 2; + } + } else if (path.charAt(i) != '/') { + if (state == 2) { + parent = path.substring(0, i + 1); + break; + } + state = 1; + } + } + + SimpleDateFormat format = new SimpleDateFormat("MMM dd, yyyy HH:mm:ss"); + int i = 0; + if (parent != null) { + i++; + builder.append("\n"); + } + + for (Resource entry : resource.list()) { + builder.append("\n"); + } + builder.append("\n
Directory Listing - ").append(path).append("
NameLast ModifiedSize
Powered by Undertow
[..]"); + builder.append(format.format(resource.getLastModified())).append("--
").append(entry.getName()).append(""); + builder.append(format.format(entry.getLastModified())).append(""); + if (entry.isDirectory()) { + builder.append("--"); + } else { + formatSize(builder, entry.getContentLength()); + } + builder.append("
\n\n"); + + return builder; + + } + + public static void renderDirectoryListing(HttpServerExchange exchange, Resource resource) { + String requestPath = exchange.getRequestPath(); + if (! requestPath.endsWith("/")) { + exchange.setResponseCode(302); + exchange.getResponseHeaders().put(Headers.LOCATION, RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true)); + exchange.endExchange(); + return; + } + String resolvedPath = exchange.getResolvedPath(); + + StringBuilder builder = renderDirectoryListing(requestPath, resource); + + try { + ByteBuffer output = ByteBuffer.wrap(builder.toString().getBytes("UTF-8")); + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "text/html"); + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, String.valueOf(output.limit())); + Channels.writeBlocking(exchange.getResponseChannel(), output); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(e); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + exchange.setResponseCode(500); + } + + exchange.endExchange(); + } + + + private static StringBuilder formatSize(StringBuilder builder, Long size) { + if(size == null) { + builder.append("???"); + return builder; + } + int n = 1024 * 1024 * 1024; + int type = 0; + while (size < n && n >= 1024) { + n /= 1024; + type++; + } + + long top = (size * 100) / n; + long bottom = top % 100; + top /= 100; + + builder.append(top); + if (bottom > 0) { + builder.append(".").append(bottom / 10); + bottom %= 10; + if (bottom > 0) { + builder.append(bottom); + } + + } + + switch (type) { + case 0: builder.append(" GB"); break; + case 1: builder.append(" MB"); break; + case 2: builder.append(" KB"); break; + } + + return builder; + } + + + + private DirectoryUtils() { + + } + + /** + * Constant Content + * + * @author Jason T. Greene + */ + public static class Blobs { + public static final String FILE_JS="function growit() {\n" + + " var table = document.getElementById(\"thetable\");\n" + + "\n" + + " var i = table.rows.length - 1;\n" + + " while (i-- > 0) {\n" + + " if (table.rows[i].id == \"eraseme\") {\n" + + " table.deleteRow(i);\n" + + " } else {\n" + + " break;\n" + + " }\n" + + " }\n" + + " table.style.height=\"\";\n" + + " var i = 0;\n" + + " while (table.offsetHeight < window.innerHeight - 24) {\n" + + " i++;\n" + + " var tbody = table.tBodies[0];\n" + + " var row = tbody.insertRow(tbody.rows.length);\n" + + " row.id=\"eraseme\";\n" + + " var cell = row.insertCell(0);\n" + + " if (table.rows.length % 2 != 0) {\n" + + " row.className=\"even eveninvis\";\n" + + " } else {\n" + + " row.className=\"odd oddinvis\";\n" + + " }\n" + + "\n" + + " cell.colSpan=3;\n" + + " cell.appendChild(document.createTextNode(\"i\"));\n" + + " }\n" + + " table.style.height=\"100%\";\n" + + " if (i > 0) {\n" + + " document.documentElement.style.overflowY=\"hidden\";\n" + + " } else {\n" + + " document.documentElement.style.overflowY=\"auto\";\n" + + " }\n" + + "}"; + public static final String FILE_CSS = + "body {\n" + + " font-family: \"Lucida Grande\", \"Lucida Sans Unicode\", \"Trebuchet MS\", Helvetica, Arial, Verdana, sans-serif;\n" + + " margin: 5px;\n" + + "}\n" + + "\n" + + "th.loc {\n" + + " background-image: linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" + + " background-image: -o-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" + + " background-image: -moz-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" + + " background-image: -webkit-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" + + " background-image: -ms-linear-gradient(bottom, rgb(153,151,153) 8%, rgb(199,199,199) 54%);\n" + + " \n" + + " background-image: -webkit-gradient(\n" + + " linear,\n" + + " left bottom,\n" + + " left top,\n" + + " color-stop(0.08, rgb(153,151,153)),\n" + + " color-stop(0.54, rgb(199,199,199))\n" + + " );\n" + + " color: black;\n" + + " padding: 2px;\n" + + " font-weight: normal;\n" + + " border: solid 1px;\n" + + " font-size: 150%;\n" + + " text-align: left;\n" + + "}\n" + + "\n" + + "th.label {\n" + + " border: solid 1px;\n" + + " text-align: left;\n" + + " padding: 4px;\n" + + " padding-left: 8px;\n" + + " font-weight: normal;\n" + + " font-size: small;\n" + + " background-color: #e8e8e8;\n" + + "}\n" + + "\n" + + "th.offset {\n" + + " padding-left: 32px;\n" + + "}\n" + + "\n" + + "th.footer {\n" + + " font-size: 75%;\n" + + " text-align: right;\n" + + "}\n" + + "\n" + + "a.icon {\n" + + " padding-left: 24px;\n" + + " text-decoration: none;\n" + + " color: black;\n" + + "}\n" + + "\n" + + "a.icon:hover {\n" + + " text-decoration: underline;\n" + + "}\n" + + "\n" + + "table {\n" + + " border: 1px solid;\n" + + " border-spacing: 0px;\n" + + " width: 100%;\n" + + " border-collapse: collapse;\n" + + "}\n" + + "\n" + + "tr.odd {\n" + + " background-color: #f3f6fa;\n" + + "}\n" + + "\n" + + "tr.odd td {\n" + + " padding: 2px;\n" + + " padding-left: 8px;\n" + + " font-size: smaller;\n" + + "}\n" + + "\n" + + "tr.even {\n" + + " background-color: #ffffff;\n" + + "}\n" + + "\n" + + "tr.even td {\n" + + " padding: 2px;\n" + + " padding-left: 8px;\n" + + " font-size: smaller;\n" + + "}\n" + + "\n" + + "tr.eveninvis td {\n" + + " color: #ffffff;\n" + + "}\n" + + "\n" + + "tr.oddinvis td {\n" + + " color: #f3f6fa\n" + + "}\n" + + "\n" + + "a.up {\n" + + " background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABI0lEQVQ4y2P4//8/Ay7sM4nhPwjjUwMm0ua//Y+M0+e//QrSGDAfgvEZAjdgydHXcAzTXLjWDoxhhqBbhGLA1N0vwBhdM7ohMHVwA8yrzn4zLj/936j8FE7N6IaA1IL0gPQy2DVc+rnp3FeCmtENAekB6WXw7Lz1tWD5x/+wEIdhdI3o8iA9IL0MYZMfvq9a9+V/w+avcIzLAGQ1ID0gvQxJc56/aNn29X/vnm9wjMsAZDWtQD0gvQwFy94+6N37/f/Moz/gGJcByGpAekB6GarXf7427ciP/0vP/YRjdP/CMLIakB6QXobKDd9PN+769b91P2kYpAekl2HJhb8r11/583/9ZRIxUM+8U783MQCBGBDXAHEbibgGrBdfTiMGU2wAAPz+nxp+TnhDAAAAAElFTkSuQmCC') left center no-repeat; background-size: 16px 16px;\n" + + "}\n" + + "\n" + + "a.dir {\n" + + " background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXZwQWcAAAAQAAAAEABcxq3DAAAA+UlEQVQ4jWP4//8/AyUYTKTNf/sfGafPf/s1be47G5IMWHL0NRxP2f3mbcaCtz/RDUbHKAZM3f2CJAw3wLzq7Dfj8tP/jcpPkYRBekB6GewaLv3cdO7r/y0XSMMgPSC9DJ6dt74WLP/4v3TVZ5IwSA9IL0PY5Ifvq9Z9+d+w+StJGKQHpJchac7zFy3bvv7v3fONJNwK1APSy5C/7O2D3r3f/888+oMkDNID0stQvf7ztWlHfvxfeu4nSRikB6SXoXLD99ONu379b91PGgbpAellWHLh38r1V/78X3+ZRAzUM/fUr00MQCAGxDVA3EYirgHrpUpupAQDAPs+7c1tGDnPAAAAAElFTkSuQmCC') left center no-repeat; background-size: 16px 16px;\n" + + "}\n" + + "\n" + + "a.file {\n" + + " background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXZwQWcAAAAQAAAAEABcxq3DAAABM0lEQVQ4y5WSTW6DMBCF3xvzc4wuOEIO0kVAuUB7vJ4g3KBdoHSRROomEpusUaoAcaYLfmKoqVRLIxnJ7/M3YwJVBcknACv8b+1U9SvoP1bXa/3WNDVIAQmQBLsNOEsGQYAwDNcARgDqusbl+wIRA2NkBEyqP0s+kCOAQhhjICJdkaDIJDwEvQAhH+G+SHagWTsi4jHoAWYIOxYDZDjnb8Fn4Akvz6AHcAbx3Tp5ETwI3RwckyVtv4Fr4VEe9qq6bDB5tlnYWou2bWGtRRRF6jdwAm5Za1FVFc7nM0QERVG8A9hPDRaGpapomgZlWSJJEuR5ftpsNq8ADr9amC+SuN/vuN1uIIntdnvKsuwZwKf2wxgBxpjpX+dA4jjW4/H4kabpixt2AbvAmDX+XnsAB509ww+A8mAar+XXgQAAAABJRU5ErkJggg==') left center no-repeat;\n" + + "}"; + + public static final ByteBuffer FILE_CSS_BUFFER; + public static final ByteBuffer FILE_JS_BUFFER; + + static { + try { + byte[] bytes = FILE_CSS.getBytes("US-ASCII"); + FILE_CSS_BUFFER = ByteBuffer.allocateDirect(bytes.length); + FILE_CSS_BUFFER.put(bytes); + FILE_CSS_BUFFER.flip(); + + bytes = FILE_JS.getBytes("US-ASCII"); + FILE_JS_BUFFER = ByteBuffer.allocateDirect(bytes.length); + FILE_JS_BUFFER.put(bytes); + FILE_JS_BUFFER.flip(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/FileResource.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/FileResource.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/FileResource.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,257 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.DateUtils; +import io.undertow.util.ETag; +import io.undertow.util.MimeMappings; +import org.xnio.FileAccess; +import org.xnio.IoUtils; +import org.xnio.Pooled; + +/** + * A file resource + * + * @author Stuart Douglas + */ +public class FileResource implements Resource { + + private final File file; + private final String path; + private final FileResourceManager manager; + + public FileResource(final File file, final FileResourceManager manager, String path) { + this.file = file; + this.path = path; + this.manager = manager; + } + + @Override + public String getPath() { + return path; + } + + @Override + public Date getLastModified() { + return new Date(file.lastModified()); + } + + @Override + public String getLastModifiedString() { + final Date lastModified = getLastModified(); + if (lastModified == null) { + return null; + } + return DateUtils.toDateString(lastModified); + } + + @Override + public ETag getETag() { + return null; + } + + @Override + public String getName() { + return file.getName(); + } + + @Override + public boolean isDirectory() { + return file.isDirectory(); + } + + @Override + public List list() { + final List resources = new ArrayList<>(); + for (String child : file.list()) { + resources.add(new FileResource(new File(this.file, child), manager, path)); + } + return resources; + } + + @Override + public String getContentType(final MimeMappings mimeMappings) { + final String fileName = file.getName(); + int index = fileName.lastIndexOf('.'); + if (index != -1 && index != fileName.length() - 1) { + return mimeMappings.getMimeType(fileName.substring(index + 1)); + } + return null; + } + + @Override + public void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback callback) { + abstract class BaseFileTask implements Runnable { + protected volatile FileChannel fileChannel; + + protected boolean openFile() { + try { + fileChannel = exchange.getConnection().getWorker().getXnio().openFile(file, FileAccess.READ_ONLY); + } catch (FileNotFoundException e) { + exchange.setResponseCode(404); + callback.onException(exchange, sender, e); + return false; + } catch (IOException e) { + exchange.setResponseCode(500); + callback.onException(exchange, sender, e); + return false; + } + return true; + } + } + + class ServerTask extends BaseFileTask implements IoCallback { + + private Pooled pooled; + + @Override + public void run() { + if (fileChannel == null) { + if (!openFile()) { + return; + } + pooled = exchange.getConnection().getBufferPool().allocate(); + } + if (pooled != null) { + ByteBuffer buffer = pooled.getResource(); + try { + buffer.clear(); + int res = fileChannel.read(buffer); + if (res == -1) { + //we are done + pooled.free(); + IoUtils.safeClose(fileChannel); + callback.onComplete(exchange, sender); + return; + } + buffer.flip(); + sender.send(buffer, this); + } catch (IOException e) { + onException(exchange, sender, e); + } + } + + } + + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + } else { + run(); + } + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + if (pooled != null) { + pooled.free(); + pooled = null; + } + IoUtils.safeClose(fileChannel); + if (!exchange.isResponseStarted()) { + exchange.setResponseCode(500); + } + callback.onException(exchange, sender, exception); + } + } + + class TransferTask extends BaseFileTask { + @Override + public void run() { + if (!openFile()) { + return; + } + + sender.transferFrom(fileChannel, new IoCallback() { + @Override + public void onComplete(HttpServerExchange exchange, Sender sender) { + try { + IoUtils.safeClose(fileChannel); + } finally { + callback.onComplete(exchange, sender); + } + } + + @Override + public void onException(HttpServerExchange exchange, Sender sender, IOException exception) { + try { + IoUtils.safeClose(fileChannel); + } finally { + callback.onException(exchange, sender, exception); + } + } + }); + } + } + + BaseFileTask task = manager.getTransferMinSize() > file.length() ? new ServerTask() : new TransferTask(); + if (exchange.isInIoThread()) { + exchange.dispatch(task); + } else { + task.run(); + } + } + + @Override + public Long getContentLength() { + return file.length(); + } + + @Override + public String getCacheKey() { + return file.toString(); + } + + @Override + public File getFile() { + return file; + } + + @Override + public File getResourceManagerRoot() { + return manager.getBase(); + } + + @Override + public URL getUrl() { + try { + return file.toURI().toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/FileResourceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/FileResourceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/FileResourceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,284 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.TreeSet; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import org.xnio.FileChangeCallback; +import org.xnio.FileChangeEvent; +import org.xnio.FileSystemWatcher; +import org.xnio.OptionMap; +import org.xnio.Xnio; + +/** + * Serves files from the file system. + */ +public class FileResourceManager implements ResourceManager { + + private final List listeners = new ArrayList<>(); + + private FileSystemWatcher fileSystemWatcher; + + private volatile String base; + + /** + * Size to use direct FS to network transfer (if supported by OS/JDK) instead of read/write + */ + private final long transferMinSize; + + /** + * Check to validate caseSensitive issues for specific case-insensitive FS. + * @see io.undertow.server.handlers.resource.FileResourceManager#isFileSameCase(java.io.File) + */ + private final boolean caseSensitive; + + /** + * Check to allow follow symbolic links + */ + private final boolean followLinks; + + /** + * Used if followLinks == true. Set of paths valid to follow symbolic links + */ + private final TreeSet safePaths = new TreeSet(); + + public FileResourceManager(final File base, long transferMinSize) { + this(base, transferMinSize, true, false, null); + } + + public FileResourceManager(final File base, long transferMinSize, boolean caseSensitive) { + this(base, transferMinSize, caseSensitive, false, null); + } + + public FileResourceManager(final File base, long transferMinSize, boolean followLinks, final String... safePaths) { + this(base, transferMinSize, true, followLinks, safePaths); + } + + public FileResourceManager(final File base, long transferMinSize, boolean caseSensitive, boolean followLinks, final String... safePaths) { + if (base == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("base"); + } + String basePath = base.getAbsolutePath(); + if (!basePath.endsWith("/")) { + basePath = basePath + '/'; + } + this.base = basePath; + this.transferMinSize = transferMinSize; + this.caseSensitive = caseSensitive; + this.followLinks = followLinks; + if (this.followLinks) { + if (safePaths == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths"); + } + for (final String safePath : safePaths) { + if (safePath == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("safePaths"); + } + } + this.safePaths.addAll(Arrays.asList(safePaths)); + } + } + + public File getBase() { + return new File(base); + } + + public FileResourceManager setBase(final File base) { + if (base == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("base"); + } + String basePath = base.getAbsolutePath(); + if (!basePath.endsWith("/")) { + basePath = basePath + '/'; + } + this.base = basePath; + return this; + } + + public Resource getResource(final String p) { + String path = null; + //base always ends with a / + if (p.startsWith("/")) { + path = p.substring(1); + } else { + path = p; + } + try { + File file = new File(base, path); + if (file.exists()) { + boolean isSymlinkPath = isSymlinkPath(base, file); + if (isSymlinkPath) { + if (this.followLinks && isSymlinkSafe(file)) { + return getFileResource(file, path); + } + } else { + return getFileResource(file, path); + } + } + return null; + } catch (Exception e) { + UndertowLogger.REQUEST_LOGGER.debugf(e, "Invalid path %s"); + return null; + } + } + + @Override + public boolean isResourceChangeListenerSupported() { + return true; + } + + @Override + public synchronized void registerResourceChangeListener(ResourceChangeListener listener) { + listeners.add(listener); + if (fileSystemWatcher == null) { + fileSystemWatcher = Xnio.getInstance().createFileSystemWatcher("Watcher for " + base, OptionMap.EMPTY); + fileSystemWatcher.watchPath(new File(base), new FileChangeCallback() { + @Override + public void handleChanges(Collection changes) { + synchronized (FileResourceManager.this) { + final List events = new ArrayList<>(); + for (FileChangeEvent change : changes) { + if (change.getFile().getAbsolutePath().startsWith(base)) { + String path = change.getFile().getAbsolutePath().substring(base.length()); + events.add(new ResourceChangeEvent(path, ResourceChangeEvent.Type.valueOf(change.getType().name()))); + } + } + for (ResourceChangeListener listener : listeners) { + listener.handleChanges(events); + } + } + } + }); + } + } + + + @Override + public synchronized void removeResourceChangeListener(ResourceChangeListener listener) { + listeners.remove(listener); + } + + public long getTransferMinSize() { + return transferMinSize; + } + + @Override + public synchronized void close() throws IOException { + if (fileSystemWatcher != null) { + fileSystemWatcher.close(); + } + } + + /** + * Returns true is some element of path inside base path is a symlink. + */ + private boolean isSymlinkPath(final String base, final File file) throws IOException { + Path path = file.toPath(); + int nameCount = path.getNameCount(); + File root = new File(base); + Path rootPath = root.toPath(); + int rootCount = rootPath.getNameCount(); + if (nameCount > rootCount) { + File f = root; + for (int i= rootCount; i 0) { + if (safePath.charAt(0) == '/') { + /* + * Absolute path + */ + return safePath.length() > 0 && + canonicalPath.length() >= safePath.length() && + canonicalPath.startsWith(safePath); + } else { + /* + * In relative path we build the path appending to base + */ + String absSafePath = base + '/' + safePath; + File absSafePathFile = new File(absSafePath); + String canonicalSafePath = absSafePathFile.getCanonicalPath(); + return canonicalSafePath.length() > 0 && + canonicalPath.length() >= canonicalSafePath.length() && + canonicalPath.startsWith(canonicalSafePath); + + } + } + } + return false; + } + + /** + * Apply security check for case insensitive file systems. + */ + private FileResource getFileResource(final File file, final String path) throws IOException { + if (this.caseSensitive) { + if (isFileSameCase(file)) { + return new FileResource(file, this, path); + } else { + return null; + } + } else { + return new FileResource(file, this, path); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/Resource.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/Resource.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/Resource.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,117 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.File; +import java.net.URL; +import java.util.Date; +import java.util.List; + +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ETag; +import io.undertow.util.MimeMappings; + +/** + * Representation of a static resource. + * + * @author Stuart Douglas + */ +public interface Resource { + + /** + * + * @return The path from the resource manager root + */ + String getPath(); + + /** + * @return The last modified date of this resource, or null if this cannot be determined + */ + Date getLastModified(); + + /** + * @return A string representation of the last modified date, or null if this cannot be determined + */ + String getLastModifiedString(); + + /** + * @return The resources etags + */ + ETag getETag(); + + /** + * @return The name of the resource + */ + String getName(); + + /** + * @return true if this resource represents a directory + */ + boolean isDirectory(); + + /** + * @return a list of resources in this directory + */ + List list(); + + /** + * Return the resources content type. In most cases this will simply use the provided + * mime mappings, however in some cases the resource may have additional information as + * to the actual content type. + */ + String getContentType(final MimeMappings mimeMappings); + + /** + * Serve the resource, and call the provided callback when complete. + * + * @param sender The sender to use. + * @param exchange The exchange + */ + void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback completionCallback); + + /** + * @return The content length, or null if it is unknown + */ + Long getContentLength(); + + /** + * @return A string that uniquely identifies this resource + */ + String getCacheKey(); + + /** + * @return The underlying file that matches the resource. This may return null if the resource does not map to a file + */ + File getFile(); + + /** + * Returns the resource manager root. If the resource manager has multiple roots then this returns the one that + * is the parent of this resource. + * + * @return a file representing the resource manager root. This may return null if the resource does not map to a file + */ + File getResourceManagerRoot(); + + /** + * @return The URL of the resource + */ + URL getUrl(); +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceChangeEvent.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceChangeEvent.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceChangeEvent.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +/** + * An event that is fired when a resource is modified + * + * @author Stuart Douglas + */ +public class ResourceChangeEvent { + + private final String resource; + private final Type type; + + public ResourceChangeEvent(String resource, Type type) { + this.resource = resource; + this.type = type; + } + + public String getResource() { + return resource; + } + + public Type getType() { + return type; + } + + /** + * Watched file event types. More may be added in the future. + */ + public static enum Type { + /** + * A file was added in a directory. + */ + ADDED, + /** + * A file was removed from a directory. + */ + REMOVED, + /** + * A file was modified in a directory. + */ + MODIFIED, + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceChangeListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceChangeListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceChangeListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,36 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.util.Collection; + +/** + * Listener that is invoked when a resource changes. + * +* @author Stuart Douglas +*/ +public interface ResourceChangeListener { + + /** + * callback that is invoked when resources change. + * @param changes The collection of changes + */ + void handleChanges(final Collection changes); + +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,406 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.predicate.Predicate; +import io.undertow.predicate.Predicates; +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.builder.HandlerBuilder; +import io.undertow.server.handlers.cache.ResponseCache; +import io.undertow.server.handlers.encoding.ContentEncodedResource; +import io.undertow.server.handlers.encoding.ContentEncodedResourceManager; +import io.undertow.util.DateUtils; +import io.undertow.util.ETag; +import io.undertow.util.ETagUtils; +import io.undertow.util.Headers; +import io.undertow.util.Methods; +import io.undertow.util.MimeMappings; +import io.undertow.util.RedirectBuilder; +import io.undertow.util.StatusCodes; + +/** + * @author Stuart Douglas + */ +public class ResourceHandler implements HttpHandler { + + private final List welcomeFiles = new CopyOnWriteArrayList<>(new String[]{"index.html", "index.htm", "default.html", "default.htm"}); + /** + * If directory listing is enabled. + */ + private volatile boolean directoryListingEnabled = false; + /** + * The mime mappings that are used to determine the content type. + */ + private volatile MimeMappings mimeMappings = MimeMappings.DEFAULT; + private volatile Predicate cachable = Predicates.truePredicate(); + private volatile Predicate allowed = Predicates.truePredicate(); + private volatile ResourceManager resourceManager; + /** + * If this is set this will be the maximum time the client will cache the resource. + *

+ * Note: Do not set this for private resources, as it will cause a Cache-Control: public + * to be sent. + *

+ * TODO: make this more flexible + *

+ * This will only be used if the {@link #cachable} predicate returns true + */ + private volatile Integer cacheTime; + /** + * we do not calculate a new expiry date every request. Instead calculate it once + * and cache it until it is in the past. + *

+ * TODO: do we need this policy to be pluggable + */ + private volatile long lastExpiryDate; + private volatile String lastExpiryHeader; + + private volatile ContentEncodedResourceManager contentEncodedResourceManager; + + public ResourceHandler(ResourceManager resourceManager) { + this.resourceManager = resourceManager; + } + + + /** + * You should use {@link ResourceHandler(ResourceManager)} instead. + */ + @Deprecated + public ResourceHandler() { + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if (exchange.getRequestMethod().equals(Methods.GET) || + exchange.getRequestMethod().equals(Methods.POST)) { + serveResource(exchange, true); + } else if (exchange.getRequestMethod().equals(Methods.HEAD)) { + serveResource(exchange, false); + } else { + exchange.setResponseCode(405); + exchange.endExchange(); + } + } + + private void serveResource(final HttpServerExchange exchange, final boolean sendContent) { + + if (DirectoryUtils.sendRequestedBlobs(exchange)) { + return; + } + + if (!allowed.resolve(exchange)) { + exchange.setResponseCode(403); + exchange.endExchange(); + return; + } + + ResponseCache cache = exchange.getAttachment(ResponseCache.ATTACHMENT_KEY); + final boolean cachable = this.cachable.resolve(exchange); + + //we set caching headers before we try and serve from the cache + if (cachable && cacheTime != null) { + exchange.getResponseHeaders().put(Headers.CACHE_CONTROL, "public, max-age=" + cacheTime); + if (System.currentTimeMillis() > lastExpiryDate) { + long date = System.currentTimeMillis(); + lastExpiryHeader = DateUtils.toDateString(new Date(date)); + lastExpiryDate = date; + } + exchange.getResponseHeaders().put(Headers.EXPIRES, lastExpiryHeader); + } + + if (cache != null && cachable) { + if (cache.tryServeResponse()) { + return; + } + } + + + //we now dispatch to a worker thread + //as resource manager methods are potentially blocking + exchange.dispatch(new Runnable() { + @Override + public void run() { + Resource resource = null; + try { + resource = resourceManager.getResource(exchange.getRelativePath()); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + exchange.setResponseCode(500); + exchange.endExchange(); + return; + } + if (resource == null) { + exchange.setResponseCode(404); + exchange.endExchange(); + return; + } + + if (resource.isDirectory()) { + Resource indexResource = null; + try { + indexResource = getIndexFiles(resourceManager, resource.getPath(), welcomeFiles); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + exchange.setResponseCode(500); + exchange.endExchange(); + return; + } + if (indexResource == null) { + if (directoryListingEnabled) { + DirectoryUtils.renderDirectoryListing(exchange, resource); + return; + } else { + exchange.setResponseCode(StatusCodes.FORBIDDEN); + exchange.endExchange(); + return; + } + } else if (!exchange.getRequestPath().endsWith("/")) { + exchange.setResponseCode(302); + exchange.getResponseHeaders().put(Headers.LOCATION, RedirectBuilder.redirect(exchange, exchange.getRelativePath() + "/", true)); + exchange.endExchange(); + return; + } + resource = indexResource; + } + + final ETag etag = resource.getETag(); + final Date lastModified = resource.getLastModified(); + if (!ETagUtils.handleIfMatch(exchange, etag, false) || + !DateUtils.handleIfUnmodifiedSince(exchange, lastModified)) { + exchange.setResponseCode(412); + exchange.endExchange(); + return; + } + if (!ETagUtils.handleIfNoneMatch(exchange, etag, true) || + !DateUtils.handleIfModifiedSince(exchange, lastModified)) { + exchange.setResponseCode(304); + exchange.endExchange(); + return; + } + //todo: handle range requests + //we are going to proceed. Set the appropriate headers + final String contentType = resource.getContentType(mimeMappings); + + if(!exchange.getResponseHeaders().contains(Headers.CONTENT_TYPE)) { + if (contentType != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, contentType); + } else { + exchange.getResponseHeaders().put(Headers.CONTENT_TYPE, "application/octet-stream"); + } + } + if (lastModified != null) { + exchange.getResponseHeaders().put(Headers.LAST_MODIFIED, resource.getLastModifiedString()); + } + if (etag != null) { + exchange.getResponseHeaders().put(Headers.ETAG, etag.toString()); + } + Long contentLength = resource.getContentLength(); + if (contentLength != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, contentLength.toString()); + } + + final ContentEncodedResourceManager contentEncodedResourceManager = ResourceHandler.this.contentEncodedResourceManager; + if (contentEncodedResourceManager != null) { + try { + ContentEncodedResource encoded = contentEncodedResourceManager.getResource(resource, exchange); + if (encoded != null) { + exchange.getResponseHeaders().put(Headers.CONTENT_ENCODING, encoded.getContentEncoding()); + exchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, encoded.getResource().getContentLength()); + encoded.getResource().serve(exchange.getResponseSender(), exchange, IoCallback.END_EXCHANGE); + return; + } + + } catch (IOException e) { + //TODO: should this be fatal + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + exchange.setResponseCode(500); + exchange.endExchange(); + return; + } + } + + if (!sendContent) { + exchange.endExchange(); + } else { + resource.serve(exchange.getResponseSender(), exchange, IoCallback.END_EXCHANGE); + } + } + }); + + + } + + private Resource getIndexFiles(ResourceManager resourceManager, final String base, List possible) throws IOException { + String realBase; + if (base.endsWith("/")) { + realBase = base; + } else { + realBase = base + "/"; + } + for (String possibility : possible) { + Resource index = resourceManager.getResource(realBase + possibility); + if (index != null) { + return index; + } + } + return null; + } + + public boolean isDirectoryListingEnabled() { + return directoryListingEnabled; + } + + public ResourceHandler setDirectoryListingEnabled(final boolean directoryListingEnabled) { + this.directoryListingEnabled = directoryListingEnabled; + return this; + } + + public ResourceHandler addWelcomeFiles(String... files) { + this.welcomeFiles.addAll(Arrays.asList(files)); + return this; + } + + public ResourceHandler setWelcomeFiles(String... files) { + this.welcomeFiles.clear(); + this.welcomeFiles.addAll(Arrays.asList(files)); + return this; + } + + public MimeMappings getMimeMappings() { + return mimeMappings; + } + + public ResourceHandler setMimeMappings(final MimeMappings mimeMappings) { + this.mimeMappings = mimeMappings; + return this; + } + + public Predicate getCachable() { + return cachable; + } + + public ResourceHandler setCachable(final Predicate cachable) { + this.cachable = cachable; + return this; + } + + public Predicate getAllowed() { + return allowed; + } + + public ResourceHandler setAllowed(final Predicate allowed) { + this.allowed = allowed; + return this; + } + + public ResourceManager getResourceManager() { + return resourceManager; + } + + public ResourceHandler setResourceManager(final ResourceManager resourceManager) { + this.resourceManager = resourceManager; + return this; + } + + public Integer getCacheTime() { + return cacheTime; + } + + public ResourceHandler setCacheTime(final Integer cacheTime) { + this.cacheTime = cacheTime; + return this; + } + + public ContentEncodedResourceManager getContentEncodedResourceManager() { + return contentEncodedResourceManager; + } + + public ResourceHandler setContentEncodedResourceManager(ContentEncodedResourceManager contentEncodedResourceManager) { + this.contentEncodedResourceManager = contentEncodedResourceManager; + return this; + } + + + + public static class Builder implements HandlerBuilder { + + @Override + public String name() { + return "resource"; + } + + @Override + public Map> parameters() { + Map> params = new HashMap<>(); + params.put("location", String.class); + params.put("allow-listing", boolean.class); + return params; + } + + @Override + public Set requiredParameters() { + return Collections.singleton("location"); + } + + @Override + public String defaultParameter() { + return "location"; + } + + @Override + public HandlerWrapper build(Map config) { + return new Wrapper((String)config.get("location"), (Boolean) config.get("allow-listing")); + } + + } + + private static class Wrapper implements HandlerWrapper { + + private final String location; + private final boolean allowDirectoryListing; + + private Wrapper(String location, boolean allowDirectoryListing) { + this.location = location; + this.allowDirectoryListing = allowDirectoryListing; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + ResourceManager rm = new FileResourceManager(new File(location), 1024); + ResourceHandler resourceHandler = new ResourceHandler(rm); + resourceHandler.setDirectoryListingEnabled(allowDirectoryListing); + return resourceHandler; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/ResourceManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,89 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import io.undertow.UndertowMessages; + +import java.io.Closeable; +import java.io.IOException; + +/** + * + * Representation of a resource manager. A resource manager knows how to obtain + * a resource for a given path. + * + * @author Stuart Douglas + */ +public interface ResourceManager extends Closeable { + + /** + * Returns a resource for the given path. + * + * It is the responsibility of the called to make sure that the path in Canonicalised. + * + * @param path The path + * @return The resource representing the path, or null if no resource was found. + */ + Resource getResource(final String path) throws IOException; + + /** + * + * @return true if a resource change listener is supported + */ + boolean isResourceChangeListenerSupported(); + + /** + * Registers a resource change listener, if the underlying resource manager support it + * @param listener The listener to register + * @throws IllegalArgumentException If resource change listeners are not supported + */ + void registerResourceChangeListener(final ResourceChangeListener listener); + + /** + * Removes a resource change listener + * @param listener + */ + void removeResourceChangeListener(final ResourceChangeListener listener); + + ResourceManager EMPTY_RESOURCE_MANAGER = new ResourceManager() { + @Override + public Resource getResource(final String path){ + return null; + } + + @Override + public boolean isResourceChangeListenerSupported() { + return false; + } + + @Override + public void registerResourceChangeListener(ResourceChangeListener listener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported(); + } + + @Override + public void removeResourceChangeListener(ResourceChangeListener listener) { + throw UndertowMessages.MESSAGES.resourceChangeListenerNotSupported(); + } + + @Override + public void close() throws IOException { + } + }; +} Index: 3rdParty_sources/undertow/io/undertow/server/handlers/resource/URLResource.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/handlers/resource/URLResource.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/handlers/resource/URLResource.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,220 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.handlers.resource; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.URLConnection; +import java.nio.ByteBuffer; +import java.util.Date; +import java.util.LinkedList; +import java.util.List; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.DateUtils; +import io.undertow.util.ETag; +import io.undertow.util.MimeMappings; +import org.xnio.IoUtils; + +/** + * @author Stuart Douglas + */ +public class URLResource implements Resource { + + private final URL url; + private final URLConnection connection; + private final String path; + + public URLResource(final URL url, final URLConnection connection, String path) { + this.url = url; + this.connection = connection; + this.path = path; + } + + @Override + public String getPath() { + return path; + } + + @Override + public Date getLastModified() { + return new Date(connection.getLastModified()); + } + + @Override + public String getLastModifiedString() { + return DateUtils.toDateString(getLastModified()); + } + + @Override + public ETag getETag() { + return null; + } + + @Override + public String getName() { + String path = url.getPath(); + if (path.endsWith("/")) { + path = path.substring(0, path.length() - 1); + } + int sepIndex = path.lastIndexOf("/"); + if (sepIndex != -1) { + path = path.substring(sepIndex + 1); + } + return path; + } + + @Override + public boolean isDirectory() { + File file = getFile(); + if(file != null) { + return file.isDirectory(); + } else if(url.getPath().endsWith("/")) { + return true; + } + return false; + } + + @Override + public List list() { + List result = new LinkedList<>(); + File file = getFile(); + try { + if (file != null) { + for (File f : file.listFiles()) { + result.add(new URLResource(f.toURI().toURL(), connection, f.getPath())); + } + } + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + return result; + } + + @Override + public String getContentType(final MimeMappings mimeMappings) { + final String fileName = getName(); + int index = fileName.lastIndexOf('.'); + if (index != -1 && index != fileName.length() - 1) { + return mimeMappings.getMimeType(fileName.substring(index + 1)); + } + return null; + } + + @Override + public void serve(final Sender sender, final HttpServerExchange exchange, final IoCallback completionCallback) { + + class ServerTask implements Runnable, IoCallback { + + private InputStream inputStream; + private byte[] buffer; + + @Override + public void run() { + if (inputStream == null) { + try { + inputStream = url.openStream(); + } catch (IOException e) { + exchange.setResponseCode(500); + return; + } + buffer = new byte[1024];//TODO: we should be pooling these + } + try { + int res = inputStream.read(buffer); + if (res == -1) { + //we are done, just return + IoUtils.safeClose(inputStream); + completionCallback.onComplete(exchange, sender); + return; + } + sender.send(ByteBuffer.wrap(buffer, 0, res), this); + } catch (IOException e) { + onException(exchange, sender, e); + } + + } + + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + if (exchange.isInIoThread()) { + exchange.dispatch(this); + } else { + run(); + } + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + IoUtils.safeClose(inputStream); + if (!exchange.isResponseStarted()) { + exchange.setResponseCode(500); + } + completionCallback.onException(exchange, sender, exception); + } + } + + ServerTask serveTask = new ServerTask(); + if (exchange.isInIoThread()) { + exchange.dispatch(serveTask); + } else { + serveTask.run(); + } + } + + @Override + public Long getContentLength() { + return (long) connection.getContentLength(); + } + + @Override + public String getCacheKey() { + return url.toString(); + } + + @Override + public File getFile() { + if(url.getProtocol().equals("file")) { + try { + return new File(url.toURI()); + } catch (URISyntaxException e) { + return null; + } + } + return null; + } + + @Override + public File getResourceManagerRoot() { + return null; + } + + @Override + public URL getUrl() { + return url; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AbstractAjpParseState.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AbstractAjpParseState.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AbstractAjpParseState.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,54 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +/** + * Abstract AJP parse state. Stores state common to both request and response parsers + * + * + * @author Stuart Douglas + */ +public class AbstractAjpParseState { + + /** + * The length of the string being read + */ + public int stringLength = -1; + + /** + * The current string being read + */ + public StringBuilder currentString; + + /** + * when reading the first byte of an integer this stores the first value. It is set to -1 to signify that + * the first byte has not been read yet. + */ + public int currentIntegerPart = -1; + + boolean containsUrlCharacters = false; + public int readHeaders = 0; + + public void reset() { + stringLength = -1; + currentString = null; + currentIntegerPart = -1; + readHeaders = 0; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AbstractAjpParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AbstractAjpParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AbstractAjpParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,145 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import io.undertow.util.HttpString; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +public abstract class AbstractAjpParser { + + public static final int STRING_LENGTH_MASK = 1 << 31; + + protected IntegerHolder parse16BitInteger(ByteBuffer buf, AbstractAjpParseState state) { + if (!buf.hasRemaining()) { + return new IntegerHolder(-1, false); + } + int number = state.currentIntegerPart; + if (number == -1) { + number = (buf.get() & 0xFF); + } + if (buf.hasRemaining()) { + final byte b = buf.get(); + int result = ((0xFF & number) << 8) + (b & 0xFF); + state.currentIntegerPart = -1; + return new IntegerHolder(result, true); + } else { + state.currentIntegerPart = number; + return new IntegerHolder(-1, false); + } + } + + protected StringHolder parseString(ByteBuffer buf, AbstractAjpParseState state, boolean header) { + boolean containsUrlCharacters = state.containsUrlCharacters; + if (!buf.hasRemaining()) { + return new StringHolder(null, false, false); + } + int stringLength = state.stringLength; + if (stringLength == -1) { + int number = buf.get() & 0xFF; + if (buf.hasRemaining()) { + final byte b = buf.get(); + stringLength = ((0xFF & number) << 8) + (b & 0xFF); + } else { + state.stringLength = number | STRING_LENGTH_MASK; + return new StringHolder(null, false, false); + } + } else if ((stringLength & STRING_LENGTH_MASK) != 0) { + int number = stringLength & ~STRING_LENGTH_MASK; + stringLength = ((0xFF & number) << 8) + (buf.get() & 0xFF); + } + if (header && (stringLength & 0xFF00) != 0) { + state.stringLength = -1; + return new StringHolder(headers(stringLength & 0xFF)); + } + if (stringLength == 0xFFFF) { + //OxFFFF means null + state.stringLength = -1; + return new StringHolder(null, true, false); + } + StringBuilder builder = state.currentString; + + if (builder == null) { + builder = new StringBuilder(); + state.currentString = builder; + } + int length = builder.length(); + while (length < stringLength) { + if (!buf.hasRemaining()) { + state.stringLength = stringLength; + state.containsUrlCharacters = containsUrlCharacters; + return new StringHolder(null, false, false); + } + char c = (char) buf.get(); + if(c == '+' || c == '%') { + containsUrlCharacters = true; + } + builder.append(c); + ++length; + } + + if (buf.hasRemaining()) { + buf.get(); //null terminator + state.currentString = null; + state.stringLength = -1; + state.containsUrlCharacters = false; + return new StringHolder(builder.toString(), true, containsUrlCharacters); + } else { + state.stringLength = stringLength; + state.containsUrlCharacters = containsUrlCharacters; + return new StringHolder(null, false, false); + } + } + + protected abstract HttpString headers(int offset); + + protected static class IntegerHolder { + public final int value; + public final boolean readComplete; + + private IntegerHolder(int value, boolean readComplete) { + this.value = value; + this.readComplete = readComplete; + } + } + + protected static class StringHolder { + public final String value; + public final HttpString header; + public final boolean readComplete; + public final boolean containsUrlCharacters; + + private StringHolder(String value, boolean readComplete, boolean containsUrlCharacters) { + this.value = value; + this.readComplete = readComplete; + this.containsUrlCharacters = containsUrlCharacters; + this.header = null; + } + + private StringHolder(HttpString value) { + this.value = null; + this.readComplete = true; + this.header = value; + this.containsUrlCharacters = false; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpOpenListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpOpenListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpOpenListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,143 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.conduits.ReadTimeoutStreamSourceConduit; +import io.undertow.conduits.WriteTimeoutStreamSinkConduit; +import io.undertow.server.HttpHandler; +import io.undertow.server.OpenListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.Options; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static io.undertow.UndertowOptions.DECODE_URL; +import static io.undertow.UndertowOptions.URL_CHARSET; + +/** + * @author Stuart Douglas + */ +public class AjpOpenListener implements OpenListener { + + public static final String UTF_8 = "UTF-8"; + private final Pool bufferPool; + private final int bufferSize; + + private volatile String scheme; + + private volatile HttpHandler rootHandler; + + private volatile OptionMap undertowOptions; + + private final AjpRequestParser parser; + + public AjpOpenListener(final Pool pool, final int bufferSize) { + this(pool, OptionMap.EMPTY, bufferSize); + } + + public AjpOpenListener(final Pool pool, final OptionMap undertowOptions, final int bufferSize) { + this.undertowOptions = undertowOptions; + this.bufferPool = pool; + this.bufferSize = bufferSize; + parser = new AjpRequestParser(undertowOptions.get(URL_CHARSET, UTF_8), undertowOptions.get(DECODE_URL, true)); + } + + @Override + public void handleEvent(final StreamConnection channel) { + if (UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) { + UndertowLogger.REQUEST_LOGGER.tracef("Opened connection with %s", channel.getPeerAddress()); + } + + //set read and write timeouts + try { + Integer readTimeout = channel.getOption(Options.READ_TIMEOUT); + Integer idleTimeout = undertowOptions.get(UndertowOptions.IDLE_TIMEOUT); + if ((readTimeout == null || readTimeout <= 0) && idleTimeout != null) { + readTimeout = idleTimeout; + } else if (readTimeout != null && idleTimeout != null && idleTimeout > 0) { + readTimeout = Math.min(readTimeout, idleTimeout); + } + if (readTimeout != null && readTimeout > 0) { + channel.getSourceChannel().setConduit(new ReadTimeoutStreamSourceConduit(channel.getSourceChannel().getConduit(), channel, this)); + } + Integer writeTimeout = channel.getOption(Options.WRITE_TIMEOUT); + if ((writeTimeout == null || writeTimeout <= 0) && idleTimeout != null) { + writeTimeout = idleTimeout; + } else if (writeTimeout != null && idleTimeout != null && idleTimeout > 0) { + writeTimeout = Math.min(writeTimeout, idleTimeout); + } + if (writeTimeout != null && writeTimeout > 0) { + channel.getSinkChannel().setConduit(new WriteTimeoutStreamSinkConduit(channel.getSinkChannel().getConduit(), channel, this)); + } + } catch (IOException e) { + IoUtils.safeClose(channel); + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + } + + AjpServerConnection connection = new AjpServerConnection(channel, bufferPool, rootHandler, undertowOptions, bufferSize); + AjpReadListener readListener = new AjpReadListener(connection, scheme, parser); + connection.setAjpReadListener(readListener); + readListener.startRequest(); + channel.getSourceChannel().setReadListener(readListener); + readListener.handleEvent(channel.getSourceChannel()); + } + + @Override + public HttpHandler getRootHandler() { + return rootHandler; + } + + @Override + public void setRootHandler(final HttpHandler rootHandler) { + this.rootHandler = rootHandler; + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public void setUndertowOptions(final OptionMap undertowOptions) { + if (undertowOptions == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("undertowOptions"); + } + this.undertowOptions = undertowOptions; + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } + + public String getScheme() { + return scheme; + } + + public void setScheme(final String scheme) { + this.scheme = scheme; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpReadListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpReadListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpReadListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,310 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.conduits.ConduitListener; +import io.undertow.conduits.EmptyStreamSourceConduit; +import io.undertow.conduits.ReadDataStreamSourceConduit; +import io.undertow.server.AbstractServerConnection; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import org.xnio.ChannelListener; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSourceConduit; +import org.xnio.conduits.WriteReadyHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import static org.xnio.IoUtils.safeClose; + +/** + * @author Stuart Douglas + */ + +final class AjpReadListener implements ChannelListener { + + private static final byte[] CPONG = {'A', 'B', 0, 1, 9}; //CPONG response data + + private final AjpServerConnection connection; + private final String scheme; + private final boolean recordRequestStartTime; + private AjpRequestParseState state = new AjpRequestParseState(); + private HttpServerExchange httpServerExchange; + + private volatile int read = 0; + private final int maxRequestSize; + private final long maxEntitySize; + private final AjpRequestParser parser; + private WriteReadyHandler.ChannelListenerHandler writeReadyHandler; + + + AjpReadListener(final AjpServerConnection connection, final String scheme, AjpRequestParser parser) { + this.connection = connection; + this.scheme = scheme; + this.parser = parser; + this.maxRequestSize = connection.getUndertowOptions().get(UndertowOptions.MAX_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_HEADER_SIZE); + this.maxEntitySize = connection.getUndertowOptions().get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); + this.writeReadyHandler = new WriteReadyHandler.ChannelListenerHandler<>(connection.getChannel().getSinkChannel()); + this.recordRequestStartTime = connection.getUndertowOptions().get(UndertowOptions.RECORD_REQUEST_START_TIME, false); + } + + public void startRequest() { + connection.resetChannel(); + state = new AjpRequestParseState(); + httpServerExchange = new HttpServerExchange(connection, maxEntitySize); + read = 0; + } + + public void handleEvent(final StreamSourceChannel channel) { + if(connection.getOriginalSinkConduit().isWriteShutdown() || connection.getOriginalSourceConduit().isReadShutdown()) { + safeClose(connection); + channel.suspendReads(); + return; + } + Pooled existing = connection.getExtraBytes(); + + final Pooled pooled = existing == null ? connection.getBufferPool().allocate() : existing; + final ByteBuffer buffer = pooled.getResource(); + boolean free = true; + + try { + int res; + do { + if (existing == null) { + buffer.clear(); + try { + res = channel.read(buffer); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + safeClose(channel); + return; + } + } else { + res = buffer.remaining(); + } + if (res == 0) { + if (!channel.isReadResumed()) { + channel.getReadSetter().set(this); + channel.resumeReads(); + } + return; + } + if (res == -1) { + try { + channel.shutdownReads(); + final StreamSinkChannel responseChannel = connection.getChannel().getSinkChannel(); + responseChannel.shutdownWrites(); + safeClose(connection); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + // fuck it, it's all ruined + safeClose(connection); + return; + } + return; + } + //TODO: we need to handle parse errors + if (existing != null) { + existing = null; + connection.setExtraBytes(null); + } else { + buffer.flip(); + } + int begin = buffer.remaining(); + parser.parse(buffer, state, httpServerExchange); + read += begin - buffer.remaining(); + if (buffer.hasRemaining()) { + free = false; + connection.setExtraBytes(pooled); + } + if (read > maxRequestSize) { + UndertowLogger.REQUEST_LOGGER.requestHeaderWasTooLarge(connection.getPeerAddress(), maxRequestSize); + safeClose(connection); + return; + } + } while (!state.isComplete()); + + + if (state.prefix != AjpRequestParser.FORWARD_REQUEST) { + if (state.prefix == AjpRequestParser.CPING) { + UndertowLogger.REQUEST_LOGGER.debug("Received CPING, sending CPONG"); + handleCPing(); + } else if (state.prefix == AjpRequestParser.CPONG) { + UndertowLogger.REQUEST_LOGGER.debug("Received CPONG, starting next request"); + state = new AjpRequestParseState(); + channel.getReadSetter().set(this); + channel.resumeReads(); + } else { + UndertowLogger.REQUEST_LOGGER.ignoringAjpRequestWithPrefixCode(state.prefix); + safeClose(connection); + } + return; + } + + // we remove ourselves as the read listener from the channel; + // if the http handler doesn't set any then reads will suspend, which is the right thing to do + channel.getReadSetter().set(null); + channel.suspendReads(); + + final HttpServerExchange httpServerExchange = this.httpServerExchange; + final AjpServerResponseConduit responseConduit = new AjpServerResponseConduit(connection.getChannel().getSinkChannel().getConduit(), connection.getBufferPool(), httpServerExchange, new ConduitListener() { + @Override + public void handleEvent(AjpServerResponseConduit channel) { + Connectors.terminateResponse(httpServerExchange); + } + }, httpServerExchange.getRequestMethod().equals(Methods.HEAD)); + connection.getChannel().getSinkChannel().setConduit(responseConduit); + connection.getChannel().getSourceChannel().setConduit(createSourceConduit(connection.getChannel().getSourceChannel().getConduit(), responseConduit, httpServerExchange)); + //we need to set the write ready handler. This allows the response conduit to wrap it + responseConduit.setWriteReadyHandler(writeReadyHandler); + + try { + connection.setSSLSessionInfo(state.createSslSessionInfo()); + httpServerExchange.setSourceAddress(state.createPeerAddress()); + httpServerExchange.setDestinationAddress(state.createDestinationAddress()); + if(scheme != null) { + httpServerExchange.setRequestScheme(scheme); + } + state = null; + this.httpServerExchange = null; + httpServerExchange.setPersistent(true); + + if(recordRequestStartTime) { + Connectors.setRequestStartTime(httpServerExchange); + } + connection.setCurrentExchange(httpServerExchange); + Connectors.executeRootHandler(connection.getRootHandler(), httpServerExchange); + + } catch (Throwable t) { + //TODO: we should attempt to return a 500 status code in this situation + UndertowLogger.REQUEST_LOGGER.exceptionProcessingRequest(t); + safeClose(channel); + safeClose(connection); + } + } catch (Exception e) { + UndertowLogger.REQUEST_LOGGER.exceptionProcessingRequest(e); + safeClose(connection.getChannel()); + } finally { + if (free) pooled.free(); + } + } + + private void handleCPing() { + state = new AjpRequestParseState(); + final StreamConnection underlyingChannel = connection.getChannel(); + underlyingChannel.getSourceChannel().suspendReads(); + final ByteBuffer buffer = ByteBuffer.wrap(CPONG); + int res; + try { + do { + res = underlyingChannel.getSinkChannel().write(buffer); + if (res == 0) { + underlyingChannel.getSinkChannel().setWriteListener(new ChannelListener() { + @Override + public void handleEvent(ConduitStreamSinkChannel channel) { + int res; + do { + try { + res = channel.write(buffer); + if (res == 0) { + return; + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + safeClose(connection); + } + } while (buffer.hasRemaining()); + channel.suspendWrites(); + AjpReadListener.this.handleEvent(underlyingChannel.getSourceChannel()); + } + }); + underlyingChannel.getSinkChannel().resumeWrites(); + return; + } + } while (buffer.hasRemaining()); + AjpReadListener.this.handleEvent(underlyingChannel.getSourceChannel()); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + safeClose(connection); + } + } + + public void exchangeComplete(final HttpServerExchange exchange) { + if (!exchange.isUpgrade() && exchange.isPersistent()) { + startRequest(); + ConduitStreamSourceChannel channel = ((AjpServerConnection) exchange.getConnection()).getChannel().getSourceChannel(); + channel.getReadSetter().set(this); + channel.wakeupReads(); + } else if(!exchange.isPersistent()) { + safeClose(exchange.getConnection()); + } + } + + private StreamSourceConduit createSourceConduit(StreamSourceConduit underlyingConduit, AjpServerResponseConduit responseConduit, final HttpServerExchange exchange) { + + ReadDataStreamSourceConduit conduit = new ReadDataStreamSourceConduit(underlyingConduit, (AbstractServerConnection) exchange.getConnection()); + + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + HttpString transferEncoding = Headers.IDENTITY; + Long length; + final String teHeader = requestHeaders.getLast(Headers.TRANSFER_ENCODING); + boolean hasTransferEncoding = teHeader != null; + if (hasTransferEncoding) { + transferEncoding = new HttpString(teHeader); + } + final String requestContentLength = requestHeaders.getFirst(Headers.CONTENT_LENGTH); + if (hasTransferEncoding && !transferEncoding.equals(Headers.IDENTITY)) { + length = null; //unknown length + } else if (requestContentLength != null) { + final long contentLength = Long.parseLong(requestContentLength); + if (contentLength == 0L) { + UndertowLogger.REQUEST_LOGGER.trace("No content, starting next request"); + // no content - immediately start the next request, returning an empty stream for this one + Connectors.terminateRequest(httpServerExchange); + return new EmptyStreamSourceConduit(conduit.getReadThread()); + } else { + length = contentLength; + } + } else { + UndertowLogger.REQUEST_LOGGER.trace("No content length or transfer coding, starting next request"); + // no content - immediately start the next request, returning an empty stream for this one + Connectors.terminateRequest(exchange); + return new EmptyStreamSourceConduit(conduit.getReadThread()); + } + return new AjpServerRequestConduit(conduit, exchange, responseConduit, length, new ConduitListener() { + @Override + public void handleEvent(AjpServerRequestConduit channel) { + Connectors.terminateRequest(exchange); + } + }); + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpRequestParseState.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpRequestParseState.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpRequestParseState.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,120 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.security.cert.CertificateException; +import java.util.HashMap; +import java.util.Map; + +import io.undertow.server.BasicSSLSessionInfo; +import io.undertow.util.HttpString; + +/** + * @author Stuart Douglas + */ +class AjpRequestParseState extends AbstractAjpParseState { + + //states + public static final int BEGIN = 0; + public static final int READING_MAGIC_NUMBER = 1; + public static final int READING_DATA_SIZE = 2; + public static final int READING_PREFIX_CODE = 3; + public static final int READING_METHOD = 4; + public static final int READING_PROTOCOL = 5; + public static final int READING_REQUEST_URI = 6; + public static final int READING_REMOTE_ADDR = 7; + public static final int READING_REMOTE_HOST = 8; + public static final int READING_SERVER_NAME = 9; + public static final int READING_SERVER_PORT = 10; + public static final int READING_IS_SSL = 11; + public static final int READING_NUM_HEADERS = 12; + public static final int READING_HEADERS = 13; + public static final int READING_ATTRIBUTES = 14; + public static final int DONE = 15; + public static final String AJP_REMOTE_PORT = "AJP_REMOTE_PORT"; + + int state; + + byte prefix; + + int dataSize; + + int numHeaders = 0; + + HttpString currentHeader; + + String currentAttribute; + + //TODO: can there be more than one attribute? + Map attributes = new HashMap<>(); + + String remoteAddress; + int serverPort = 80; + String serverAddress; + + public boolean isComplete() { + return state == 15; + } + + BasicSSLSessionInfo createSslSessionInfo() { + String sessionId = attributes.get(AjpRequestParser.SSL_SESSION); + String cypher = attributes.get(AjpRequestParser.SSL_CIPHER); + String cert = attributes.get(AjpRequestParser.SSL_CERT); + if (cert == null && sessionId == null) { + return null; + } + try { + return new BasicSSLSessionInfo(sessionId, cypher, cert); + } catch (CertificateException e) { + return null; + } catch (javax.security.cert.CertificateException e) { + return null; + } + } + + InetSocketAddress createPeerAddress() { + if (remoteAddress == null) { + return null; + } + String portString = attributes.get(AJP_REMOTE_PORT); + int port = 0; + if (portString != null) { + try { + port = Integer.parseInt(portString); + } catch (IllegalArgumentException e) { + } + } + try { + InetAddress address = InetAddress.getByName(remoteAddress); + return new InetSocketAddress(address, port); + } catch (UnknownHostException e) { + return null; + } + } + + InetSocketAddress createDestinationAddress() { + if (serverAddress == null) { + return null; + } + return InetSocketAddress.createUnresolved(serverAddress, serverPort); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpRequestParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpRequestParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpRequestParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,416 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import static io.undertow.util.Methods.ACL; +import static io.undertow.util.Methods.BASELINE_CONTROL; +import static io.undertow.util.Methods.CHECKIN; +import static io.undertow.util.Methods.CHECKOUT; +import static io.undertow.util.Methods.COPY; +import static io.undertow.util.Methods.DELETE; +import static io.undertow.util.Methods.GET; +import static io.undertow.util.Methods.HEAD; +import static io.undertow.util.Methods.LABEL; +import static io.undertow.util.Methods.LOCK; +import static io.undertow.util.Methods.MERGE; +import static io.undertow.util.Methods.MKACTIVITY; +import static io.undertow.util.Methods.MKCOL; +import static io.undertow.util.Methods.MKWORKSPACE; +import static io.undertow.util.Methods.MOVE; +import static io.undertow.util.Methods.OPTIONS; +import static io.undertow.util.Methods.POST; +import static io.undertow.util.Methods.PROPFIND; +import static io.undertow.util.Methods.PROPPATCH; +import static io.undertow.util.Methods.PUT; +import static io.undertow.util.Methods.REPORT; +import static io.undertow.util.Methods.SEARCH; +import static io.undertow.util.Methods.TRACE; +import static io.undertow.util.Methods.UNCHECKOUT; +import static io.undertow.util.Methods.UNLOCK; +import static io.undertow.util.Methods.UPDATE; +import static io.undertow.util.Methods.VERSION_CONTROL; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.nio.ByteBuffer; + +import io.undertow.security.impl.ExternalAuthenticationMechanism; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.URLUtils; + +/** + * @author Stuart Douglas + */ +public class AjpRequestParser extends AbstractAjpParser { + + + private final String encoding; + private final boolean doDecode; + + private static final HttpString[] HTTP_HEADERS; + + public static final int FORWARD_REQUEST = 2; + public static final int CPONG = 9; + public static final int CPING = 10; + public static final int SHUTDOWN = 7; + + + private static final HttpString[] HTTP_METHODS; + private static final String[] ATTRIBUTES; + + public static final String QUERY_STRING = "query_string"; + + public static final String SSL_CERT = "ssl_cert"; + + public static final String CONTEXT = "context"; + + public static final String SERVLET_PATH = "servlet_path"; + + public static final String REMOTE_USER = "remote_user"; + + public static final String AUTH_TYPE = "auth_type"; + + public static final String ROUTE = "route"; + + public static final String SSL_CIPHER = "ssl_cipher"; + + public static final String SSL_SESSION = "ssl_session"; + + public static final String REQ_ATTRIBUTE = "req_attribute"; + + public static final String SSL_KEY_SIZE = "ssl_key_size"; + + public static final String SECRET = "secret"; + + public static final String STORED_METHOD = "stored_method"; + + static { + HTTP_METHODS = new HttpString[28]; + HTTP_METHODS[1] = OPTIONS; + HTTP_METHODS[2] = GET; + HTTP_METHODS[3] = HEAD; + HTTP_METHODS[4] = POST; + HTTP_METHODS[5] = PUT; + HTTP_METHODS[6] = DELETE; + HTTP_METHODS[7] = TRACE; + HTTP_METHODS[8] = PROPFIND; + HTTP_METHODS[9] = PROPPATCH; + HTTP_METHODS[10] = MKCOL; + HTTP_METHODS[11] = COPY; + HTTP_METHODS[12] = MOVE; + HTTP_METHODS[13] = LOCK; + HTTP_METHODS[14] = UNLOCK; + HTTP_METHODS[15] = ACL; + HTTP_METHODS[16] = REPORT; + HTTP_METHODS[17] = VERSION_CONTROL; + HTTP_METHODS[18] = CHECKIN; + HTTP_METHODS[19] = CHECKOUT; + HTTP_METHODS[20] = UNCHECKOUT; + HTTP_METHODS[21] = SEARCH; + HTTP_METHODS[22] = MKWORKSPACE; + HTTP_METHODS[23] = UPDATE; + HTTP_METHODS[24] = LABEL; + HTTP_METHODS[25] = MERGE; + HTTP_METHODS[26] = BASELINE_CONTROL; + HTTP_METHODS[27] = MKACTIVITY; + + HTTP_HEADERS = new HttpString[0xF]; + HTTP_HEADERS[1] = Headers.ACCEPT; + HTTP_HEADERS[2] = Headers.ACCEPT_CHARSET; + HTTP_HEADERS[3] = Headers.ACCEPT_ENCODING; + HTTP_HEADERS[4] = Headers.ACCEPT_LANGUAGE; + HTTP_HEADERS[5] = Headers.AUTHORIZATION; + HTTP_HEADERS[6] = Headers.CONNECTION; + HTTP_HEADERS[7] = Headers.CONTENT_TYPE; + HTTP_HEADERS[8] = Headers.CONTENT_LENGTH; + HTTP_HEADERS[9] = Headers.COOKIE; + HTTP_HEADERS[0xA] = Headers.COOKIE2; + HTTP_HEADERS[0xB] = Headers.HOST; + HTTP_HEADERS[0xC] = Headers.PRAGMA; + HTTP_HEADERS[0xD] = Headers.REFERER; + HTTP_HEADERS[0xE] = Headers.USER_AGENT; + + ATTRIBUTES = new String[0xE]; + ATTRIBUTES[1] = CONTEXT; + ATTRIBUTES[2] = SERVLET_PATH; + ATTRIBUTES[3] = REMOTE_USER; + ATTRIBUTES[4] = AUTH_TYPE; + ATTRIBUTES[5] = QUERY_STRING; + ATTRIBUTES[6] = ROUTE; + ATTRIBUTES[7] = SSL_CERT; + ATTRIBUTES[8] = SSL_CIPHER; + ATTRIBUTES[9] = SSL_SESSION; + ATTRIBUTES[10] = REQ_ATTRIBUTE; + ATTRIBUTES[11] = SSL_KEY_SIZE; + ATTRIBUTES[12] = SECRET; + ATTRIBUTES[13] = STORED_METHOD; + } + + public AjpRequestParser(String encoding, boolean doDecode) { + this.encoding = encoding; + this.doDecode = doDecode; + } + + + public void parse(final ByteBuffer buf, final AjpRequestParseState state, final HttpServerExchange exchange) throws IOException { + if (!buf.hasRemaining()) { + return; + } + switch (state.state) { + case AjpRequestParseState.BEGIN: { + IntegerHolder result = parse16BitInteger(buf, state); + if (!result.readComplete) { + return; + } else { + if (result.value != 0x1234) { + throw new IllegalStateException("Wrong magic number"); + } + } + } + case AjpRequestParseState.READING_DATA_SIZE: { + IntegerHolder result = parse16BitInteger(buf, state); + if (!result.readComplete) { + state.state = AjpRequestParseState.READING_DATA_SIZE; + return; + } else { + state.dataSize = result.value; + } + } + case AjpRequestParseState.READING_PREFIX_CODE: { + if (!buf.hasRemaining()) { + state.state = AjpRequestParseState.READING_PREFIX_CODE; + return; + } else { + final byte prefix = buf.get(); + state.prefix = prefix; + if (prefix != 2) { + state.state = AjpRequestParseState.DONE; + return; + } + } + } + case AjpRequestParseState.READING_METHOD: { + if (!buf.hasRemaining()) { + state.state = AjpRequestParseState.READING_METHOD; + return; + } else { + int method = buf.get(); + if (method > 0 && method < 28) { + exchange.setRequestMethod(HTTP_METHODS[method]); + } else if((method & 0xFF) != 0xFF) { + throw new IllegalArgumentException("Unknown method type " + method); + } + } + } + case AjpRequestParseState.READING_PROTOCOL: { + StringHolder result = parseString(buf, state, false); + if (result.readComplete) { + //TODO: more efficient way of doing this + exchange.setProtocol(HttpString.tryFromString(result.value)); + } else { + state.state = AjpRequestParseState.READING_PROTOCOL; + return; + } + } + case AjpRequestParseState.READING_REQUEST_URI: { + StringHolder result = parseString(buf, state, false); + if (result.readComplete) { + int colon = result.value.indexOf(';'); + if (colon == -1) { + String res = decode(result.value, result.containsUrlCharacters); + exchange.setRequestURI(result.value); + exchange.setRequestPath(res); + exchange.setRelativePath(res); + } else { + final String url = result.value.substring(0, colon); + String res = decode(url, result.containsUrlCharacters); + exchange.setRequestURI(url); + exchange.setRequestPath(res); + exchange.setRelativePath(res); + URLUtils.parsePathParms(result.value.substring(colon + 1), exchange, encoding, doDecode && result.containsUrlCharacters); + } + } else { + state.state = AjpRequestParseState.READING_REQUEST_URI; + return; + } + } + case AjpRequestParseState.READING_REMOTE_ADDR: { + StringHolder result = parseString(buf, state, false); + if (result.readComplete) { + state.remoteAddress = result.value; + } else { + state.state = AjpRequestParseState.READING_REMOTE_ADDR; + return; + } + } + case AjpRequestParseState.READING_REMOTE_HOST: { + StringHolder result = parseString(buf, state, false); + if (result.readComplete) { + //exchange.setRequestURI(result.value); + } else { + state.state = AjpRequestParseState.READING_REMOTE_HOST; + return; + } + } + case AjpRequestParseState.READING_SERVER_NAME: { + StringHolder result = parseString(buf, state, false); + if (result.readComplete) { + state.serverAddress = result.value; + } else { + state.state = AjpRequestParseState.READING_SERVER_NAME; + return; + } + } + case AjpRequestParseState.READING_SERVER_PORT: { + IntegerHolder result = parse16BitInteger(buf, state); + if (result.readComplete) { + state.serverPort = result.value; + } else { + state.state = AjpRequestParseState.READING_SERVER_PORT; + return; + } + } + case AjpRequestParseState.READING_IS_SSL: { + if (!buf.hasRemaining()) { + state.state = AjpRequestParseState.READING_IS_SSL; + return; + } else { + final byte isSsl = buf.get(); + if (isSsl != 0) { + exchange.setRequestScheme("https"); + } else { + exchange.setRequestScheme("http"); + } + } + } + case AjpRequestParseState.READING_NUM_HEADERS: { + IntegerHolder result = parse16BitInteger(buf, state); + if (!result.readComplete) { + state.state = AjpRequestParseState.READING_NUM_HEADERS; + return; + } else { + state.numHeaders = result.value; + } + } + case AjpRequestParseState.READING_HEADERS: { + int readHeaders = state.readHeaders; + while (readHeaders < state.numHeaders) { + if (state.currentHeader == null) { + StringHolder result = parseString(buf, state, true); + if (!result.readComplete) { + state.state = AjpRequestParseState.READING_HEADERS; + state.readHeaders = readHeaders; + return; + } + if (result.header != null) { + state.currentHeader = result.header; + } else { + state.currentHeader = HttpString.tryFromString(result.value); + } + } + StringHolder result = parseString(buf, state, false); + if (!result.readComplete) { + state.state = AjpRequestParseState.READING_HEADERS; + state.readHeaders = readHeaders; + return; + } + exchange.getRequestHeaders().add(state.currentHeader, result.value); + state.currentHeader = null; + ++readHeaders; + } + } + case AjpRequestParseState.READING_ATTRIBUTES: { + for (; ; ) { + if (state.currentAttribute == null && state.currentIntegerPart == -1) { + if (!buf.hasRemaining()) { + state.state = AjpRequestParseState.READING_ATTRIBUTES; + return; + } + int val = (0xFF & buf.get()); + if (val == 0xFF) { + state.state = AjpRequestParseState.DONE; + return; + } else if (val == 0x0A) { + //we need to read the name. We overload currentIntegerPart to avoid adding another state field + state.currentIntegerPart = 1; + } else { + state.currentAttribute = ATTRIBUTES[val]; + } + } + if (state.currentIntegerPart == 1) { + StringHolder result = parseString(buf, state, false); + if (!result.readComplete) { + state.state = AjpRequestParseState.READING_ATTRIBUTES; + return; + } + state.currentAttribute = result.value; + state.currentIntegerPart = -1; + } + String result; + if (state.currentAttribute.equals(SSL_KEY_SIZE)) { + IntegerHolder resultHolder = parse16BitInteger(buf, state); + if (!resultHolder.readComplete) { + state.state = AjpRequestParseState.READING_ATTRIBUTES; + return; + } + result = Integer.toString(resultHolder.value); + } else { + StringHolder resultHolder = parseString(buf, state, false); + if (!resultHolder.readComplete) { + state.state = AjpRequestParseState.READING_ATTRIBUTES; + return; + } + result = resultHolder.value; + } + //query string. + if (state.currentAttribute.equals(QUERY_STRING)) { + exchange.setQueryString(result == null ? "" : result); + URLUtils.parseQueryString(result, exchange, encoding, doDecode); + } else if (state.currentAttribute.equals(REMOTE_USER)) { + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_PRINCIPAL, result); + } else if (state.currentAttribute.equals(AUTH_TYPE)) { + exchange.putAttachment(ExternalAuthenticationMechanism.EXTERNAL_AUTHENTICATION_TYPE, result); + } else if (state.currentAttribute.equals(STORED_METHOD)) { + exchange.setRequestMethod(new HttpString(result)); + } else { + //other attributes + state.attributes.put(state.currentAttribute, result); + } + state.currentAttribute = null; + } + } + } + state.state = AjpRequestParseState.DONE; + } + + private String decode(String url, final boolean containsUrlCharacters) throws UnsupportedEncodingException { + if (doDecode && containsUrlCharacters) { + return URLDecoder.decode(url, encoding); + } + return url; + } + + @Override + protected HttpString headers(int offset) { + return HTTP_HEADERS[offset]; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,153 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import io.undertow.UndertowMessages; +import io.undertow.conduits.ReadDataStreamSourceConduit; +import io.undertow.server.AbstractServerConnection; +import io.undertow.server.BasicSSLSessionInfo; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.protocol.http.HttpContinue; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; +import io.undertow.util.DateUtils; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.WriteReadyHandler; + +import java.nio.ByteBuffer; + +/** + * A server-side AJP connection. + *

+ * + * @author David M. Lloyd + */ +public final class AjpServerConnection extends AbstractServerConnection { + private SSLSessionInfo sslSessionInfo; + private WriteReadyHandler.ChannelListenerHandler writeReadyHandler; + private AjpReadListener ajpReadListener; + + public AjpServerConnection(StreamConnection channel, Pool bufferPool, HttpHandler rootHandler, OptionMap undertowOptions, int bufferSize) { + super(channel, bufferPool, rootHandler, undertowOptions, bufferSize); + this.writeReadyHandler = new WriteReadyHandler.ChannelListenerHandler<>(channel.getSinkChannel()); + } + + @Override + public HttpServerExchange sendOutOfBandResponse(HttpServerExchange exchange) { + if (exchange == null || !HttpContinue.requiresContinueResponse(exchange)) { + throw UndertowMessages.MESSAGES.outOfBandResponseOnlyAllowedFor100Continue(); + } + final ConduitState state = resetChannel(); + HttpServerExchange newExchange = new HttpServerExchange(this); + for (HttpString header : exchange.getRequestHeaders().getHeaderNames()) { + newExchange.getRequestHeaders().putAll(header, exchange.getRequestHeaders().get(header)); + } + newExchange.setProtocol(exchange.getProtocol()); + newExchange.setRequestMethod(exchange.getRequestMethod()); + newExchange.setRequestPath(exchange.getRequestPath()); + newExchange.getRequestHeaders().put(Headers.CONNECTION, Headers.KEEP_ALIVE.toString()); + newExchange.getRequestHeaders().put(Headers.CONTENT_LENGTH, 0); + + newExchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + restoreChannel(state); + } + }); + return newExchange; + } + + @Override + public void terminateRequestChannel(HttpServerExchange exchange) { + //todo: terminate + } + + @Override + public void restoreChannel(ConduitState state) { + super.restoreChannel(state); + channel.getSinkChannel().getConduit().setWriteReadyHandler(writeReadyHandler); + } + + @Override + public ConduitState resetChannel() { + ConduitState state = super.resetChannel(); + channel.getSinkChannel().getConduit().setWriteReadyHandler(writeReadyHandler); + return state; + } + + @Override + public void clearChannel() { + super.clearChannel(); + channel.getSinkChannel().getConduit().setWriteReadyHandler(writeReadyHandler); + } + + @Override + public SSLSessionInfo getSslSessionInfo() { + return sslSessionInfo; + } + + @Override + public void setSslSessionInfo(SSLSessionInfo sessionInfo) { + this.sslSessionInfo = sessionInfo; + } + + void setSSLSessionInfo(BasicSSLSessionInfo sslSessionInfo) { + this.sslSessionInfo = sslSessionInfo; + } + + @Override + protected StreamConnection upgradeChannel() { + resetChannel(); + if (extraBytes != null) { + channel.getSourceChannel().setConduit(new ReadDataStreamSourceConduit(channel.getSourceChannel().getConduit(), this)); + } + return channel; + } + + @Override + protected StreamSinkConduit getSinkConduit(HttpServerExchange exchange, StreamSinkConduit conduit) { + DateUtils.addDateHeaderIfRequired(exchange); + return conduit; + } + + @Override + protected boolean isUpgradeSupported() { + return true; //TODO: should we support this? + } + + void setAjpReadListener(AjpReadListener ajpReadListener) { + this.ajpReadListener = ajpReadListener; + } + + @Override + protected void exchangeComplete(HttpServerExchange exchange) { + ajpReadListener.exchangeComplete(exchange); + } + + void setCurrentExchange(HttpServerExchange exchange) { + this.current = exchange; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerRequestConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerRequestConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerRequestConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,296 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowMessages; +import io.undertow.conduits.ConduitListener; +import io.undertow.server.HttpServerExchange; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.conduits.AbstractStreamSourceConduit; +import org.xnio.conduits.ConduitReadableByteChannel; +import org.xnio.conduits.StreamSourceConduit; + +import static org.xnio.Bits.anyAreSet; +import static org.xnio.Bits.longBitMask; + +/** + * Underlying AJP request channel. + * + * @author Stuart Douglas + */ +public class AjpServerRequestConduit extends AbstractStreamSourceConduit { + + private static final ByteBuffer READ_BODY_CHUNK; + + static { + ByteBuffer readBody = ByteBuffer.allocateDirect(7); + readBody.put((byte) 'A'); + readBody.put((byte) 'B'); + readBody.put((byte) 0); + readBody.put((byte) 3); + readBody.put((byte) 6); + readBody.put((byte) 0x1F); + readBody.put((byte) 0xFA); + readBody.flip(); + READ_BODY_CHUNK = readBody; + + } + + private static final int HEADER_LENGTH = 6; + + /** + * There is a packet coming from apache. + */ + private static final long STATE_READING = 1L << 63L; + /** + * There is no packet coming, as we need to set a GET_BODY_CHUNK message + */ + private static final long STATE_SEND_REQUIRED = 1L << 62L; + /** + * read is done + */ + private static final long STATE_FINISHED = 1L << 61L; + + /** + * The remaining bits are used to store the remaining chunk size. + */ + private static final long STATE_MASK = longBitMask(0, 60); + + + private final HttpServerExchange exchange; + + private final AjpServerResponseConduit ajpResponseConduit; + + /** + * byte buffer that is used to hold header data + */ + private final ByteBuffer headerBuffer = ByteBuffer.allocateDirect(HEADER_LENGTH); + + private final ConduitListener finishListener; + + /** + /** + * The total amount of remaining data. If this is unknown it is -1. + */ + private long remaining; + + /** + * State flags, with the chunk remaining stored in the low bytes + */ + private long state; + + /** + * The total amount of data that has been read + */ + private long totalRead; + + public AjpServerRequestConduit(final StreamSourceConduit delegate, HttpServerExchange exchange, AjpServerResponseConduit ajpResponseConduit, Long size, ConduitListener finishListener) { + super(delegate); + this.exchange = exchange; + this.ajpResponseConduit = ajpResponseConduit; + this.finishListener = finishListener; + if (size == null) { + state = STATE_SEND_REQUIRED; + remaining = -1; + } else if (size.longValue() == 0L) { + state = STATE_FINISHED; + remaining = 0; + } else { + state = STATE_READING; + remaining = size; + } + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + return target.transferFrom(new ConduitReadableByteChannel(this), position, count); + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + return IoUtils.transfer(new ConduitReadableByteChannel(this), count, throughBuffer, target); + } + + @Override + public void terminateReads() throws IOException { + if(exchange.isPersistent()) { + state |= STATE_FINISHED; + return; + } + super.terminateReads(); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + long total = 0; + for (int i = offset; i < length; ++i) { + while (dsts[i].hasRemaining()) { + int r = read(dsts[i]); + if (r <= 0 && total > 0) { + return total; + } else if (r <= 0) { + return r; + } else { + total += r; + } + } + } + return total; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + long state = this.state; + if (anyAreSet(state, STATE_FINISHED)) { + return -1; + } else if (anyAreSet(state, STATE_SEND_REQUIRED)) { + state = this.state = (state & STATE_MASK) | STATE_READING; + if(ajpResponseConduit.isWriteShutdown()) { + this.state = STATE_FINISHED; + if (finishListener != null) { + finishListener.handleEvent(this); + } + return -1; + } + if (!ajpResponseConduit.doGetRequestBodyChunk(READ_BODY_CHUNK.duplicate(), this)) { + return 0; + } + } + //we might have gone into state_reading above + if (anyAreSet(state, STATE_READING)) { + return doRead(dst, state); + } + assert STATE_FINISHED == state; + return -1; + } + + private int doRead(final ByteBuffer dst, long state) throws IOException { + ByteBuffer headerBuffer = this.headerBuffer; + long headerRead = HEADER_LENGTH - headerBuffer.remaining(); + long remaining = this.remaining; + if (remaining == 0) { + this.state = STATE_FINISHED; + if (finishListener != null) { + finishListener.handleEvent(this); + } + return -1; + } + long chunkRemaining; + if (headerRead != HEADER_LENGTH) { + int read = next.read(headerBuffer); + if (read == -1) { + this.state = STATE_FINISHED; + if (finishListener != null) { + finishListener.handleEvent(this); + } + return read; + } else if (headerBuffer.hasRemaining()) { + return 0; + } else { + headerBuffer.flip(); + byte b1 = headerBuffer.get(); //0x12 + byte b2 = headerBuffer.get(); //0x34 + if (b1 != 0x12 || b2 != 0x34) { + throw UndertowMessages.MESSAGES.wrongMagicNumber(); + } + headerBuffer.get();//the length headers, two more than the string length header + headerBuffer.get(); + b1 = headerBuffer.get(); + b2 = headerBuffer.get(); + chunkRemaining = ((b1 & 0xFF) << 8) | (b2 & 0xFF); + if (chunkRemaining == 0) { + this.remaining = 0; + this.state = STATE_FINISHED; + + if (finishListener != null) { + finishListener.handleEvent(this); + } + return -1; + } + } + } else { + chunkRemaining = this.state & STATE_MASK; + } + + int limit = dst.remaining(); + try { + if (dst.remaining() > chunkRemaining) { + dst.limit((int) (dst.position() + chunkRemaining)); + } + int read = next.read(dst); + chunkRemaining -= read; + if (remaining != -1) { + remaining -= read; + } + this.totalRead += read; + if (remaining == 0) { + this.state = STATE_FINISHED; + if (finishListener != null) { + finishListener.handleEvent(this); + } + } else if (chunkRemaining == 0) { + headerBuffer.clear(); + this.state = STATE_SEND_REQUIRED; + } else { + this.state = (state & ~STATE_MASK) | chunkRemaining; + } + return read; + } finally { + this.remaining = remaining; + dst.limit(limit); + final long maxEntitySize = exchange.getMaxEntitySize(); + if (maxEntitySize > 0) { + if (totalRead > maxEntitySize) { + //kill the connection, nothing else can be sent on it + terminateReads(); + exchange.setPersistent(false); + throw UndertowMessages.MESSAGES.requestEntityWasTooLarge(maxEntitySize); + } + } + } + } + + @Override + public void awaitReadable() throws IOException { + if (anyAreSet(state, STATE_READING)) { + next.awaitReadable(); + } + } + + @Override + public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { + if (anyAreSet(state, STATE_READING)) { + next.awaitReadable(time, timeUnit); + } + } + + /** + * Method that is called when an error occurs writing out read body chunk frames + * @param e + */ + void setReadBodyChunkError(IOException e) { + //TODO: + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerResponseConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerResponseConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/ajp/AjpServerResponseConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,457 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.ajp; + +import io.undertow.UndertowMessages; +import io.undertow.conduits.AbstractFramedStreamSinkConduit; +import io.undertow.conduits.ConduitListener; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; +import org.jboss.logging.Logger; +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.WriteReadyHandler; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * AJP response channel. For now we are going to assume that the buffers are sized to + * fit complete packets. As AJP packets are limited to 8k this is a reasonable assumption. + * + * @author David M. Lloyd + * @author Stuart Douglas + */ +final class AjpServerResponseConduit extends AbstractFramedStreamSinkConduit { + + private static final Logger log = Logger.getLogger("io.undertow.server.channel.ajp.response"); + + private static final int MAX_DATA_SIZE = 8184; + + private static final Map HEADER_MAP; + + static { + final Map headers = new HashMap<>(); + headers.put(Headers.CONTENT_TYPE, 0xA001); + headers.put(Headers.CONTENT_LANGUAGE, 0xA002); + headers.put(Headers.CONTENT_LENGTH, 0xA003); + headers.put(Headers.DATE, 0xA004); + headers.put(Headers.LAST_MODIFIED, 0xA005); + headers.put(Headers.LOCATION, 0xA006); + headers.put(Headers.SET_COOKIE, 0xA007); + headers.put(Headers.SET_COOKIE2, 0xA008); + headers.put(Headers.SERVLET_ENGINE, 0xA009); + headers.put(Headers.STATUS, 0xA00A); + headers.put(Headers.WWW_AUTHENTICATE, 0xA00B); + HEADER_MAP = Collections.unmodifiableMap(headers); + } + + private static final int FLAG_START = 1; //indicates that the header has not been generated yet. + private static final int FLAG_WRITE_RESUMED = 1 << 2; + private static final int FLAG_WRITE_READ_BODY_CHUNK_FROM_LISTENER = 1 << 3; + private static final int FLAG_WRITE_SHUTDOWN = 1 << 4; + private static final int FLAG_READS_DONE = 1 << 5; + + private static final ByteBuffer CLOSE_FRAME_PERSISTENT; + private static final ByteBuffer CLOSE_FRAME_NON_PERSISTENT; + + static { + ByteBuffer buffer = ByteBuffer.wrap(new byte[6]); + buffer.put((byte) 'A'); + buffer.put((byte) 'B'); + buffer.put((byte) 0); + buffer.put((byte) 2); + buffer.put((byte) 5); + buffer.put((byte) 1); //reuse + buffer.flip(); + CLOSE_FRAME_PERSISTENT = buffer; + buffer = ByteBuffer.wrap(new byte[6]); + buffer.put(CLOSE_FRAME_PERSISTENT.duplicate()); + buffer.put(5, (byte) 0); + buffer.flip(); + CLOSE_FRAME_NON_PERSISTENT = buffer; + } + + + private final Pool pool; + + /** + * State flags + */ + private int state = FLAG_START; + + private final HttpServerExchange exchange; + + private final ConduitListener finishListener; + + private final boolean headRequest; + + AjpServerResponseConduit(final StreamSinkConduit next, final Pool pool, final HttpServerExchange exchange, ConduitListener finishListener, boolean headRequest) { + super(next); + this.pool = pool; + this.exchange = exchange; + this.finishListener = finishListener; + this.headRequest = headRequest; + state = FLAG_START; + } + + private static void putInt(final ByteBuffer buf, int value) { + buf.put((byte) ((value >> 8) & 0xFF)); + buf.put((byte) (value & 0xFF)); + } + + private static void putString(final ByteBuffer buf, String value) { + final int length = value.length(); + putInt(buf, length); + for (int i = 0; i < length; ++i) { + buf.put((byte) value.charAt(i)); + } + buf.put((byte) 0); + } + + private void putHttpString(final ByteBuffer buf, HttpString value) { + final int length = value.length(); + putInt(buf, length); + value.appendTo(buf); + buf.put((byte) 0); + } + + /** + * Handles generating the header if required, and adding it to the frame queue. + * + * No attempt is made to actually flush this, so a gathering write can be used to actually flush the data + */ + private void processAJPHeader() { + int oldState = this.state; + if (anyAreSet(oldState, FLAG_START)) { + + Pooled[] byteBuffers = null; + + //merge the cookies into the header map + Connectors.flattenCookies(exchange); + + Pooled pooled = pool.allocate(); + ByteBuffer buffer = pooled.getResource(); + buffer.put((byte) 'A'); + buffer.put((byte) 'B'); + buffer.put((byte) 0); //we fill the size in later + buffer.put((byte) 0); + buffer.put((byte) 4); + putInt(buffer, exchange.getResponseCode()); + putString(buffer, StatusCodes.getReason(exchange.getResponseCode())); + + int headers = 0; + //we need to count the headers + final HeaderMap responseHeaders = exchange.getResponseHeaders(); + for (HttpString name : responseHeaders.getHeaderNames()) { + headers += responseHeaders.get(name).size(); + } + + putInt(buffer, headers); + + + for (final HttpString header : responseHeaders.getHeaderNames()) { + for (String headerValue : responseHeaders.get(header)) { + if(buffer.remaining() < header.length() + headerValue.length() + 6) { + //if there is not enough room in the buffer we need to allocate more + buffer.flip(); + if(byteBuffers == null) { + byteBuffers = new Pooled[2]; + byteBuffers[0] = pooled; + } else { + Pooled[] old = byteBuffers; + byteBuffers = new Pooled[old.length + 1]; + System.arraycopy(old, 0, byteBuffers, 0, old.length); + } + pooled = pool.allocate(); + byteBuffers[byteBuffers.length - 1] = pooled; + buffer = pooled.getResource(); + } + + Integer headerCode = HEADER_MAP.get(header); + if (headerCode != null) { + putInt(buffer, headerCode); + } else { + putHttpString(buffer, header); + } + putString(buffer, headerValue); + } + } + if(byteBuffers == null) { + int dataLength = buffer.position() - 4; + buffer.put(2, (byte) ((dataLength >> 8) & 0xFF)); + buffer.put(3, (byte) (dataLength & 0xFF)); + buffer.flip(); + queueFrame(new PooledBufferFrameCallback(pooled), buffer); + } else { + ByteBuffer[] bufs = new ByteBuffer[byteBuffers.length]; + for(int i = 0; i < bufs.length; ++i) { + bufs[i] = byteBuffers[i].getResource(); + } + int dataLength = (int) (Buffers.remaining(bufs) - 4); + bufs[0].put(2, (byte) ((dataLength >> 8) & 0xFF)); + bufs[0].put(3, (byte) (dataLength & 0xFF)); + buffer.flip(); + queueFrame(new PooledBuffersFrameCallback(byteBuffers), bufs); + } + state &= ~FLAG_START; + } + } + + + @Override + protected void queueCloseFrames() { + processAJPHeader(); + final ByteBuffer buffer = exchange.isPersistent() ? CLOSE_FRAME_PERSISTENT.duplicate() : CLOSE_FRAME_NON_PERSISTENT.duplicate(); + queueFrame(null, buffer); + } + + public int write(final ByteBuffer src) throws IOException { + if(queuedDataLength() > 0) { + //if there is data in the queue we flush and return + //otherwise the queue can grow indefinitely + if(!flush()) { + return 0; + } + } + processAJPHeader(); + if (headRequest) { + int remaining = src.remaining(); + src.position(src.position() + remaining); + return remaining; + } + int limit = src.limit(); + try { + if (src.remaining() > MAX_DATA_SIZE) { + src.limit(src.position() + MAX_DATA_SIZE); + } + final int writeSize = src.remaining(); + final ByteBuffer[] buffers = createHeader(src); + int toWrite = 0; + for (ByteBuffer buffer : buffers) { + toWrite += buffer.remaining(); + } + final int originalPayloadSize = writeSize; + long r = 0; + do { + r = super.write(buffers, 0, buffers.length); + toWrite -= r; + if (r == -1) { + throw new ClosedChannelException(); + } else if (r == 0) { + //we need to copy all the remaining bytes + //TODO: this assumes the buffer is big enough + Pooled newPooledBuffer = pool.allocate(); + while (src.hasRemaining()) { + newPooledBuffer.getResource().put(src); + } + newPooledBuffer.getResource().flip(); + ByteBuffer[] savedBuffers = new ByteBuffer[3]; + savedBuffers[0] = buffers[0]; + savedBuffers[1] = newPooledBuffer.getResource(); + savedBuffers[2] = buffers[2]; + queueFrame(new PooledBufferFrameCallback(newPooledBuffer), savedBuffers); + return originalPayloadSize; + } + } while (toWrite > 0); + return originalPayloadSize; + } finally { + src.limit(limit); + } + } + + private ByteBuffer[] createHeader(final ByteBuffer src) { + int remaining = src.remaining(); + int chunkSize = remaining + 4; + byte[] header = new byte[7]; + header[0] = (byte) 'A'; + header[1] = (byte) 'B'; + header[2] = (byte) ((chunkSize >> 8) & 0xFF); + header[3] = (byte) (chunkSize & 0xFF); + header[4] = (byte) (3 & 0xFF); + header[5] = (byte) ((remaining >> 8) & 0xFF); + header[6] = (byte) (remaining & 0xFF); + + byte[] footer = new byte[1]; + footer[0] = 0; + + final ByteBuffer[] buffers = new ByteBuffer[3]; + buffers[0] = ByteBuffer.wrap(header); + buffers[1] = src; + buffers[2] = ByteBuffer.wrap(footer); + return buffers; + } + + public long write(final ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + long total = 0; + for (int i = offset; i < offset + length; ++i) { + while (srcs[i].hasRemaining()) { + int written = write(srcs[i]); + if (written == 0) { + return total; + } + total += written; + } + } + return total; + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + protected void finished() { + if (finishListener != null) { + finishListener.handleEvent(this); + } + } + + @Override + public void setWriteReadyHandler(WriteReadyHandler handler) { + next.setWriteReadyHandler(new AjpServerWriteReadyHandler(handler)); + } + + public void suspendWrites() { + log.trace("suspend"); + state &= ~FLAG_WRITE_RESUMED; + if (allAreClear(state, FLAG_WRITE_READ_BODY_CHUNK_FROM_LISTENER)) { + next.suspendWrites(); + } + } + + public void resumeWrites() { + log.trace("resume"); + state |= FLAG_WRITE_RESUMED; + next.resumeWrites(); + } + + public boolean isWriteResumed() { + return anyAreSet(state, FLAG_WRITE_RESUMED); + } + + public void wakeupWrites() { + log.trace("wakeup"); + state |= FLAG_WRITE_RESUMED; + next.wakeupWrites(); + } + + @Override + protected void doTerminateWrites() throws IOException { + if (!exchange.isPersistent()) { + next.terminateWrites(); + } + state |= FLAG_WRITE_SHUTDOWN; + } + + @Override + public boolean isWriteShutdown() { + return super.isWriteShutdown() || anyAreSet(state, FLAG_WRITE_SHUTDOWN); + } + + boolean doGetRequestBodyChunk(ByteBuffer buffer, final AjpServerRequestConduit requestChannel) throws IOException { + //first attempt to just write out the buffer + //if there are other frames queued they will be written out first + if(isWriteShutdown()) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + super.write(buffer); + if (buffer.hasRemaining()) { + //write it out in a listener + this.state |= FLAG_WRITE_READ_BODY_CHUNK_FROM_LISTENER; + queueFrame(new FrameCallBack() { + + @Override + public void done() { + state &= ~FLAG_WRITE_READ_BODY_CHUNK_FROM_LISTENER; + if (allAreClear(state, FLAG_WRITE_RESUMED)) { + next.suspendWrites(); + } + } + + @Override + public void failed(IOException e) { + requestChannel.setReadBodyChunkError(e); + } + }, buffer); + next.resumeWrites(); + return false; + } + return true; + } + + private final class AjpServerWriteReadyHandler implements WriteReadyHandler { + + private final WriteReadyHandler delegate; + + private AjpServerWriteReadyHandler(WriteReadyHandler delegate) { + this.delegate = delegate; + } + + @Override + public void writeReady() { + if (anyAreSet(state, FLAG_WRITE_READ_BODY_CHUNK_FROM_LISTENER)) { + try { + flush(); + } catch (IOException e) { + log.debug("Error flushing when doing async READ_BODY_CHUNK flush", e); + } + } + if (anyAreSet(state, FLAG_WRITE_RESUMED)) { + delegate.writeReady(); + } + } + + @Override + public void forceTermination() { + delegate.forceTermination(); + } + + @Override + public void terminated() { + delegate.terminated(); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,868 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.framed; + +import static org.xnio.IoUtils.safeClose; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.ListIterator; +import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import org.xnio.Buffers; +import org.xnio.ChannelListener; +import org.xnio.ChannelListener.Setter; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.CloseableChannel; +import org.xnio.channels.ConnectedChannel; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.conduits.IdleTimeoutConduit; +import io.undertow.util.ReferenceCountedPooled; +import io.undertow.websockets.core.WebSocketLogger; + +/** + * A {@link org.xnio.channels.ConnectedChannel} which can be used to send and receive Frames. + *

+ * This provides a common base for framed protocols such as websockets and SPDY + * + * @author Stuart Douglas + */ +public abstract class AbstractFramedChannel, R extends AbstractFramedStreamSourceChannel, S extends AbstractFramedStreamSinkChannel> implements ConnectedChannel { + + private final StreamConnection channel; + private final IdleTimeoutConduit idleTimeoutConduit; + + private final ChannelListener.SimpleSetter closeSetter; + private final ChannelListener.SimpleSetter receiveSetter; + private final Pool bufferPool; + + /** + * Frame priority implementation. This is used to determine the order in which frames get sent + */ + private final FramePriority framePriority; + + /** + * List of frames that are ready to send + */ + private final List pendingFrames = new LinkedList<>(); + /** + * Frames that are not yet read to send. + */ + private final Deque heldFrames = new ArrayDeque<>(); + + /** + * new frames to be sent. These will be added to either the pending or held frames list + * depending on the {@link #framePriority} implementation in use. + */ + private final Deque newFrames = new LinkedBlockingDeque<>(); + + private volatile long frameDataRemaining; + private volatile R receiver; + + private boolean receivesSuspended = true; + + @SuppressWarnings("unused") + private volatile int readsBroken = 0; + + @SuppressWarnings("unused") + private volatile int writesBroken = 0; + + private static final AtomicIntegerFieldUpdater readsBrokenUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractFramedChannel.class, "readsBroken"); + private static final AtomicIntegerFieldUpdater writesBrokenUpdater = AtomicIntegerFieldUpdater.newUpdater(AbstractFramedChannel.class, "writesBroken"); + + private ReferenceCountedPooled readData = null; + private final List> closeTasks = new CopyOnWriteArrayList<>(); + private boolean flushingSenders = false; + + private final Set> receivers = new HashSet<>(); + + /** + * Create a new {@link io.undertow.server.protocol.framed.AbstractFramedChannel} + * 8 + * + * @param connectedStreamChannel The {@link org.xnio.channels.ConnectedStreamChannel} over which the WebSocket Frames should get send and received. + * Be aware that it already must be "upgraded". + * @param bufferPool The {@link org.xnio.Pool} which will be used to acquire {@link java.nio.ByteBuffer}'s from. + * @param framePriority + */ + protected AbstractFramedChannel(final StreamConnection connectedStreamChannel, Pool bufferPool, FramePriority framePriority, final Pooled readData) { + this.framePriority = framePriority; + if (readData != null) { + this.readData = new ReferenceCountedPooled<>(readData, 1); + } + IdleTimeoutConduit idle = createIdleTimeoutChannel(connectedStreamChannel); + connectedStreamChannel.getSourceChannel().setConduit(idle); + connectedStreamChannel.getSinkChannel().setConduit(idle); + this.idleTimeoutConduit = idle; + this.channel = connectedStreamChannel; + this.bufferPool = bufferPool; + + closeSetter = new ChannelListener.SimpleSetter<>(); + receiveSetter = new ChannelListener.SimpleSetter<>(); + channel.getSourceChannel().getReadSetter().set(null); + channel.getSourceChannel().suspendReads(); + + channel.getSourceChannel().getReadSetter().set(new FrameReadListener()); + connectedStreamChannel.getSinkChannel().getWriteSetter().set(new FrameWriteListener()); + FrameCloseListener closeListener = new FrameCloseListener(); + connectedStreamChannel.getSinkChannel().getCloseSetter().set(closeListener); + connectedStreamChannel.getSourceChannel().getCloseSetter().set(closeListener); + } + + protected IdleTimeoutConduit createIdleTimeoutChannel(StreamConnection connectedStreamChannel) { + return new IdleTimeoutConduit(connectedStreamChannel.getSinkChannel().getConduit(), connectedStreamChannel.getSourceChannel().getConduit()); + } + + /** + * Get the buffer pool for this connection. + * + * @return the buffer pool for this connection + */ + public Pool getBufferPool() { + return bufferPool; + } + + @Override + public SocketAddress getLocalAddress() { + return channel.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return channel.getLocalAddress(type); + } + + @Override + public XnioWorker getWorker() { + return channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return channel.getIoThread(); + } + + @Override + public boolean supportsOption(Option option) { + return channel.supportsOption(option); + } + + @Override + public T getOption(Option option) throws IOException { + return channel.getOption(option); + } + + @Override + public T setOption(Option option, T value) throws IOException { + return channel.setOption(option, value); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public SocketAddress getPeerAddress() { + return channel.getPeerAddress(); + } + + @Override + public A getPeerAddress(Class type) { + return channel.getPeerAddress(type); + } + + /** + * Get the source address of the Channel. + * + * @return the source address of the Channel + */ + public InetSocketAddress getSourceAddress() { + return getPeerAddress(InetSocketAddress.class); + } + + /** + * Get the destination address of the Channel. + * + * @return the destination address of the Channel + */ + public InetSocketAddress getDestinationAddress() { + return getLocalAddress(InetSocketAddress.class); + } + + /** + * receive method, returns null if no frame is ready. Otherwise returns a + * channel that can be used to read the frame contents. + *

+ * Calling this method can also have the side effect of making additional data available to + * existing source channels. In general if you suspend receives or don't have some other way + * of calling this method then it can prevent frame channels for being fully consumed. + */ + public synchronized R receive() throws IOException { + if (isLastFrameReceived() && receiver == null) { + //we have received the last frame, we just shut down and return + //it would probably make more sense to have the last channel responsible for this + //however it is much simpler just to have it here + channel.getSourceChannel().suspendReads(); + channel.getSourceChannel().shutdownReads(); + return null; + } + ReferenceCountedPooled pooled = this.readData; + boolean hasData; + if (pooled == null) { + Pooled buf = bufferPool.allocate(); + this.readData = pooled = new ReferenceCountedPooled<>(buf, 1); + hasData = false; + } else { + hasData = pooled.getResource().hasRemaining(); + } + boolean forceFree = false; + int read = 0; + try { + if (!hasData) { + pooled.getResource().clear(); + read = channel.getSourceChannel().read(pooled.getResource()); + if (read == 0) { + //no data, we just free the buffer + forceFree = true; + return null; + } else if (read == -1) { + try { + channel.getSourceChannel().shutdownReads(); + } catch (IOException e) { + if (WebSocketLogger.REQUEST_LOGGER.isDebugEnabled()) { + WebSocketLogger.REQUEST_LOGGER.debugf(e, "Connection closed with IOException when attempting to shut down reads"); + } + // nothing we can do here.. close + safeClose(channel.getSourceChannel()); + throw e; + } + forceFree = true; + lastDataRead(); + return null; + } + pooled.getResource().flip(); + } + if (frameDataRemaining > 0) { + if (frameDataRemaining >= pooled.getResource().remaining()) { + frameDataRemaining -= pooled.getResource().remaining(); + if(receiver != null) { + receiver.dataReady(null, pooled); + } else { + //we are dropping a frame + pooled.free(); + } + readData = null; + if(frameDataRemaining == 0) { + receiver = null; + } + return null; + } else { + ByteBuffer buf = pooled.getResource().duplicate(); + buf.limit((int) (buf.position() + frameDataRemaining)); + pooled.getResource().position((int) (pooled.getResource().position() + frameDataRemaining)); + frameDataRemaining = 0; + Pooled frameData = pooled.createView(buf); + //note that we don't return here, there may be another frame + if(receiver != null) { + receiver.dataReady(null, frameData); + } else{ + //we are dropping the frame + frameData.free(); + } + receiver = null; + } + } + FrameHeaderData data = parseFrame(pooled.getResource()); + if (data != null) { + Pooled frameData; + if (data.getFrameLength() >= pooled.getResource().remaining()) { + frameDataRemaining = data.getFrameLength() - pooled.getResource().remaining(); + frameData = pooled.createView(pooled.getResource().duplicate()); + pooled.getResource().position(pooled.getResource().limit()); + } else { + ByteBuffer buf = pooled.getResource().duplicate(); + buf.limit((int) (buf.position() + data.getFrameLength())); + pooled.getResource().position((int) (pooled.getResource().position() + data.getFrameLength())); + frameData = pooled.createView(buf); + } + AbstractFramedStreamSourceChannel existing = data.getExistingChannel(); + if (existing != null) { + if (data.getFrameLength() > frameData.getResource().remaining()) { + receiver = (R) existing; + } + existing.dataReady(data, frameData); + return null; + } else { + boolean moreData = data.getFrameLength() > frameData.getResource().remaining(); + R newChannel = createChannel(data, frameData); + if (newChannel != null) { + receivers.add(newChannel); + if (moreData) { + receiver = newChannel; + } + } else { + frameData.free(); + } + + return newChannel; + } + } + return null; + } catch (IOException e) { + //something has code wrong with parsing, close the read side + //we don't close the write side, as the underlying implementation will most likely want to send an error + UndertowLogger.REQUEST_LOGGER.ioException(e); + markReadsBroken(e); + forceFree = true; + throw e; + } finally { + //if the receive caused the channel to break the close listener may be have been called + //which will make readData null + if (readData != null) { + if (!pooled.getResource().hasRemaining() || forceFree) { + pooled.free(); + this.readData = null; + } + } + } + } + + /** + * Method than is invoked when read() returns -1. + */ + protected void lastDataRead() { + + } + + /** + * Method that creates the actual stream source channel implementation that is in use. + * + * @param frameHeaderData The header data, as returned by {@link #parseFrame(java.nio.ByteBuffer)} + * @param frameData Any additional data for the frame that has already been read. This may not be the complete frame contents + * @return A new stream source channel + */ + protected abstract R createChannel(FrameHeaderData frameHeaderData, Pooled frameData) throws IOException; + + /** + * Attempts to parse an incoming frame header from the data in the buffer. + * + * @param data The data that has been read from the channel + * @return The frame header data, or null if the data was incomplete + * @throws IOException If the data could not be parsed. + */ + protected abstract FrameHeaderData parseFrame(ByteBuffer data) throws IOException; + + protected synchronized void recalculateHeldFrames() throws IOException { + if (!heldFrames.isEmpty()) { + framePriority.frameAdded(null, pendingFrames, heldFrames); + flushSenders(); + } + } + + /** + * Flushes all ready stream sink conduits to the channel. + *

+ * Frames will be batched up, to allow them all to be written out via a gathering + * write. The {@link #framePriority} implementation will be invoked to decide which + * frames are eligible for sending and in what order. + * + * @throws IOException + */ + protected synchronized void flushSenders() { + if(flushingSenders) { + throw UndertowMessages.MESSAGES.recursiveCallToFlushingSenders(); + } + flushingSenders = true; + try { + int toSend = 0; + while (!newFrames.isEmpty()) { + S frame = newFrames.poll(); + if (framePriority.insertFrame(frame, pendingFrames)) { + if (!heldFrames.isEmpty()) { + framePriority.frameAdded(frame, pendingFrames, heldFrames); + } + } else { + heldFrames.add(frame); + } + } + + boolean finalFrame = false; + ListIterator it = pendingFrames.listIterator(); + while (it.hasNext()) { + S sender = it.next(); + if (sender.isReadyForFlush()) { + ++toSend; + } else { + break; + } + if (sender.isLastFrame()) { + finalFrame = true; + } + } + if (toSend == 0) { + //if there is nothing to send we just attempt a flush on the underlying channel + try { + if(channel.getSinkChannel().flush()) { + channel.getSinkChannel().suspendWrites(); + } + } catch (IOException e) { + safeClose(channel); + markWritesBroken(e); + } + return; + } + ByteBuffer[] data = new ByteBuffer[toSend * 3]; + int j = 0; + it = pendingFrames.listIterator(); + try { + while (j < toSend) { + S next = it.next(); + //todo: rather than adding empty buffers just store the offsets + SendFrameHeader frameHeader = next.getFrameHeader(); + Pooled frameHeaderByteBuffer = frameHeader.getByteBuffer(); + data[j * 3] = frameHeaderByteBuffer != null + ? frameHeaderByteBuffer.getResource() + : Buffers.EMPTY_BYTE_BUFFER; + data[(j * 3) + 1] = next.getBuffer(); + data[(j * 3) + 2] = next.getFrameFooter(); + ++j; + } + long toWrite = Buffers.remaining(data); + long res; + do { + res = channel.getSinkChannel().write(data); + toWrite -= res; + } while (res > 0 && toWrite > 0); + int max = toSend; + + while (max > 0) { + S sinkChannel = pendingFrames.get(0); + Pooled frameHeaderByteBuffer = sinkChannel.getFrameHeader().getByteBuffer(); + if (frameHeaderByteBuffer != null && frameHeaderByteBuffer.getResource().hasRemaining() + || sinkChannel.getBuffer().hasRemaining() + || sinkChannel.getFrameFooter().hasRemaining()) { + break; + } + sinkChannel.flushComplete(); + pendingFrames.remove(sinkChannel); + max--; + } + if (!pendingFrames.isEmpty() || !channel.getSinkChannel().flush()) { + channel.getSinkChannel().resumeWrites(); + } else { + channel.getSinkChannel().suspendWrites(); + } + if (pendingFrames.isEmpty() && finalFrame) { + //all data has been sent. Close gracefully + channel.getSinkChannel().shutdownWrites(); + if (!channel.getSinkChannel().flush()) { + channel.getSinkChannel().setWriteListener(ChannelListeners.flushingChannelListener(null, null)); + channel.getSinkChannel().resumeWrites(); + } + } + + } catch (IOException e) { + safeClose(channel); + markWritesBroken(e); + } + } finally { + flushingSenders = false; + if(!newFrames.isEmpty()) { + getIoThread().execute(new Runnable() { + @Override + public void run() { + flushSenders(); + } + }); + } + } + } + + void awaitWritable() throws IOException { + this.channel.getSinkChannel().awaitWritable(); + } + + void awaitWritable(long time, TimeUnit unit) throws IOException { + this.channel.getSinkChannel().awaitWritable(time, unit); + } + + /** + * Queues a new frame to be sent, and attempts a flush if this is the first frame in the new frame queue. + *

+ * Depending on the {@link FramePriority} implementation in use the channel may or may not be added to the actual + * pending queue + * + * @param channel The channel + */ + protected synchronized void queueFrame(final S channel) throws IOException { + assert !newFrames.contains(channel); + if (isWritesBroken() || !this.channel.getSinkChannel().isOpen()) { + IoUtils.safeClose(channel); + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + newFrames.add(channel); + if (!flushingSenders) { + if(channel.getIoThread() == Thread.currentThread()) { + flushSenders(); + } else { + channel.getIoThread().execute(new Runnable() { + @Override + public void run() { + flushSenders(); + } + }); + } + } + } + + /** + * Returns true if the protocol specific final frame has been received. + * + * @return true If the last frame has been received + */ + protected abstract boolean isLastFrameReceived(); + + /** + * @return true If the last frame has been sent + */ + protected abstract boolean isLastFrameSent(); + + /** + * Method that is invoked when the read side of the channel is broken. This generally happens on a protocol error. + */ + protected abstract void handleBrokenSourceChannel(Throwable e); + + /** + * Method that is invoked when then write side of a channel is broken. This generally happens on a protocol error. + */ + protected abstract void handleBrokenSinkChannel(Throwable e); + + /** + * Return the {@link org.xnio.ChannelListener.Setter} which will holds the {@link org.xnio.ChannelListener} that gets notified once a frame was + * received. + */ + public Setter getReceiveSetter() { + return receiveSetter; + } + + /** + * Suspend the receive of new frames via {@link #receive()} + */ + public synchronized void suspendReceives() { + receivesSuspended = true; + if (receiver == null) { + channel.getSourceChannel().suspendReads(); + } + } + + /** + * Resume the receive of new frames via {@link #receive()} + */ + public synchronized void resumeReceives() { + receivesSuspended = false; + if (readData != null) { + channel.getSourceChannel().wakeupReads(); + } else { + channel.getSourceChannel().resumeReads(); + } + } + + public boolean isReceivesResumed() { + return !receivesSuspended; + } + + /** + * Forcibly closes the {@link io.undertow.server.protocol.framed.AbstractFramedChannel}. + */ + @Override + public void close() throws IOException { + safeClose(channel); + if(readData != null) { + readData.free(); + readData = null; + } + } + + @Override + public Setter getCloseSetter() { + return closeSetter; + } + + /** + * Called when a source sub channel fails to fulfil its contract, and leaves the channel in an inconsistent state. + *

+ * The underlying read side will be forcibly closed. + * + * @param cause The possibly null cause + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void markReadsBroken(Throwable cause) { + if (readsBrokenUpdater.compareAndSet(this, 0, 1)) { + handleBrokenSourceChannel(cause); + safeClose(channel.getSourceChannel()); + + + closeSubChannels(); + } + } + + /** + * Method that is called when the channel is being forcibly closed, and all sub stream sink/source + * channels should also be forcibly closed. + */ + protected abstract void closeSubChannels(); + + + + /** + * Called when a sub channel fails to fulfil its contract, and leaves the channel in an inconsistent state. + *

+ * The underlying channel will be closed, and any sub channels that have writes resumed will have their + * listeners notified. It is expected that these listeners will then attempt to use the channel, and their standard + * error handling logic will take over. + * + * @param cause The possibly null cause + */ + @SuppressWarnings({"unchecked", "rawtypes"}) + protected void markWritesBroken(Throwable cause) { + if (writesBrokenUpdater.compareAndSet(this, 0, 1)) { + handleBrokenSinkChannel(cause); + safeClose(channel.getSinkChannel()); + synchronized (this) { + for (final S channel : pendingFrames) { + channel.markBroken(); + } + pendingFrames.clear(); + for (final S channel : newFrames) { + channel.markBroken(); + } + newFrames.clear(); + for (final S channel : heldFrames) { + channel.markBroken(); + } + heldFrames.clear(); + } + } + } + + protected boolean isWritesBroken() { + return writesBrokenUpdater.get(this) != 0; + } + + protected boolean isReadsBroken() { + return readsBrokenUpdater.get(this) != 0; + } + + + void resumeWrites() { + channel.getSinkChannel().resumeWrites(); + } + + void suspendWrites() { + channel.getSinkChannel().suspendWrites(); + } + + void wakeupWrites() { + channel.getSinkChannel().wakeupWrites(); + } + + StreamSourceChannel getSourceChannel() { + return channel.getSourceChannel(); + } + + void notifyFrameReadComplete(AbstractFramedStreamSourceChannel channel) { + synchronized (AbstractFramedChannel.this) { + if (isLastFrameReceived()) { + safeClose(AbstractFramedChannel.this.channel.getSourceChannel()); + } + } + } + + void notifyClosed(AbstractFramedStreamSourceChannel channel) { + synchronized (AbstractFramedChannel.this) { + receivers.remove(channel); + } + } + + + /** + * {@link org.xnio.ChannelListener} which delegates the read notification to the appropriate listener + */ + private final class FrameReadListener implements ChannelListener { + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void handleEvent(final StreamSourceChannel channel) { + final R receiver = AbstractFramedChannel.this.receiver; + if ((isLastFrameReceived() || receivesSuspended) && receiver == null) { + channel.suspendReads(); + return; + } else { + final ChannelListener listener = receiveSetter.get(); + if (listener != null) { + WebSocketLogger.REQUEST_LOGGER.debugf("Invoking receive listener", receiver); + ChannelListeners.invokeChannelListener(AbstractFramedChannel.this, listener); + } else { + channel.suspendReads(); + } + } + if (readData != null && channel.isOpen()) { + ChannelListeners.invokeChannelListener(channel.getIoThread(), channel, this); + } + } + } + + private class FrameWriteListener implements ChannelListener { + @Override + public void handleEvent(final StreamSinkChannel channel) { + flushSenders(); + } + } + + /** + * close listener, just goes through and activates any sub channels to make sure their listeners are invoked + */ + private class FrameCloseListener implements ChannelListener { + + private boolean sinkClosed; + private boolean sourceClosed; + + @Override + public void handleEvent(final CloseableChannel c) { + if(c instanceof StreamSinkChannel) { + sinkClosed = true; + } else if(c instanceof StreamSourceChannel) { + sourceClosed = true; + } + if(!sourceClosed || !sinkClosed) { + return; //both sides need to be closed + } else if(readData != null) { + //we make sure there is no data left to receive, if there is then we invoke the receive listener + final ChannelListener listener = receiveSetter.get(); + if(listener != null) { + channel.getIoThread().execute(new Runnable() { + @Override + public void run() { + while (readData != null) { + int rem = readData.getResource().remaining(); + ChannelListeners.invokeChannelListener(AbstractFramedChannel.this, (ChannelListener) receiveSetter.get()); + if(readData != null && rem == readData.getResource().remaining()) { + readData.free(); + readData = null; + break;//make sure we are making progress + } + } + handleEvent(c); + } + }); + } + return; + } + + if (Thread.currentThread() != c.getIoThread()) { + ChannelListeners.invokeChannelListener(c.getIoThread(), c, this); + return; + } + R receiver = AbstractFramedChannel.this.receiver; + try { + if (receiver != null && receiver.isOpen() && receiver.isReadResumed()) { + ChannelListeners.invokeChannelListener(receiver, ((SimpleSetter) receiver.getReadSetter()).get()); + } + synchronized (AbstractFramedChannel.this) { + for (final S channel : pendingFrames) { + //if this was a clean shutdown there should not be any senders + channel.markBroken(); + } + for (final S channel : newFrames) { + //if this was a clean shutdown there should not be any senders + channel.markBroken(); + } + for (final S channel : heldFrames) { + //if this was a clean shutdown there should not be any senders + channel.markBroken(); + } + for(AbstractFramedStreamSourceChannel r : new ArrayList<>(receivers)) { + IoUtils.safeClose(r); + } + } + } finally { + try { + for (ChannelListener task : closeTasks) { + ChannelListeners.invokeChannelListener((C) AbstractFramedChannel.this, task); + } + } finally { + synchronized (AbstractFramedChannel.this) { + closeSubChannels(); + if (readData != null) { + readData.free(); + readData = null; + } + } + ChannelListeners.invokeChannelListener((C) AbstractFramedChannel.this, closeSetter.get()); + } + } + } + } + + public void setIdleTimeout(long timeout) { + idleTimeoutConduit.setIdleTimeout(timeout); + } + + public long getIdleTimeout() { + return idleTimeoutConduit.getIdleTimeout(); + } + + protected FramePriority getFramePriority() { + return framePriority; + } + + public void addCloseTask(final ChannelListener task) { + closeTasks.add(task); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "[ " + (receiver == null ? "No Receiver" : receiver.toString()) + " " + pendingFrames.toString() + " -- " + heldFrames.toString() + " -- " + newFrames.toString() + "]"; + } + + protected StreamConnection getUnderlyingConnection() { + return channel; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedStreamSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedStreamSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedStreamSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,607 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.framed; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.util.ImmediatePooled; +import org.xnio.Buffers; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Pooled; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.Channels; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreSet; + +/** + * Framed Stream Sink Channel. + *

+ * Thread safety notes: + *

+ * The general contract is that this channel is only to be used by a single thread at a time. The only exception to this is + * during flush. A flush will only happen when {@link #readyForFlush} is set, and while this bit is set the buffer + * must not be modified. + * + * @author Stuart Douglas + */ +public abstract class AbstractFramedStreamSinkChannel, R extends AbstractFramedStreamSourceChannel, S extends AbstractFramedStreamSinkChannel> implements StreamSinkChannel { + + private static final Pooled EMPTY_BYTE_BUFFER = new ImmediatePooled<>(ByteBuffer.allocateDirect(0)); + + private Pooled buffer; + private final C channel; + private final ChannelListener.SimpleSetter writeSetter = new ChannelListener.SimpleSetter<>(); + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter<>(); + + private final Object lock = new Object(); + + /** + * the state variable, this must only be access by the thread that 'owns' the channel + */ + private volatile int state = 0; + /** + * If this channel is ready for flush, updated by multiple threads. In general it will be set by the thread + * that 'owns' the channel, and cleared by the IO thread + */ + private volatile boolean readyForFlush; + + /** + * If all the data has been written out and the channel has been fully flushed + */ + private volatile boolean fullyFlushed; + + /** + * If the last frame has been queued. + * + * Note that this may not actually be the final frame in some circumstances, e.g. if the final frame + * is two large to fit in the flow control window. In this case the flag may be cleared after flush is complete. + */ + private volatile boolean finalFrameQueued; + + /** + * If this channel is broken, updated by multiple threads + */ + private volatile boolean broken; + + private volatile int waiterCount = 0; + + private SendFrameHeader header; + private Pooled trailer; + + private static final int STATE_CLOSED = 1; + private static final int STATE_WRITES_RESUMED = 1 << 1; + private static final int STATE_WRITES_SHUTDOWN = 1 << 2; + private static final int STATE_IN_LISTENER_LOOP = 1 << 3; + private static final int STATE_FIRST_DATA_WRITTEN = 1 << 4; + + + protected AbstractFramedStreamSinkChannel(C channel) { + this.channel = channel; + this.buffer = channel.getBufferPool().allocate(); + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return src.transferTo(position, count, this); + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, this); + } + + @Override + public void suspendWrites() { + state &= ~STATE_WRITES_RESUMED; + } + + /** + * Returns the header for the current frame. + * + * This consists of the frame data, and also an integer specifying how much data is remaining in the buffer. + * If this is non-zero then this method must adjust the buffers limit accordingly. + * + * It is expected that this will be used when limits on the size of a data frame prevent the whole buffer from + * being sent at once. + * + * + * @return The header for the current frame, or null + */ + final SendFrameHeader getFrameHeader() throws IOException { + if (header == null) { + header = createFrameHeader(); + if (header == null) { + header = new SendFrameHeader(0, null); + } + } + return header; + } + + protected SendFrameHeader createFrameHeader() throws IOException{ + return null; + } + + /** + * Returns the footer for the current frame. + * + * @return The footer for the current frame, or null + */ + final ByteBuffer getFrameFooter() { + if (trailer == null) { + trailer = createFrameFooter(); + if (trailer == null) { + trailer = EMPTY_BYTE_BUFFER; + } + } + return trailer.getResource(); + } + + protected Pooled createFrameFooter() { + return null; + } + + @Override + public boolean isWriteResumed() { + return anyAreSet(state, STATE_WRITES_RESUMED); + } + + @Override + public void wakeupWrites() { + resumeWritesInternal(true); + } + + @Override + public void resumeWrites() { + resumeWritesInternal(false); + } + + protected void resumeWritesInternal(boolean wakeup) { + boolean alreadyResumed = anyAreSet(state, STATE_WRITES_RESUMED); + if(!wakeup && alreadyResumed) { + return; + } + state |= STATE_WRITES_RESUMED; + if(readyForFlush && !wakeup) { + //we already have data queued to be flushed + return; + } + + if (!anyAreSet(state, STATE_IN_LISTENER_LOOP)) { + state |= STATE_IN_LISTENER_LOOP; + getIoThread().execute(new Runnable() { + + int loopCount = 0; + + @Override + public void run() { + try { + ChannelListener listener = getWriteListener(); + if (listener == null || !isWriteResumed()) { + return; + } + if(loopCount++ == 100) { + //should never happen + UndertowLogger.ROOT_LOGGER.listenerNotProgressing(); + IoUtils.safeClose(AbstractFramedStreamSinkChannel.this); + return; + } + ChannelListeners.invokeChannelListener((S) AbstractFramedStreamSinkChannel.this, listener); + //if writes are shutdown or we become active then we stop looping + //we stop when writes are shutdown because we can't flush until we are active + //although we may be flushed as part of a batch + + if (allAreSet(state, STATE_WRITES_RESUMED) && allAreClear(state, STATE_CLOSED) && !broken && !readyForFlush && !fullyFlushed) { + getIoThread().execute(this); + } + } finally { + state &= ~STATE_IN_LISTENER_LOOP; + } + } + }); + } + + } + + @Override + public void shutdownWrites() throws IOException { + if(anyAreSet(state, STATE_WRITES_SHUTDOWN) || broken ) { + return; + } + state |= STATE_WRITES_SHUTDOWN; + queueFinalFrame(); + } + + private void queueFinalFrame() throws IOException { + if (!readyForFlush && !fullyFlushed && allAreClear(state, STATE_CLOSED) && !broken && !finalFrameQueued) { + readyForFlush = true; + buffer.getResource().flip(); + state |= STATE_FIRST_DATA_WRITTEN; + finalFrameQueued = true; + channel.queueFrame((S) this); + } + } + + protected boolean isFinalFrameQueued() { + return finalFrameQueued; + } + + @Override + public void awaitWritable() throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + synchronized (lock) { + if (anyAreSet(state, STATE_CLOSED) || broken) { + return; + } + if (readyForFlush) { + try { + waiterCount++; + //we need to re-check after incrementing the waiters count + + if(readyForFlush && !anyAreSet(state, STATE_CLOSED) && !broken) { + lock.wait(); + } + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } finally { + waiterCount--; + } + } + } + } + + @Override + public void awaitWritable(long l, TimeUnit timeUnit) throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + synchronized (lock) { + if (anyAreSet(state, STATE_CLOSED) || broken) { + return; + } + if (readyForFlush) { + try { + waiterCount++; + if(readyForFlush && !anyAreSet(state, STATE_CLOSED) && !broken) { + lock.wait(timeUnit.toMillis(l)); + } + } catch (InterruptedException e) { + throw new InterruptedIOException(); + } finally { + waiterCount--; + } + } + } + } + + @Override + public XnioExecutor getWriteThread() { + return channel.getIoThread(); + } + + @Override + public ChannelListener.Setter getWriteSetter() { + return writeSetter; + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public XnioWorker getWorker() { + return channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return channel.getIoThread(); + } + + @Override + public boolean flush() throws IOException { + if(anyAreSet(state, STATE_CLOSED)) { + return true; + } + if (broken) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + + if (readyForFlush) { + return false; + } + if (fullyFlushed) { + state |= STATE_CLOSED; + return true; + } + if (anyAreSet(state, STATE_WRITES_SHUTDOWN) && !finalFrameQueued) { + queueFinalFrame(); + return false; + } + if(anyAreSet(state, STATE_WRITES_SHUTDOWN)) { + return false; + } + return true; + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + int state = this.state; + if (readyForFlush) { + return 0; //we can't do anything, we are waiting for a flush + } + if (anyAreSet(state, STATE_CLOSED | STATE_WRITES_SHUTDOWN) || broken) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + long copied = Buffers.copy(this.buffer.getResource(), srcs, offset, length); + if (!buffer.getResource().hasRemaining()) { + handleBufferFull(); + } + return copied; + } + + @Override + public long write(ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + @Override + public int write(ByteBuffer src) throws IOException { + int state = this.state; + if (readyForFlush) { + return 0; //we can't do anything, we are waiting for a flush + } + if (anyAreSet(state, STATE_CLOSED | STATE_WRITES_SHUTDOWN) || broken) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + int copied = Buffers.copy(this.buffer.getResource(), src); + if (!buffer.getResource().hasRemaining()) { + handleBufferFull(); + } + return copied; + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Channels.writeFinalBasic(this, srcs, offset, length); + } + + @Override + public long writeFinal(ByteBuffer[] srcs) throws IOException { + return writeFinal(srcs, 0, srcs.length); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Channels.writeFinalBasic(this, src); + } + + private void handleBufferFull() throws IOException { + if (!readyForFlush) { + readyForFlush = true; + getBuffer().flip(); + state |= STATE_FIRST_DATA_WRITTEN; + channel.queueFrame((S) this); + } + } + + /** + * @return true If this is the last frame that will be sent on this connection + */ + protected abstract boolean isLastFrame(); + + /** + * @return true if the channel is ready to be flushed. When a channel is ready to be flushed nothing should modify the buffer, + * as it may be written out by another thread. + */ + public boolean isReadyForFlush() { + return readyForFlush; + } + + /** + * Returns true writes have been shutdown + */ + public boolean isWritesShutdown() { + return anyAreSet(state, STATE_WRITES_SHUTDOWN); + } + + @Override + public boolean isOpen() { + return allAreClear(state, STATE_CLOSED); + } + + @Override + public void close() throws IOException { + if(fullyFlushed || anyAreSet(state, STATE_CLOSED)) { + return; + } + state |= STATE_CLOSED; + buffer.free(); + buffer = null; + if(header != null && header.getByteBuffer() != null) { + header.getByteBuffer().free(); + } + if(trailer != null) { + trailer.free(); + } + if(anyAreSet(state, STATE_FIRST_DATA_WRITTEN)) { + channelForciblyClosed(); + } + //we need to wake up/invoke the write listener + if(isWriteResumed()) { + ChannelListeners.invokeChannelListener(getIoThread(), this, (ChannelListener)getWriteListener()); + } + wakeupWrites(); + } + + /** + * Called when a channel has been forcibly closed, and data (frames) have already been written. + * + * The action this should take is protocol dependent, e.g. for SPDY a RST_STREAM should be sent, + * for websockets the channel should be closed. + * + * By default this will just close the underlying channel + * + * @throws IOException + */ + protected void channelForciblyClosed() throws IOException { + if(isFirstDataWritten()) { + getChannel().markWritesBroken(null); + } + wakeupWaiters(); + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option tOption) throws IOException { + return null; + } + + @Override + public T setOption(Option tOption, T t) throws IllegalArgumentException, IOException { + return null; + } + + public ByteBuffer getBuffer() { + return buffer.getResource(); + } + + /** + * Method that is invoked when a frame has been fully flushed. This method is only invoked by the IO thread + */ + final void flushComplete() throws IOException { + try { + int remaining = header.getRemainingInBuffer(); + boolean finalFrame = finalFrameQueued; + boolean channelClosed = finalFrame && remaining == 0 && !header.isAnotherFrameRequired(); + if(remaining > 0) { + buffer.getResource().limit(buffer.getResource().limit() + remaining); + if(finalFrame) { + //we clear the final frame flag, as it could not actually be written out + //note that we don't attempt to requeue, as whatever stopped it from being written will likely still + //be an issue + this.finalFrameQueued = false; + } + } else if(header.isAnotherFrameRequired()) { + this.finalFrameQueued = false; + } + if (channelClosed) { + fullyFlushed = true; + buffer.free(); + buffer = null; + } else { + buffer.getResource().compact(); + } + if (header.getByteBuffer() != null) { + header.getByteBuffer().free(); + } + trailer.free(); + header = null; + trailer = null; + + readyForFlush = false; + if (isWriteResumed() && !channelClosed) { + wakeupWrites(); + } else if(isWriteResumed()) { + //we need to execute the write listener one last time + //we need to dispatch it back to the IO thread, so we don't invoke it recursivly + ChannelListeners.invokeChannelListener(getIoThread(), (S)this, getWriteListener()); + } + + final ChannelListener closeListener = this.closeSetter.get(); + if (channelClosed && closeListener != null) { + ChannelListeners.invokeChannelListener(getIoThread(), (S) AbstractFramedStreamSinkChannel.this, closeListener); + } + handleFlushComplete(channelClosed); + } finally { + wakeupWaiters(); + } + } + + protected void handleFlushComplete(boolean finalFrame) { + + } + + protected boolean isFirstDataWritten() { + return anyAreSet(state, STATE_FIRST_DATA_WRITTEN); + } + + public void markBroken() { + this.broken = true; + try { + wakeupWrites(); + wakeupWaiters(); + if (isWriteResumed()) { + ChannelListener writeListener = this.writeSetter.get(); + if (writeListener != null) { + ChannelListeners.invokeChannelListener(getIoThread(), (S) this, writeListener); + } + } + ChannelListener closeListener = this.closeSetter.get(); + if (closeListener != null) { + ChannelListeners.invokeChannelListener(getIoThread(), (S) this, closeListener); + } + } finally { + if(header != null && header.getByteBuffer() != null) { + header.getByteBuffer().free(); + } + if(trailer != null) { + trailer.free(); + } + if(buffer != null) { + buffer.free(); + } + } + } + + ChannelListener getWriteListener() { + return writeSetter.get(); + } + + private void wakeupWaiters() { + if(waiterCount > 0) { + synchronized (lock) { + lock.notifyAll(); + } + } + } + + public C getChannel() { + return channel; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/framed/AbstractFramedStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,612 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.framed; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; +import static org.xnio.Bits.anyAreSet; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.Deque; +import java.util.LinkedList; +import java.util.concurrent.TimeUnit; +import org.xnio.Buffers; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Option; +import org.xnio.Pooled; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import io.undertow.UndertowMessages; + +/** + * Source channel, used to receive framed messages. + * + * @author Stuart Douglas + */ +public abstract class AbstractFramedStreamSourceChannel, R extends AbstractFramedStreamSourceChannel, S extends AbstractFramedStreamSinkChannel> implements StreamSourceChannel { + + private final ChannelListener.SimpleSetter readSetter = new ChannelListener.SimpleSetter(); + private final ChannelListener.SimpleSetter closeSetter = new ChannelListener.SimpleSetter(); + + private final C framedChannel; + private final Deque pendingFrameData = new LinkedList<>(); + + private int state = 0; + + private static final int STATE_DONE = 1 << 1; + private static final int STATE_READS_RESUMED = 1 << 2; + private static final int STATE_CLOSED = 1 << 3; + private static final int STATE_LAST_FRAME = 1 << 4; + private static final int STATE_IN_LISTENER_LOOP = 1 << 5; + private static final int STATE_STREAM_BROKEN = 1 << 6; + + + /** + * The backing data for the current frame. + */ + private Pooled data; + + /** + * The amount of data left in the frame. If this is larger than the data in the backing buffer then + */ + private long frameDataRemaining; + + private final Object lock = new Object(); + private int waiters; + private volatile boolean waitingForFrame; + private int readFrameCount = 0; + private long maxStreamSize = -1; + private long currentStreamSize; + + public AbstractFramedStreamSourceChannel(C framedChannel) { + this.framedChannel = framedChannel; + this.waitingForFrame = true; + } + + public AbstractFramedStreamSourceChannel(C framedChannel, Pooled data, long frameDataRemaining) { + this.framedChannel = framedChannel; + this.waitingForFrame = data == null && frameDataRemaining <= 0; + this.data = data; + this.frameDataRemaining = frameDataRemaining; + this.currentStreamSize = frameDataRemaining; + if (data != null) { + if (!data.getResource().hasRemaining()) { + data.free(); + this.data = null; + this.waitingForFrame = frameDataRemaining <= 0; + } + } + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + if (anyAreSet(state, STATE_DONE)) { + return -1; + } + beforeRead(); + if (waitingForFrame) { + return 0; + } + try { + if (frameDataRemaining == 0 && anyAreSet(state, STATE_LAST_FRAME)) { + return -1; + } else if (data != null) { + int old = data.getResource().limit(); + try { + if (count < data.getResource().remaining()) { + data.getResource().limit((int) (data.getResource().position() + count)); + } + int written = target.write(data.getResource(), position); + frameDataRemaining -= written; + return written; + } finally { + data.getResource().limit(old); + } + } + return 0; + } finally { + exitRead(); + } + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel streamSinkChannel) throws IOException { + if (anyAreSet(state, STATE_DONE)) { + return -1; + } + beforeRead(); + if (waitingForFrame) { + throughBuffer.position(throughBuffer.limit()); + return 0; + } + try { + if (frameDataRemaining == 0 && anyAreSet(state, STATE_LAST_FRAME)) { + return -1; + } else if (data != null && data.getResource().hasRemaining()) { + int old = data.getResource().limit(); + try { + throughBuffer.position(throughBuffer.limit()); + if (count < data.getResource().remaining()) { + data.getResource().limit((int) (data.getResource().position() + count)); + } + int written = streamSinkChannel.write(data.getResource()); + frameDataRemaining -= written; + return written; + } finally { + data.getResource().limit(old); + } + } else { + throughBuffer.position(throughBuffer.limit()); + } + return 0; + } finally { + exitRead(); + } + } + + public long getMaxStreamSize() { + return maxStreamSize; + } + + public void setMaxStreamSize(long maxStreamSize) { + this.maxStreamSize = maxStreamSize; + if(maxStreamSize > 0) { + if(maxStreamSize < currentStreamSize) { + handleStreamTooLarge(); + } + } + } + + private void handleStreamTooLarge() { + IoUtils.safeClose(this); + } + + @Override + public void suspendReads() { + state &= ~STATE_READS_RESUMED; + } + + /** + * Method that is invoked when all data has been read. + * + * @throws IOException + */ + protected void complete() throws IOException { + + } + + protected boolean isComplete() { + return anyAreSet(state, STATE_DONE); + } + + @Override + public void resumeReads() { + resumeReadsInternal(false); + } + + @Override + public boolean isReadResumed() { + return anyAreSet(state, STATE_READS_RESUMED); + } + + @Override + public void wakeupReads() { + resumeReadsInternal(true); + } + + /** + * For this class there is no difference between a resume and a wakeup + */ + void resumeReadsInternal(boolean wakeup) { + boolean alreadyResumed = anyAreSet(state, STATE_READS_RESUMED); + state |= STATE_READS_RESUMED; + if(!alreadyResumed || wakeup) { + if (!anyAreSet(state, STATE_IN_LISTENER_LOOP)) { + state |= STATE_IN_LISTENER_LOOP; + getIoThread().execute(new Runnable() { + + @Override + public void run() { + try { + boolean moreData; + do { + ChannelListener listener = getReadListener(); + if (listener == null || !isReadResumed()) { + return; + } + ChannelListeners.invokeChannelListener((R) AbstractFramedStreamSourceChannel.this, listener); + //if writes are shutdown or we become active then we stop looping + //we stop when writes are shutdown because we can't flush until we are active + //although we may be flushed as part of a batch + moreData = (frameDataRemaining > 0 && data != null) || !pendingFrameData.isEmpty(); + } while (allAreSet(state, STATE_READS_RESUMED) && allAreClear(state, STATE_CLOSED) && moreData); + } finally { + state &= ~STATE_IN_LISTENER_LOOP; + } + } + }); + } + } + } + + private ChannelListener getReadListener() { + return (ChannelListener) readSetter.get(); + } + + @Override + public void shutdownReads() throws IOException { + close(); + } + + protected void lastFrame() { + state |= STATE_LAST_FRAME; + waitingForFrame = false; + if(data == null && pendingFrameData.isEmpty() && frameDataRemaining == 0) { + state |= STATE_DONE | STATE_CLOSED; + getFramedChannel().notifyFrameReadComplete(this); + getFramedChannel().notifyClosed(this); + } + } + + @Override + public void awaitReadable() throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + if (data == null && pendingFrameData.isEmpty()) { + synchronized (lock) { + if (data == null && pendingFrameData.isEmpty()) { + try { + waiters++; + lock.wait(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(); + } finally { + waiters--; + } + } + } + } + } + + @Override + public void awaitReadable(long l, TimeUnit timeUnit) throws IOException { + if(Thread.currentThread() == getIoThread()) { + throw UndertowMessages.MESSAGES.awaitCalledFromIoThread(); + } + if (data == null) { + synchronized (lock) { + if (data == null) { + try { + waiters++; + lock.wait(timeUnit.toMillis(l)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new InterruptedIOException(); + } finally { + waiters--; + } + } + } + } + } + + /** + * Called when data has been read from the underlying channel. + * + * @param headerData The frame header data. This may be null if the data is part of a an existing frame + * @param frameData The frame data + */ + void dataReady(FrameHeaderData headerData, Pooled frameData) { + if(anyAreSet(state, STATE_STREAM_BROKEN)) { + frameData.free(); + return; + } + synchronized (lock) { + boolean newData = pendingFrameData.isEmpty(); + this.pendingFrameData.add(new FrameData(headerData, frameData)); + if (newData) { + if (waiters > 0) { + lock.notifyAll(); + } + } + waitingForFrame = false; + } + if (anyAreSet(state, STATE_READS_RESUMED)) { + resumeReadsInternal(true); + } + if(headerData != null) { + currentStreamSize += headerData.getFrameLength(); + if(maxStreamSize > 0 && currentStreamSize > maxStreamSize) { + handleStreamTooLarge(); + } + } + } + + protected long handleFrameData(Pooled frameData, long frameDataRemaining) { + return frameDataRemaining; + } + + protected void handleHeaderData(FrameHeaderData headerData) { + + } + + @Override + public XnioExecutor getReadThread() { + return framedChannel.getIoThread(); + } + + @Override + public ChannelListener.Setter getReadSetter() { + return readSetter; + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return closeSetter; + } + + @Override + public XnioWorker getWorker() { + return framedChannel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return framedChannel.getIoThread(); + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option tOption) throws IOException { + return null; + } + + @Override + public T setOption(Option tOption, T t) throws IllegalArgumentException, IOException { + return null; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + if (anyAreSet(state, STATE_DONE)) { + return -1; + } + beforeRead(); + if (waitingForFrame) { + return 0; + } + try { + if (frameDataRemaining == 0 && anyAreSet(state, STATE_LAST_FRAME)) { + return -1; + } else if (data != null) { + int old = data.getResource().limit(); + try { + long count = Buffers.remaining(dsts, offset, length); + if (count < data.getResource().remaining()) { + data.getResource().limit((int) (data.getResource().position() + count)); + } else { + count = data.getResource().remaining(); + } + int written = Buffers.copy((int) count, dsts, offset, length, data.getResource()); + frameDataRemaining -= written; + return written; + } finally { + data.getResource().limit(old); + } + } + return 0; + } finally { + exitRead(); + } + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + return read(dsts, 0, dsts.length); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + if (anyAreSet(state, STATE_DONE)) { + return -1; + } + if (!dst.hasRemaining()) { + return 0; + } + beforeRead(); + if (waitingForFrame) { + return 0; + } + try { + if (frameDataRemaining == 0 && anyAreSet(state, STATE_LAST_FRAME)) { + return -1; + } else if (data != null) { + int old = data.getResource().limit(); + try { + int count = dst.remaining(); + if (count < data.getResource().remaining()) { + data.getResource().limit(data.getResource().position() + count); + } else { + count = data.getResource().remaining(); + } + int written = Buffers.copy(count, dst, data.getResource()); + frameDataRemaining -= written; + return written; + } finally { + data.getResource().limit(old); + } + } + return 0; + } finally { + exitRead(); + } + } + + private void beforeRead() throws ClosedChannelException { + if (anyAreSet(state, STATE_STREAM_BROKEN)) { + throw UndertowMessages.MESSAGES.channelIsClosed(); + } + if (data == null) { + synchronized (lock) { + FrameData pending = pendingFrameData.poll(); + if (pending != null) { + Pooled frameData = pending.getFrameData(); + boolean hasData = true; + if(frameData.getResource().hasRemaining()) { + this.data = frameData; + } else { + frameData.free(); + hasData = false; + } + if (pending.getFrameHeaderData() != null) { + this.frameDataRemaining = pending.getFrameHeaderData().getFrameLength(); + handleHeaderData(pending.getFrameHeaderData()); + } + if(hasData) { + this.frameDataRemaining = handleFrameData(frameData, frameDataRemaining); + } + } + } + } + } + + private void exitRead() throws IOException { + if (data != null && !data.getResource().hasRemaining()) { + data.free(); + data = null; + } + if (frameDataRemaining == 0) { + try { + synchronized (lock) { + readFrameCount++; + if (pendingFrameData.isEmpty()) { + if (anyAreSet(state, STATE_LAST_FRAME)) { + state |= STATE_DONE; + getFramedChannel().notifyClosed(this); + complete(); + } else { + waitingForFrame = true; + } + } + } + } finally { + if (pendingFrameData.isEmpty()) { + framedChannel.notifyFrameReadComplete(this); + } + } + } + } + + @Override + public boolean isOpen() { + return allAreClear(state, STATE_CLOSED); + } + + @Override + public void close() throws IOException { + if(anyAreSet(state, STATE_CLOSED)) { + return; + } + state |= STATE_CLOSED; + if (allAreClear(state, STATE_DONE | STATE_LAST_FRAME)) { + state |= STATE_STREAM_BROKEN; + getFramedChannel().notifyClosed(this); + channelForciblyClosed(); + } + if (data != null) { + data.free(); + data = null; + } + while (!pendingFrameData.isEmpty()) { + pendingFrameData.poll().frameData.free(); + } + ChannelListeners.invokeChannelListener(this, (ChannelListener>) closeSetter.get()); + } + + protected void channelForciblyClosed() { + //TODO: what should be the default action? + //we can probably just ignore it, as it does not affect the underlying protocol + } + + protected C getFramedChannel() { + return framedChannel; + } + + protected int getReadFrameCount() { + return readFrameCount; + } + + /** + * Called when this stream is no longer valid. Reads from the stream will result + * in an exception. + */ + protected synchronized void markStreamBroken() { + state |= STATE_STREAM_BROKEN; + if(data != null) { + data.free(); + data = null; + } + for(FrameData frame : pendingFrameData) { + frame.frameData.free(); + } + pendingFrameData.clear(); + if(isReadResumed()) { + resumeReadsInternal(true); + } + if (waiters > 0) { + lock.notifyAll(); + } + } + + private class FrameData { + + private final FrameHeaderData frameHeaderData; + private final Pooled frameData; + + FrameData(FrameHeaderData frameHeaderData, Pooled frameData) { + this.frameHeaderData = frameHeaderData; + this.frameData = frameData; + } + + FrameHeaderData getFrameHeaderData() { + return frameHeaderData; + } + + Pooled getFrameData() { + return frameData; + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/framed/FrameHeaderData.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/framed/FrameHeaderData.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/framed/FrameHeaderData.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,31 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.framed; + +/** + * Frame header data for frames that are received + * + * @author Stuart Douglas + */ +public interface FrameHeaderData { + + long getFrameLength(); + + AbstractFramedStreamSourceChannel getExistingChannel(); +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/framed/FramePriority.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/framed/FramePriority.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/framed/FramePriority.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,65 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.framed; + +import java.util.Deque; +import java.util.List; + +/** + * + * Interface that can be used to determine where to insert a given frame into the pending frame queue. + * + * @author Stuart Douglas + */ +public interface FramePriority, R extends AbstractFramedStreamSourceChannel, S extends AbstractFramedStreamSinkChannel> { + + /** + * Inserts the new frame at the correct location in the pending frame list. Note that this must + * never insert a frame at the very start of the senders list, as this frame has already been activated. + * + * This method should return true if the frame was successfully inserted into the pending frame list, + * if it returns false the frame must not be inserted and will be added to the held frames list instead. + * + * Frames held in the held frames list are frames that are not yet ready to be included in the pending frame + * list, generally because other frames have to be written first. + * + * Note that if this method returns true without adding the frame the frame will be dropped. + * + * @param newFrame The new frame to insert into the pending frame list + * @param pendingFrames The pending frame list + * @return true if the frame can be inserted into the pending frame list + */ + boolean insertFrame(S newFrame, final List pendingFrames); + + /** + * Invoked when a new frame is successfully added to the pending frames queue. + * + * If frames in the held frame queue are now eligible to be sent they can be added + * to the pending frames queue. + * + * Note that if the protocol has explicitly asked for the held frames to be recalculated + * then the added frame may be null. + * + * @param addedFrame The newly added frame + * @param pendingFrames The pending frame queue + * @param holdFrames The held frame queue + */ + void frameAdded(S addedFrame, final List pendingFrames, final Deque holdFrames); + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/framed/SendFrameHeader.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/framed/SendFrameHeader.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/framed/SendFrameHeader.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,76 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.framed; + +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * @author Stuart Douglas + */ +public class SendFrameHeader { + + private final int reminingInBuffer; + private final Pooled byteBuffer; + private final boolean anotherFrameRequired; + + public SendFrameHeader(int reminingInBuffer, Pooled byteBuffer, boolean anotherFrameRequired) { + this.byteBuffer = byteBuffer; + this.reminingInBuffer = reminingInBuffer; + this.anotherFrameRequired = anotherFrameRequired; + } + + public SendFrameHeader(int reminingInBuffer, Pooled byteBuffer) { + this.byteBuffer = byteBuffer; + this.reminingInBuffer = reminingInBuffer; + this.anotherFrameRequired = false; + } + + public SendFrameHeader(Pooled byteBuffer) { + this.byteBuffer = byteBuffer; + this.reminingInBuffer = 0; + this.anotherFrameRequired = false; + } + + /** + * + * @return The header byte buffer + */ + public Pooled getByteBuffer() { + return byteBuffer; + } + + /** + * + * @return + */ + public int getRemainingInBuffer() { + return reminingInBuffer; + } + + /** + * Returns true if another frame is required after this one. Note that returning false + * does not mean that this is the last frame. This is used for protocols that require a trailing packet + * after all data has been written. + */ + public boolean isAnotherFrameRequired() { + return anotherFrameRequired; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpAttachments.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpAttachments.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpAttachments.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,55 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderMap; + +/** + * Exchange attachments that have specific meaning when using the HTTP protocol + * + * @author Stuart Douglas + */ +public class HttpAttachments { + + /** + * Attachment key for request trailers when using chunked encoding. When the request is parsed the trailers + * will be attached under this key. + */ + public static final AttachmentKey REQUEST_TRAILERS = AttachmentKey.create(HeaderMap.class); + + /** + * Attachment key for response trailers. If a header map is attached under this key then the contents will be written + * out at the end of the chunked request. + * + * Note that if pre chunked streams are being used then the trailers will not be appended to the response, however any + * trailers parsed out of the chunked stream will be attached here instead. + */ + public static final AttachmentKey RESPONSE_TRAILERS = AttachmentKey.create(HeaderMap.class); + + /** + * If the value {@code true} is attached to the exchange under this key then Undertow will assume that the underlying application + * has already taken care of chunking, and will not attempt to add its own chunk markers. + * + * This will only take effect if the application has explicitly set the {@literal Transfer-Encoding: chunked) header. + * + */ + public static final AttachmentKey PRE_CHUNKED_RESPONSE = AttachmentKey.create(Boolean.class); + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpContinue.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpContinue.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpContinue.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,205 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.UndertowMessages; +import io.undertow.io.IoCallback; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.channels.StreamSinkChannel; + +import java.io.IOException; +import java.nio.channels.Channel; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * Class that provides support for dealing with HTTP 100 (Continue) responses. + *

+ * Note that if a client is pipelining some requests and sending continue for others this + * could cause problems if the pipelining buffer is enabled. + * + * @author Stuart Douglas + */ +public class HttpContinue { + + public static final String CONTINUE = "100-continue"; + + /** + * Returns true if this exchange requires the server to send a 100 (Continue) response. + * + * @param exchange The exchange + * @return true if the server needs to send a continue response + */ + public static boolean requiresContinueResponse(final HttpServerExchange exchange) { + if (!exchange.isHttp11() || exchange.isResponseStarted()) { + return false; + } + if (exchange.getConnection() instanceof HttpServerConnection) { + if (((HttpServerConnection) exchange.getConnection()).getExtraBytes() != null) { + //we have already received some of the request body + //so according to the RFC we do not need to send the Continue + return false; + } + } + List expect = exchange.getRequestHeaders().get(Headers.EXPECT); + if (expect != null) { + for (String header : expect) { + if (header.equalsIgnoreCase(CONTINUE)) { + return true; + } + } + } + return false; + } + + /** + * Sends a continuation using async IO, and calls back when it is complete. + * + * @param exchange The exchange + * @param callback The completion callback + */ + public static void sendContinueResponse(final HttpServerExchange exchange, final IoCallback callback) { + if (!exchange.isResponseChannelAvailable()) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + internalSendContinueResponse(exchange, callback); + } + + /** + * Creates a response sender that can be used to send a HTTP 100-continue response. + * + * @param exchange The exchange + * @return The response sender + */ + public static ContinueResponseSender createResponseSender(final HttpServerExchange exchange) { + if (!exchange.isResponseChannelAvailable()) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + + HttpServerExchange newExchange = exchange.getConnection().sendOutOfBandResponse(exchange); + newExchange.setResponseCode(100); + newExchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, 0); + final StreamSinkChannel responseChannel = newExchange.getResponseChannel(); + return new ContinueResponseSender() { + boolean shutdown = false; + + @Override + public boolean send() throws IOException { + if (!shutdown) { + shutdown = true; + responseChannel.shutdownWrites(); + } + return responseChannel.flush(); + } + + @Override + public void awaitWritable() throws IOException { + responseChannel.awaitWritable(); + } + + @Override + public void awaitWritable(final long time, final TimeUnit timeUnit) throws IOException { + responseChannel.awaitWritable(time, timeUnit); + } + }; + } + + /** + * Sends a continue response using blocking IO + * + * @param exchange The exchange + */ + public static void sendContinueResponseBlocking(final HttpServerExchange exchange) throws IOException { + if (!exchange.isResponseChannelAvailable()) { + throw UndertowMessages.MESSAGES.responseChannelAlreadyProvided(); + } + HttpServerExchange newExchange = exchange.getConnection().sendOutOfBandResponse(exchange); + newExchange.setResponseCode(100); + newExchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, 0); + newExchange.startBlocking(); + newExchange.getOutputStream().close(); + newExchange.getInputStream().close(); + } + + /** + * Sets a 417 response code and ends the exchange. + * + * @param exchange The exchange to reject + */ + public static void rejectExchange(final HttpServerExchange exchange) { + exchange.setResponseCode(417); + exchange.setPersistent(false); + exchange.endExchange(); + } + + + private static void internalSendContinueResponse(final HttpServerExchange exchange, final IoCallback callback) { + HttpServerExchange newExchange = exchange.getConnection().sendOutOfBandResponse(exchange); + newExchange.setResponseCode(100); + newExchange.getResponseHeaders().put(Headers.CONTENT_LENGTH, 0); + final StreamSinkChannel responseChannel = newExchange.getResponseChannel(); + try { + responseChannel.shutdownWrites(); + if (!responseChannel.flush()) { + responseChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener( + new ChannelListener() { + @Override + public void handleEvent(StreamSinkChannel channel) { + channel.suspendWrites(); + callback.onComplete(exchange, null); + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(Channel channel, IOException e) { + callback.onException(exchange, null, e); + } + } + )); + responseChannel.resumeWrites(); + } else { + callback.onComplete(exchange, null); + } + } catch (IOException e) { + callback.onException(exchange, null, e); + } + } + + /** + * A continue response that is in the process of being sent. + */ + public interface ContinueResponseSender { + + /** + * Continue sending the response. + * + * @return true if the response is fully sent, false otherwise. + */ + boolean send() throws IOException; + + void awaitWritable() throws IOException; + + void awaitWritable(long time, final TimeUnit timeUnit) throws IOException; + + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpOpenListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpOpenListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpOpenListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,136 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.conduits.ReadTimeoutStreamSourceConduit; +import io.undertow.conduits.WriteTimeoutStreamSinkConduit; +import io.undertow.server.HttpHandler; +import io.undertow.server.OpenListener; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.Options; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * Open listener for HTTP server. XNIO should be set up to chain the accept handler to post-accept open + * listeners to this listener which actually initiates HTTP parsing. + * + * @author David M. Lloyd + */ +public final class HttpOpenListener implements ChannelListener, OpenListener { + + private final Pool bufferPool; + private final int bufferSize; + + private volatile HttpHandler rootHandler; + + private volatile OptionMap undertowOptions; + + private volatile HttpRequestParser parser; + + public HttpOpenListener(final Pool pool, final int bufferSize) { + this(pool, OptionMap.EMPTY, bufferSize); + } + + public HttpOpenListener(final Pool pool, final OptionMap undertowOptions, final int bufferSize) { + this.undertowOptions = undertowOptions; + this.bufferPool = pool; + this.bufferSize = bufferSize; + parser = HttpRequestParser.instance(undertowOptions); + } + + @Override + public void handleEvent(final StreamConnection channel) { + if (UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) { + UndertowLogger.REQUEST_LOGGER.tracef("Opened connection with %s", channel.getPeerAddress()); + } + + //set read and write timeouts + try { + Integer readTimeout = channel.getOption(Options.READ_TIMEOUT); + Integer idleTimeout = undertowOptions.get(UndertowOptions.IDLE_TIMEOUT); + if ((readTimeout == null || readTimeout <= 0) && idleTimeout != null) { + readTimeout = idleTimeout; + } else if (readTimeout != null && idleTimeout != null && idleTimeout > 0) { + readTimeout = Math.min(readTimeout, idleTimeout); + } + if (readTimeout != null && readTimeout > 0) { + channel.getSourceChannel().setConduit(new ReadTimeoutStreamSourceConduit(channel.getSourceChannel().getConduit(), channel, this)); + } + Integer writeTimeout = channel.getOption(Options.WRITE_TIMEOUT); + if ((writeTimeout == null || writeTimeout <= 0) && idleTimeout != null) { + writeTimeout = idleTimeout; + } else if (writeTimeout != null && idleTimeout != null && idleTimeout > 0) { + writeTimeout = Math.min(writeTimeout, idleTimeout); + } + if (writeTimeout != null && writeTimeout > 0) { + channel.getSinkChannel().setConduit(new WriteTimeoutStreamSinkConduit(channel.getSinkChannel().getConduit(), channel, this)); + } + } catch (IOException e) { + IoUtils.safeClose(channel); + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + } + + + HttpServerConnection connection = new HttpServerConnection(channel, bufferPool, rootHandler, undertowOptions, bufferSize); + HttpReadListener readListener = new HttpReadListener(connection, parser); + + connection.setReadListener(readListener); + readListener.newRequest(); + channel.getSourceChannel().setReadListener(readListener); + readListener.handleEvent(channel.getSourceChannel()); + } + + @Override + public HttpHandler getRootHandler() { + return rootHandler; + } + + @Override + public void setRootHandler(final HttpHandler rootHandler) { + this.rootHandler = rootHandler; + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public void setUndertowOptions(final OptionMap undertowOptions) { + if (undertowOptions == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("undertowOptions"); + } + this.undertowOptions = undertowOptions; + this.parser = HttpRequestParser.instance(undertowOptions); + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpReadListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpReadListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpReadListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,297 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.conduits.ReadDataStreamSourceConduit; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ClosingChannelExceptionHandler; +import io.undertow.util.StringWriteChannelListener; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * Listener which reads requests and headers off of an HTTP stream. + * + * @author David M. Lloyd + */ +final class HttpReadListener implements ChannelListener, Runnable { + + private static final String BAD_REQUEST = "HTTP/1.1 400 Bad Request\r\nContent-Length: 0\r\nConnection: close\r\n\r\n"; + + private final HttpServerConnection connection; + private final ParseState state = new ParseState(); + private final HttpRequestParser parser; + + private HttpServerExchange httpServerExchange; + + private int read = 0; + private final int maxRequestSize; + private final long maxEntitySize; + private final boolean recordRequestStartTime; + + //0 = new request ok, reads resumed + //1 = request running, new request not ok + //2 = suspending/resuming in progress + private volatile int requestState; + + private static final AtomicIntegerFieldUpdater requestStateUpdater = AtomicIntegerFieldUpdater.newUpdater(HttpReadListener.class, "requestState"); + + HttpReadListener(final HttpServerConnection connection, final HttpRequestParser parser) { + this.connection = connection; + this.parser = parser; + this.maxRequestSize = connection.getUndertowOptions().get(UndertowOptions.MAX_HEADER_SIZE, UndertowOptions.DEFAULT_MAX_HEADER_SIZE); + this.maxEntitySize = connection.getUndertowOptions().get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); + this.recordRequestStartTime = connection.getUndertowOptions().get(UndertowOptions.RECORD_REQUEST_START_TIME, false); + } + + public void newRequest() { + state.reset(); + read = 0; + httpServerExchange = new HttpServerExchange(connection, maxEntitySize); + } + + public void handleEvent(final ConduitStreamSourceChannel channel) { + while (requestStateUpdater.get(this) != 0) { + //if the CAS fails it is because another thread is in the process of changing state + //we just immediately retry + if (requestStateUpdater.compareAndSet(this, 1, 2)) { + channel.suspendReads(); + requestStateUpdater.set(this, 1); + return; + } + } + handleEventWithNoRunningRequest(channel); + } + + public void handleEventWithNoRunningRequest(final ConduitStreamSourceChannel channel) { + Pooled existing = connection.getExtraBytes(); + if ((existing == null && connection.getOriginalSourceConduit().isReadShutdown()) || connection.getOriginalSinkConduit().isWriteShutdown()) { + IoUtils.safeClose(connection); + channel.suspendReads(); + return; + } + + + final Pooled pooled = existing == null ? connection.getBufferPool().allocate() : existing; + final ByteBuffer buffer = pooled.getResource(); + boolean free = true; + + try { + int res; + do { + if (existing == null) { + buffer.clear(); + try { + res = channel.read(buffer); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.debug("Error reading request", e); + IoUtils.safeClose(connection); + return; + } + } else { + res = buffer.remaining(); + } + + if (res <= 0) { + handleFailedRead(channel, res); + return; + } + if (existing != null) { + existing = null; + connection.setExtraBytes(null); + } else { + buffer.flip(); + } + parser.handle(buffer, state, httpServerExchange); + if (buffer.hasRemaining()) { + free = false; + connection.setExtraBytes(pooled); + } + int total = read + res; + read = total; + if (read > maxRequestSize) { + UndertowLogger.REQUEST_LOGGER.requestHeaderWasTooLarge(connection.getPeerAddress(), maxRequestSize); + IoUtils.safeClose(connection); + return; + } + } while (!state.isComplete()); + + final HttpServerExchange httpServerExchange = this.httpServerExchange; + httpServerExchange.setRequestScheme(connection.getSslSession() != null ? "https" : "http"); + this.httpServerExchange = null; + requestStateUpdater.set(this, 1); + HttpTransferEncoding.setupRequest(httpServerExchange); + if (recordRequestStartTime) { + Connectors.setRequestStartTime(httpServerExchange); + } + connection.setCurrentExchange(httpServerExchange); + Connectors.executeRootHandler(connection.getRootHandler(), httpServerExchange); + } catch (Exception e) { + sendBadRequestAndClose(connection.getChannel(), e); + return; + } finally { + if (free) pooled.free(); + } + } + + private void handleFailedRead(ConduitStreamSourceChannel channel, int res) { + if (res == 0) { + channel.setReadListener(this); + channel.resumeReads(); + } else if (res == -1) { + handleConnectionClose(channel); + } + } + + private void handleConnectionClose(StreamSourceChannel channel) { + try { + channel.suspendReads(); + channel.shutdownReads(); + final StreamSinkChannel responseChannel = this.connection.getChannel().getSinkChannel(); + responseChannel.shutdownWrites(); + IoUtils.safeClose(connection); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.debug("Error reading request", e); + // fuck it, it's all ruined + IoUtils.safeClose(connection); + } + } + + private void sendBadRequestAndClose(final StreamConnection connection, final Exception exception) { + UndertowLogger.REQUEST_IO_LOGGER.failedToParseRequest(exception); + connection.getSourceChannel().suspendReads(); + new StringWriteChannelListener(BAD_REQUEST) { + @Override + protected void writeDone(final StreamSinkChannel c) { + super.writeDone(c); + c.suspendWrites(); + IoUtils.safeClose(connection); + } + + @Override + protected void handleError(StreamSinkChannel channel, IOException e) { + IoUtils.safeClose(connection); + } + }.setup(connection.getSinkChannel()); + } + + public void exchangeComplete(final HttpServerExchange exchange) { + connection.clearChannel(); + final HttpServerConnection connection = this.connection; + if (exchange.isPersistent() && !exchange.isUpgrade()) { + final StreamConnection channel = connection.getChannel(); + if (connection.getExtraBytes() == null) { + //if we are not pipelining we just register a listener + //we have to resume from with the io thread + if (exchange.isInIoThread()) { + //no need for CAS, we are in the IO thread + newRequest(); + channel.getSourceChannel().setReadListener(HttpReadListener.this); + channel.getSourceChannel().resumeReads(); + requestStateUpdater.set(this, 0); + } else { + while (true) { + if (connection.getOriginalSourceConduit().isReadShutdown() || connection.getOriginalSinkConduit().isWriteShutdown()) { + channel.getSourceChannel().suspendReads(); + channel.getSinkChannel().suspendWrites(); + IoUtils.safeClose(connection); + return; + } else { + if (requestStateUpdater.compareAndSet(this, 1, 2)) { + newRequest(); + channel.getSourceChannel().setReadListener(HttpReadListener.this); + requestStateUpdater.set(this, 0); + channel.getSourceChannel().resumeReads(); + break; + } + } + } + } + } else { + if (exchange.isInIoThread()) { + requestStateUpdater.set(this, 0); //no need to CAS, as we don't actually resume + newRequest(); + //no need to suspend reads here, the task will always run before the read listener anyway + channel.getIoThread().execute(this); + } else { + while (true) { + if (connection.getOriginalSinkConduit().isWriteShutdown()) { + channel.getSourceChannel().suspendReads(); + channel.getSinkChannel().suspendWrites(); + IoUtils.safeClose(connection); + return; + } else if (requestStateUpdater.compareAndSet(this, 1, 2)) { + newRequest(); + channel.getSourceChannel().suspendReads(); + requestStateUpdater.set(this, 0); + break; + } + } + Executor executor = exchange.getDispatchExecutor(); + if (executor == null) { + executor = exchange.getConnection().getWorker(); + } + executor.execute(this); + } + } + } else if (!exchange.isPersistent()) { + IoUtils.safeClose(connection); + } else if (exchange.isUpgrade()) { + if (connection.getExtraBytes() != null) { + connection.getChannel().getSourceChannel().setConduit(new ReadDataStreamSourceConduit(connection.getChannel().getSourceChannel().getConduit(), connection)); + } + try { + if (!connection.getChannel().getSinkChannel().flush()) { + connection.getChannel().getSinkChannel().setWriteListener(ChannelListeners.flushingChannelListener(new ChannelListener() { + @Override + public void handleEvent(ConduitStreamSinkChannel conduitStreamSinkChannel) { + connection.getUpgradeListener().handleUpgrade(connection.getChannel(), exchange); + } + }, new ClosingChannelExceptionHandler(connection))); + connection.getChannel().getSinkChannel().resumeWrites(); + return; + } + connection.getUpgradeListener().handleUpgrade(connection.getChannel(), exchange); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(connection); + } + } + } + + @Override + public void run() { + handleEvent(connection.getChannel().getSourceChannel()); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpRequestParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpRequestParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpRequestParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,840 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.Map; + +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.annotationprocessor.HttpParserConfig; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import io.undertow.util.Protocols; +import io.undertow.util.URLUtils; +import org.xnio.OptionMap; + +import static io.undertow.util.Headers.ACCEPT_CHARSET_STRING; +import static io.undertow.util.Headers.ACCEPT_ENCODING_STRING; +import static io.undertow.util.Headers.ACCEPT_LANGUAGE_STRING; +import static io.undertow.util.Headers.ACCEPT_RANGES_STRING; +import static io.undertow.util.Headers.ACCEPT_STRING; +import static io.undertow.util.Headers.AUTHORIZATION_STRING; +import static io.undertow.util.Headers.CACHE_CONTROL_STRING; +import static io.undertow.util.Headers.CONNECTION_STRING; +import static io.undertow.util.Headers.CONTENT_LENGTH_STRING; +import static io.undertow.util.Headers.CONTENT_TYPE_STRING; +import static io.undertow.util.Headers.COOKIE_STRING; +import static io.undertow.util.Headers.EXPECT_STRING; +import static io.undertow.util.Headers.FROM_STRING; +import static io.undertow.util.Headers.HOST_STRING; +import static io.undertow.util.Headers.IF_MATCH_STRING; +import static io.undertow.util.Headers.IF_MODIFIED_SINCE_STRING; +import static io.undertow.util.Headers.IF_NONE_MATCH_STRING; +import static io.undertow.util.Headers.IF_RANGE_STRING; +import static io.undertow.util.Headers.IF_UNMODIFIED_SINCE_STRING; +import static io.undertow.util.Headers.MAX_FORWARDS_STRING; +import static io.undertow.util.Headers.ORIGIN_STRING; +import static io.undertow.util.Headers.PRAGMA_STRING; +import static io.undertow.util.Headers.PROXY_AUTHORIZATION_STRING; +import static io.undertow.util.Headers.RANGE_STRING; +import static io.undertow.util.Headers.REFERER_STRING; +import static io.undertow.util.Headers.REFRESH_STRING; +import static io.undertow.util.Headers.SEC_WEB_SOCKET_KEY_STRING; +import static io.undertow.util.Headers.SEC_WEB_SOCKET_VERSION_STRING; +import static io.undertow.util.Headers.SERVER_STRING; +import static io.undertow.util.Headers.SSL_CIPHER_STRING; +import static io.undertow.util.Headers.SSL_CIPHER_USEKEYSIZE_STRING; +import static io.undertow.util.Headers.SSL_CLIENT_CERT_STRING; +import static io.undertow.util.Headers.SSL_SESSION_ID_STRING; +import static io.undertow.util.Headers.STRICT_TRANSPORT_SECURITY_STRING; +import static io.undertow.util.Headers.TRAILER_STRING; +import static io.undertow.util.Headers.TRANSFER_ENCODING_STRING; +import static io.undertow.util.Headers.UPGRADE_STRING; +import static io.undertow.util.Headers.USER_AGENT_STRING; +import static io.undertow.util.Headers.VIA_STRING; +import static io.undertow.util.Headers.WARNING_STRING; +import static io.undertow.util.Methods.CONNECT_STRING; +import static io.undertow.util.Methods.DELETE_STRING; +import static io.undertow.util.Methods.GET_STRING; +import static io.undertow.util.Methods.HEAD_STRING; +import static io.undertow.util.Methods.OPTIONS_STRING; +import static io.undertow.util.Methods.POST_STRING; +import static io.undertow.util.Methods.PUT_STRING; +import static io.undertow.util.Methods.TRACE_STRING; +import static io.undertow.util.Protocols.HTTP_0_9_STRING; +import static io.undertow.util.Protocols.HTTP_1_0_STRING; +import static io.undertow.util.Protocols.HTTP_1_1_STRING; + +/** + * The basic HTTP parser. The actual parser is a sub class of this class that is generated as part of + * the build process by the {@link io.undertow.annotationprocessor.AbstractParserGenerator} annotation processor. + *

+ * The actual processor is a state machine, that means that for common header, method, protocol values + * it will return an interned string, rather than creating a new string for each one. + *

+ * + * @author Stuart Douglas + */ +@HttpParserConfig(methods = { + OPTIONS_STRING, + GET_STRING, + HEAD_STRING, + POST_STRING, + PUT_STRING, + DELETE_STRING, + TRACE_STRING, + CONNECT_STRING}, + protocols = { + HTTP_0_9_STRING, HTTP_1_0_STRING, HTTP_1_1_STRING + }, + headers = { + ACCEPT_STRING, + ACCEPT_CHARSET_STRING, + ACCEPT_ENCODING_STRING, + ACCEPT_LANGUAGE_STRING, + ACCEPT_RANGES_STRING, + AUTHORIZATION_STRING, + CACHE_CONTROL_STRING, + COOKIE_STRING, + CONNECTION_STRING, + CONTENT_LENGTH_STRING, + CONTENT_TYPE_STRING, + EXPECT_STRING, + FROM_STRING, + HOST_STRING, + IF_MATCH_STRING, + IF_MODIFIED_SINCE_STRING, + IF_NONE_MATCH_STRING, + IF_RANGE_STRING, + IF_UNMODIFIED_SINCE_STRING, + MAX_FORWARDS_STRING, + ORIGIN_STRING, + PRAGMA_STRING, + PROXY_AUTHORIZATION_STRING, + RANGE_STRING, + REFERER_STRING, + REFRESH_STRING, + SEC_WEB_SOCKET_KEY_STRING, + SEC_WEB_SOCKET_VERSION_STRING, + SERVER_STRING, + SSL_CLIENT_CERT_STRING, + SSL_CIPHER_STRING, + SSL_SESSION_ID_STRING, + SSL_CIPHER_USEKEYSIZE_STRING, + STRICT_TRANSPORT_SECURITY_STRING, + TRAILER_STRING, + TRANSFER_ENCODING_STRING, + UPGRADE_STRING, + USER_AGENT_STRING, + VIA_STRING, + WARNING_STRING + }) +public abstract class HttpRequestParser { + + private static final byte[] HTTP; + public static final int HTTP_LENGTH; + + private final int maxParameters; + private final int maxHeaders; + private final boolean allowEncodedSlash; + private final boolean decode; + private final String charset; + + static { + try { + HTTP = "HTTP/1.".getBytes("ASCII"); + HTTP_LENGTH = HTTP.length; + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + public HttpRequestParser(OptionMap options) { + maxParameters = options.get(UndertowOptions.MAX_PARAMETERS, 1000); + maxHeaders = options.get(UndertowOptions.MAX_HEADERS, 200); + allowEncodedSlash = options.get(UndertowOptions.ALLOW_ENCODED_SLASH, false); + decode = options.get(UndertowOptions.DECODE_URL, true); + charset = options.get(UndertowOptions.URL_CHARSET, "UTF-8"); + } + + public static final HttpRequestParser instance(final OptionMap options) { + try { + final Class cls = HttpRequestParser.class.getClassLoader().loadClass(HttpRequestParser.class.getName() + "$$generated"); + + Constructor ctor = cls.getConstructor(OptionMap.class); + return (HttpRequestParser) ctor.newInstance(options); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + + public void handle(ByteBuffer buffer, final ParseState currentState, final HttpServerExchange builder) { + if (currentState.state == ParseState.VERB) { + //fast path, we assume that it will parse fully so we avoid all the if statements + + //fast path HTTP GET requests, basically just assume all requests are get + //and fall out to the state machine if it is not + final int position = buffer.position(); + if (buffer.remaining() > 3 + && buffer.get(position) == 'G' + && buffer.get(position + 1) == 'E' + && buffer.get(position + 2) == 'T' + && buffer.get(position + 3) == ' ') { + buffer.position(position + 4); + builder.setRequestMethod(Methods.GET); + currentState.state = ParseState.PATH; + } else { + handleHttpVerb(buffer, currentState, builder); + } + handlePath(buffer, currentState, builder); + boolean failed = false; + if (buffer.remaining() > HTTP_LENGTH + 3) { + int pos = buffer.position(); + for (int i = 0; i < HTTP_LENGTH; ++i) { + if (HTTP[i] != buffer.get(pos + i)) { + failed = true; + break; + } + } + if (!failed) { + final byte b = buffer.get(pos + HTTP_LENGTH); + final byte b2 = buffer.get(pos + HTTP_LENGTH + 1); + final byte b3 = buffer.get(pos + HTTP_LENGTH + 2); + if (b2 == '\r' && b3 == '\n') { + if (b == '1') { + builder.setProtocol(Protocols.HTTP_1_1); + buffer.position(pos + HTTP_LENGTH + 3); + currentState.state = ParseState.HEADER; + } else if (b == '0') { + builder.setProtocol(Protocols.HTTP_1_0); + buffer.position(pos + HTTP_LENGTH + 3); + currentState.state = ParseState.HEADER; + } else { + failed = true; + } + } else { + failed = true; + } + } + } else { + failed = true; + } + if (failed) { + handleHttpVersion(buffer, currentState, builder); + handleAfterVersion(buffer, currentState); + } + + while (currentState.state != ParseState.PARSE_COMPLETE && buffer.hasRemaining()) { + handleHeader(buffer, currentState, builder); + if (currentState.state == ParseState.HEADER_VALUE) { + handleHeaderValue(buffer, currentState, builder); + } + } + return; + } + handleStateful(buffer, currentState, builder); + } + + private void handleStateful(ByteBuffer buffer, ParseState currentState, HttpServerExchange builder) { + if (currentState.state == ParseState.PATH) { + handlePath(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + + if (currentState.state == ParseState.QUERY_PARAMETERS) { + handleQueryParameters(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + + if (currentState.state == ParseState.PATH_PARAMETERS) { + handlePathParameters(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + + if (currentState.state == ParseState.VERSION) { + handleHttpVersion(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + if (currentState.state == ParseState.AFTER_VERSION) { + handleAfterVersion(buffer, currentState); + if (!buffer.hasRemaining()) { + return; + } + } + while (currentState.state != ParseState.PARSE_COMPLETE) { + if (currentState.state == ParseState.HEADER) { + handleHeader(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + if (currentState.state == ParseState.HEADER_VALUE) { + handleHeaderValue(buffer, currentState, builder); + if (!buffer.hasRemaining()) { + return; + } + } + } + } + + + abstract void handleHttpVerb(ByteBuffer buffer, final ParseState currentState, final HttpServerExchange builder); + + abstract void handleHttpVersion(ByteBuffer buffer, final ParseState currentState, final HttpServerExchange builder); + + abstract void handleHeader(ByteBuffer buffer, final ParseState currentState, final HttpServerExchange builder); + + /** + * The parse states for parsing the path. + */ + private static final int START = 0; + private static final int FIRST_COLON = 1; + private static final int FIRST_SLASH = 2; + private static final int SECOND_SLASH = 3; + private static final int IN_PATH = 4; + private static final int HOST_DONE = 5; + + /** + * Parses a path value + * + * @param buffer The buffer + * @param state The current state + * @param exchange The exchange builder + * @return The number of bytes remaining + */ + @SuppressWarnings("unused") + final void handlePath(ByteBuffer buffer, ParseState state, HttpServerExchange exchange) { + StringBuilder stringBuilder = state.stringBuilder; + int parseState = state.parseState; + int canonicalPathStart = state.pos; + boolean urlDecodeRequired = state.urlDecodeRequired; + + while (buffer.hasRemaining()) { + char next = (char) buffer.get(); + if (next == ' ' || next == '\t') { + if (stringBuilder.length() != 0) { + final String path = stringBuilder.toString(); + if (parseState < HOST_DONE) { + String decodedPath = decode(path, urlDecodeRequired, state, allowEncodedSlash); + exchange.setRequestPath(decodedPath); + exchange.setRelativePath(decodedPath); + exchange.setRequestURI(path); + } else { + handleFullUrl(state, exchange, canonicalPathStart, urlDecodeRequired, path); + } + exchange.setQueryString(""); + state.state = ParseState.VERSION; + state.stringBuilder.setLength(0); + state.parseState = 0; + state.pos = 0; + state.urlDecodeRequired = false; + return; + } + } else if (next == '\r' || next == '\n') { + throw UndertowMessages.MESSAGES.failedToParsePath(); + } else if (next == '?' && (parseState == START || parseState == HOST_DONE || parseState == IN_PATH)) { + beginQueryParameters(buffer, state, exchange, stringBuilder, parseState, canonicalPathStart, urlDecodeRequired); + return; + } else if (next == ';' && (parseState == START || parseState == HOST_DONE || parseState == IN_PATH)) { + beginPathParameters(state, exchange, stringBuilder, parseState, canonicalPathStart, urlDecodeRequired); + handlePathParameters(buffer, state, exchange); + return; + } else { + + if (decode && (next == '+' || next == '%')) { + urlDecodeRequired = true; + } else if (next == ':' && parseState == START) { + parseState = FIRST_COLON; + } else if (next == '/' && parseState == FIRST_COLON) { + parseState = FIRST_SLASH; + } else if (next == '/' && parseState == FIRST_SLASH) { + parseState = SECOND_SLASH; + } else if (next == '/' && parseState == SECOND_SLASH) { + parseState = HOST_DONE; + canonicalPathStart = stringBuilder.length(); + } else if (parseState == FIRST_COLON || parseState == FIRST_SLASH) { + parseState = IN_PATH; + } else if (next == '/' && parseState != HOST_DONE) { + parseState = IN_PATH; + } + stringBuilder.append(next); + } + + } + state.parseState = parseState; + state.pos = canonicalPathStart; + state.urlDecodeRequired = urlDecodeRequired; + } + + private void beginPathParameters(ParseState state, HttpServerExchange exchange, StringBuilder stringBuilder, int parseState, int canonicalPathStart, boolean urlDecodeRequired) { + final String path = stringBuilder.toString(); + if (parseState < HOST_DONE) { + String decodedPath = decode(path, urlDecodeRequired, state, allowEncodedSlash); + exchange.setRequestPath(decodedPath); + exchange.setRelativePath(decodedPath); + exchange.setRequestURI(path); + } else { + String thePath = path.substring(canonicalPathStart); + exchange.setRequestPath(thePath); + exchange.setRelativePath(thePath); + exchange.setRequestURI(path, true); + } + state.state = ParseState.PATH_PARAMETERS; + state.stringBuilder.setLength(0); + state.parseState = 0; + state.pos = 0; + state.urlDecodeRequired = false; + } + + private void beginQueryParameters(ByteBuffer buffer, ParseState state, HttpServerExchange exchange, StringBuilder stringBuilder, int parseState, int canonicalPathStart, boolean urlDecodeRequired) { + final String path = stringBuilder.toString(); + if (parseState < HOST_DONE) { + String decodedPath = decode(path, urlDecodeRequired, state, allowEncodedSlash); + exchange.setRequestPath(decodedPath); + exchange.setRelativePath(decodedPath); + exchange.setRequestURI(path, false); + } else { + handleFullUrl(state, exchange, canonicalPathStart, urlDecodeRequired, path); + } + state.state = ParseState.QUERY_PARAMETERS; + state.stringBuilder.setLength(0); + state.parseState = 0; + state.pos = 0; + state.urlDecodeRequired = false; + handleQueryParameters(buffer, state, exchange); + } + + private void handleFullUrl(ParseState state, HttpServerExchange exchange, int canonicalPathStart, boolean urlDecodeRequired, String path) { + String thePath = decode(path.substring(canonicalPathStart), urlDecodeRequired, state, allowEncodedSlash); + exchange.setRequestPath(thePath); + exchange.setRelativePath(thePath); + exchange.setRequestURI(path, true); + } + + + /** + * Parses a path value + * + * @param buffer The buffer + * @param state The current state + * @param exchange The exchange builder + * @return The number of bytes remaining + */ + @SuppressWarnings("unused") + final void handleQueryParameters(ByteBuffer buffer, ParseState state, HttpServerExchange exchange) { + StringBuilder stringBuilder = state.stringBuilder; + int queryParamPos = state.pos; + int mapCount = state.mapCount; + boolean urlDecodeRequired = state.urlDecodeRequired; + String nextQueryParam = state.nextQueryParam; + + //so this is a bit funky, because it not only deals with parsing, but + //also deals with URL decoding the query parameters as well, while also + //maintaining a non-decoded version to use as the query string + //In most cases these string will be the same, and as we do not want to + //build up two separate strings we don't use encodedStringBuilder unless + //we encounter an encoded character + + while (buffer.hasRemaining()) { + char next = (char) buffer.get(); + if (next == ' ' || next == '\t') { + final String queryString = stringBuilder.toString(); + exchange.setQueryString(queryString); + if (nextQueryParam == null) { + if (queryParamPos != stringBuilder.length()) { + exchange.addQueryParam(decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true), ""); + } + } else { + exchange.addQueryParam(nextQueryParam, decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true)); + } + state.state = ParseState.VERSION; + state.stringBuilder.setLength(0); + state.pos = 0; + state.nextQueryParam = null; + state.urlDecodeRequired = false; + state.mapCount = 0; + return; + } else if (next == '\r' || next == '\n') { + throw UndertowMessages.MESSAGES.failedToParsePath(); + } else { + if (decode && (next == '+' || next == '%')) { + urlDecodeRequired = true; + } else if (next == '=' && nextQueryParam == null) { + nextQueryParam = decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true); + urlDecodeRequired = false; + queryParamPos = stringBuilder.length() + 1; + } else if (next == '&' && nextQueryParam == null) { + if (mapCount++ > maxParameters) { + throw UndertowMessages.MESSAGES.tooManyQueryParameters(maxParameters); + } + if (queryParamPos != stringBuilder.length()) { + exchange.addQueryParam(decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true), ""); + } + urlDecodeRequired = false; + queryParamPos = stringBuilder.length() + 1; + } else if (next == '&') { + if (mapCount++ > maxParameters) { + throw UndertowMessages.MESSAGES.tooManyQueryParameters(maxParameters); + } + exchange.addQueryParam(nextQueryParam, decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true)); + urlDecodeRequired = false; + queryParamPos = stringBuilder.length() + 1; + nextQueryParam = null; + } + stringBuilder.append(next); + + } + + } + state.pos = queryParamPos; + state.nextQueryParam = nextQueryParam; + state.urlDecodeRequired = urlDecodeRequired; + state.mapCount = 0; + } + + private String decode(final String value, boolean urlDecodeRequired, ParseState state, final boolean allowEncodedSlash) { + if (urlDecodeRequired) { + return URLUtils.decode(value, charset, allowEncodedSlash, state.decodeBuffer); + } else { + return value; + } + } + + + final void handlePathParameters(ByteBuffer buffer, ParseState state, HttpServerExchange exchange) { + StringBuilder stringBuilder = state.stringBuilder; + int queryParamPos = state.pos; + int mapCount = state.mapCount; + boolean urlDecodeRequired = state.urlDecodeRequired; + String nextQueryParam = state.nextQueryParam; + + //so this is a bit funky, because it not only deals with parsing, but + //also deals with URL decoding the query parameters as well, while also + //maintaining a non-decoded version to use as the query string + //In most cases these string will be the same, and as we do not want to + //build up two separate strings we don't use encodedStringBuilder unless + //we encounter an encoded character + + while (buffer.hasRemaining()) { + char next = (char) buffer.get(); + if (next == ' ' || next == '\t' || next == '?') { + if (nextQueryParam == null) { + if (queryParamPos != stringBuilder.length()) { + exchange.addPathParam(decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true), ""); + } + } else { + exchange.addPathParam(nextQueryParam, decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true)); + } + exchange.setRequestURI(exchange.getRequestURI() + ';' + stringBuilder.toString(), state.parseState > HOST_DONE); + state.stringBuilder.setLength(0); + state.pos = 0; + state.nextQueryParam = null; + state.mapCount = 0; + state.urlDecodeRequired = false; + if (next == '?') { + state.state = ParseState.QUERY_PARAMETERS; + handleQueryParameters(buffer, state, exchange); + } else { + state.state = ParseState.VERSION; + } + return; + } else if (next == '\r' || next == '\n') { + throw UndertowMessages.MESSAGES.failedToParsePath(); + } else { + if (decode && (next == '+' || next == '%')) { + urlDecodeRequired = true; + } + if (next == '=' && nextQueryParam == null) { + nextQueryParam = decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true); + urlDecodeRequired = false; + queryParamPos = stringBuilder.length() + 1; + } else if (next == '&' && nextQueryParam == null) { + if (mapCount++ > maxParameters) { + throw UndertowMessages.MESSAGES.tooManyQueryParameters(maxParameters); + } + exchange.addPathParam(decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true), ""); + urlDecodeRequired = false; + queryParamPos = stringBuilder.length() + 1; + } else if (next == '&') { + if (mapCount++ > maxParameters) { + throw UndertowMessages.MESSAGES.tooManyQueryParameters(maxParameters); + } + + exchange.addPathParam(nextQueryParam, decode(stringBuilder.substring(queryParamPos), urlDecodeRequired, state, true)); + urlDecodeRequired = false; + queryParamPos = stringBuilder.length() + 1; + nextQueryParam = null; + } + stringBuilder.append(next); + + } + + } + state.pos = queryParamPos; + state.nextQueryParam = nextQueryParam; + state.mapCount = 0; + state.urlDecodeRequired = urlDecodeRequired; + } + + + /** + * The parse states for parsing heading values + */ + private static final int NORMAL = 0; + private static final int WHITESPACE = 1; + private static final int BEGIN_LINE_END = 2; + private static final int LINE_END = 3; + private static final int AWAIT_DATA_END = 4; + + /** + * Parses a header value. This is called from the generated bytecode. + * + * @param buffer The buffer + * @param state The current state + * @param builder The exchange builder + * @return The number of bytes remaining + */ + @SuppressWarnings("unused") + final void handleHeaderValue(ByteBuffer buffer, ParseState state, HttpServerExchange builder) { + HttpString headerName = state.nextHeader; + StringBuilder stringBuilder = state.stringBuilder; + HashMap headerValuesCache = state.headerValuesCache; + if (stringBuilder.length() == 0) { + String existing = headerValuesCache.get(headerName); + if (existing != null) { + if (handleCachedHeader(existing, buffer, state, builder)) { + return; + } + } + } + + handleHeaderValueCacheMiss(buffer, state, builder, headerName, headerValuesCache, stringBuilder); + } + + private void handleHeaderValueCacheMiss(ByteBuffer buffer, ParseState state, HttpServerExchange builder, HttpString headerName, HashMap headerValuesCache, StringBuilder stringBuilder) { + + int parseState = state.parseState; + while (buffer.hasRemaining() && parseState == NORMAL) { + final byte next = buffer.get(); + if (next == '\r') { + parseState = BEGIN_LINE_END; + } else if (next == '\n') { + parseState = LINE_END; + } else if (next == ' ' || next == '\t') { + parseState = WHITESPACE; + } else { + stringBuilder.append((char) next); + } + } + + while (buffer.hasRemaining()) { + final byte next = buffer.get(); + switch (parseState) { + case NORMAL: { + if (next == '\r') { + parseState = BEGIN_LINE_END; + } else if (next == '\n') { + parseState = LINE_END; + } else if (next == ' ' || next == '\t') { + parseState = WHITESPACE; + } else { + stringBuilder.append((char) next); + } + break; + } + case WHITESPACE: { + if (next == '\r') { + parseState = BEGIN_LINE_END; + } else if (next == '\n') { + parseState = LINE_END; + } else if (next == ' ' || next == '\t') { + } else { + if (stringBuilder.length() > 0) { + stringBuilder.append(' '); + } + stringBuilder.append((char) next); + parseState = NORMAL; + } + break; + } + case LINE_END: + case BEGIN_LINE_END: { + if (next == '\n' && parseState == BEGIN_LINE_END) { + parseState = LINE_END; + } else if (next == '\t' || + next == ' ') { + //this is a continuation + parseState = WHITESPACE; + } else { + //we have a header + String headerValue = stringBuilder.toString(); + + + if (state.mapCount++ > maxHeaders) { + throw UndertowMessages.MESSAGES.tooManyHeaders(maxHeaders); + } + //TODO: we need to decode this according to RFC-2047 if we have seen a =? symbol + builder.getRequestHeaders().add(headerName, headerValue); + if(headerValuesCache.size() < maxHeaders) { + //we have a limit on how many we can cache + //to prevent memory filling and hash collision attacks + headerValuesCache.put(headerName, headerValue); + } + + state.nextHeader = null; + + state.leftOver = next; + state.stringBuilder.setLength(0); + if (next == '\r') { + parseState = AWAIT_DATA_END; + } else { + state.state = ParseState.HEADER; + state.parseState = 0; + return; + } + } + break; + } + case AWAIT_DATA_END: { + state.state = ParseState.PARSE_COMPLETE; + return; + } + } + } + //we only write to the state if we did not finish parsing + state.parseState = parseState; + } + + protected boolean handleCachedHeader(String existing, ByteBuffer buffer, ParseState state, HttpServerExchange builder) { + int pos = buffer.position(); + while (pos < buffer.limit() && buffer.get(pos) == ' ') { + pos++; + } + if (existing.length() + 3 + pos > buffer.limit()) { + return false; + } + int i = 0; + while (i < existing.length()) { + byte b = buffer.get(pos + i); + if (b != existing.charAt(i)) { + return false; + } + ++i; + } + if (buffer.get(pos + i++) != '\r') { + return false; + } + if (buffer.get(pos + i++) != '\n') { + return false; + } + int next = buffer.get(pos + i); + if (next == '\t' || next == ' ') { + //continuation + return false; + } + buffer.position(pos + i); + if (state.mapCount++ > maxHeaders) { + throw UndertowMessages.MESSAGES.tooManyHeaders(maxHeaders); + } + //TODO: we need to decode this according to RFC-2047 if we have seen a =? symbol + builder.getRequestHeaders().add(state.nextHeader, existing); + + state.nextHeader = null; + + state.state = ParseState.HEADER; + state.parseState = 0; + return true; + } + + protected void handleAfterVersion(ByteBuffer buffer, ParseState state) { + boolean newLine = state.leftOver == '\n'; + while (buffer.hasRemaining()) { + final byte next = buffer.get(); + if (newLine) { + if (next == '\n') { + state.state = ParseState.PARSE_COMPLETE; + return; + } else { + state.state = ParseState.HEADER; + state.leftOver = next; + return; + } + } else { + if (next == '\n') { + newLine = true; + } else if (next != '\r' && next != ' ' && next != '\t') { + state.state = ParseState.HEADER; + state.leftOver = next; + return; + } else { + throw UndertowMessages.MESSAGES.badRequest(); + } + } + } + if (newLine) { + state.leftOver = '\n'; + } + } + + /** + * This is a bit of hack to enable the parser to get access to the HttpString's that are sorted + * in the static fields of the relevant classes. This means that in most cases a HttpString comparison + * will take the fast path == route, as they will be the same object + * + * @return + */ + protected static Map httpStrings() { + final Map results = new HashMap<>(); + final Class[] classs = {Headers.class, Methods.class, Protocols.class}; + + for (Class c : classs) { + for (Field field : c.getDeclaredFields()) { + if (field.getType().equals(HttpString.class)) { + field.setAccessible(true); + HttpString result = null; + try { + result = (HttpString) field.get(null); + results.put(result.toString(), result); + } catch (IllegalAccessException e) { + throw new RuntimeException(e); + } + } + } + } + return results; + + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpResponseConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpResponseConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpResponseConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,689 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; + +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.TruncatedResponseException; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; + +import org.xnio.Buffers; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.allAreSet; + +/** + * @author David M. Lloyd + */ +final class HttpResponseConduit extends AbstractStreamSinkConduit { + + private final Pool pool; + + private int state = STATE_START; + + private long fiCookie = -1L; + private String string; + private HeaderValues headerValues; + private int valueIdx; + private int charIndex; + private Pooled pooledBuffer; + private HttpServerExchange exchange; + + private ByteBuffer[] writevBuffer; + private boolean done = false; + + private static final int STATE_BODY = 0; // Message body, normal pass-through operation + private static final int STATE_START = 1; // No headers written yet + private static final int STATE_HDR_NAME = 2; // Header name indexed by charIndex + private static final int STATE_HDR_D = 3; // Header delimiter ':' + private static final int STATE_HDR_DS = 4; // Header delimiter ': ' + private static final int STATE_HDR_VAL = 5; // Header value + private static final int STATE_HDR_EOL_CR = 6; // Header line CR + private static final int STATE_HDR_EOL_LF = 7; // Header line LF + private static final int STATE_HDR_FINAL_CR = 8; // Final CR + private static final int STATE_HDR_FINAL_LF = 9; // Final LF + private static final int STATE_BUF_FLUSH = 10; // flush the buffer and go to writing body + + private static final int MASK_STATE = 0x0000000F; + private static final int FLAG_SHUTDOWN = 0x00000010; + + HttpResponseConduit(final StreamSinkConduit next, final Pool pool) { + super(next); + this.pool = pool; + } + + HttpResponseConduit(final StreamSinkConduit next, final Pool pool, HttpServerExchange exchange) { + super(next); + this.pool = pool; + this.exchange = exchange; + } + void reset(HttpServerExchange exchange) { + + this.exchange = exchange; + state = STATE_START; + fiCookie = -1L; + string = null; + headerValues = null; + valueIdx = 0; + charIndex = 0; + } + + /** + * Handles writing out the header data. It can also take a byte buffer of user + * data, to enable both user data and headers to be written out in a single operation, + * which has a noticeable performance impact. + *

+ * It is up to the caller to note the current position of this buffer before and after they + * call this method, and use this to figure out how many bytes (if any) have been written. + * + * @param state + * @param userData + * @return + * @throws IOException + */ + private int processWrite(int state, final Object userData, int pos, int length) throws IOException { + if(done) { + throw new ClosedChannelException(); + } + assert state != STATE_BODY; + if (state == STATE_BUF_FLUSH) { + final ByteBuffer byteBuffer = pooledBuffer.getResource(); + do { + long res = 0; + ByteBuffer[] data; + if (userData == null || length == 0) { + res = next.write(byteBuffer); + } else if (userData instanceof ByteBuffer){ + data = writevBuffer; + if(data == null) { + data = writevBuffer = new ByteBuffer[2]; + } + data[0] = byteBuffer; + data[1] = (ByteBuffer) userData; + res = next.write(data, 0, 2); + } else { + data = writevBuffer; + if(data == null || data.length < length + 1) { + data = writevBuffer = new ByteBuffer[length + 1]; + } + data[0] = byteBuffer; + System.arraycopy(userData, pos, data, 1, length); + res = next.write(data, 0, data.length); + } + if (res == 0) { + return STATE_BUF_FLUSH; + } + } while (byteBuffer.hasRemaining()); + bufferDone(); + return STATE_BODY; + } else if (state != STATE_START) { + return processStatefulWrite(state, userData, pos, length); + } + + //merge the cookies into the header map + Connectors.flattenCookies(exchange); + + if(pooledBuffer == null) { + pooledBuffer = pool.allocate(); + } + ByteBuffer buffer = pooledBuffer.getResource(); + + + assert buffer.remaining() >= 0x100; + exchange.getProtocol().appendTo(buffer); + buffer.put((byte) ' '); + int code = exchange.getResponseCode(); + assert 999 >= code && code >= 100; + buffer.put((byte) (code / 100 + '0')); + buffer.put((byte) (code / 10 % 10 + '0')); + buffer.put((byte) (code % 10 + '0')); + buffer.put((byte) ' '); + String string = StatusCodes.getReason(code); + writeString(buffer, string); + buffer.put((byte) '\r').put((byte) '\n'); + + int remaining = buffer.remaining(); + + + HeaderMap headers = exchange.getResponseHeaders(); + long fiCookie = headers.fastIterateNonEmpty(); + while (fiCookie != -1) { + HeaderValues headerValues = headers.fiCurrent(fiCookie); + + HttpString header = headerValues.getHeaderName(); + int headerSize = header.length(); + int valueIdx = 0; + while (valueIdx < headerValues.size()) { + remaining -= (headerSize + 2); + + if (remaining < 0) { + this.fiCookie = fiCookie; + this.string = string; + this.headerValues = headerValues; + this.valueIdx = valueIdx; + this.charIndex = 0; + this.state = STATE_HDR_NAME; + buffer.flip(); + return processStatefulWrite(STATE_HDR_NAME, userData, pos, length); + } + header.appendTo(buffer); + buffer.put((byte) ':').put((byte) ' '); + string = headerValues.get(valueIdx++); + + remaining -= (string.length() + 2); + if (remaining < 2) {//we use 2 here, to make sure we always have room for the final \r\n + this.fiCookie = fiCookie; + this.string = string; + this.headerValues = headerValues; + this.valueIdx = valueIdx; + this.charIndex = 0; + this.state = STATE_HDR_VAL; + buffer.flip(); + return processStatefulWrite(STATE_HDR_VAL, userData, pos ,length); + } + writeString(buffer, string); + buffer.put((byte) '\r').put((byte) '\n'); + } + fiCookie = headers.fiNextNonEmpty(fiCookie); + } + buffer.put((byte) '\r').put((byte) '\n'); + buffer.flip(); + do { + long res = 0; + ByteBuffer[] data; + if (userData == null) { + res = next.write(buffer); + } else if (userData instanceof ByteBuffer){ + data = writevBuffer; + if(data == null) { + data = writevBuffer = new ByteBuffer[2]; + } + data[0] = buffer; + data[1] = (ByteBuffer) userData; + res = next.write(data, 0, 2); + } else { + data = writevBuffer; + if(data == null || data.length < length + 1) { + data = writevBuffer = new ByteBuffer[length + 1]; + } + data[0] = buffer; + System.arraycopy(userData, pos, data, 1, length); + res = next.write(data, 0, data.length); + } + if (res == 0) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + bufferDone(); + return STATE_BODY; + } + + private void bufferDone() { + HttpServerConnection connection = (HttpServerConnection)exchange.getConnection(); + if(connection.getExtraBytes() != null && connection.isOpen() && exchange.isRequestComplete()) { + //if we are pipelining we hold onto the buffer + pooledBuffer.getResource().clear(); + } else { + + pooledBuffer.free(); + pooledBuffer = null; + } + } + + private static void writeString(ByteBuffer buffer, String string) { + int length = string.length(); + for (int charIndex = 0; charIndex < length; charIndex++) { + buffer.put((byte) string.charAt(charIndex)); + } + } + + + /** + * Handles writing out the header data in the case where is is too big to fit into a buffer. This is a much slower code path. + */ + private int processStatefulWrite(int state, final Object userData, int pos, int len) throws IOException { + ByteBuffer buffer = pooledBuffer.getResource(); + long fiCookie = this.fiCookie; + int valueIdx = this.valueIdx; + int charIndex = this.charIndex; + int length; + String string = this.string; + HeaderValues headerValues = this.headerValues; + int res; + // BUFFER IS FLIPPED COMING IN + if (buffer.hasRemaining()) { + do { + res = next.write(buffer); + if (res == 0) { + return state; + } + } while (buffer.hasRemaining()); + } + buffer.clear(); + HeaderMap headers = exchange.getResponseHeaders(); + // BUFFER IS NOW EMPTY FOR FILLING + for (; ; ) { + switch (state) { + case STATE_HDR_NAME: { + final HttpString headerName = headerValues.getHeaderName(); + length = headerName.length(); + while (charIndex < length) { + if (buffer.hasRemaining()) { + buffer.put(headerName.byteAt(charIndex++)); + } else { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerValues = headerValues; + this.charIndex = charIndex; + this.fiCookie = fiCookie; + this.valueIdx = valueIdx; + return STATE_HDR_NAME; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + } + // fall thru + } + case STATE_HDR_D: { + if (!buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerValues = headerValues; + this.charIndex = charIndex; + this.fiCookie = fiCookie; + this.valueIdx = valueIdx; + return STATE_HDR_D; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) ':'); + // fall thru + } + case STATE_HDR_DS: { + if (!buffer.hasRemaining()) { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerValues = headerValues; + this.charIndex = charIndex; + this.fiCookie = fiCookie; + this.valueIdx = valueIdx; + return STATE_HDR_DS; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + buffer.put((byte) ' '); + //if (valueIterator == null) { + // valueIterator = exchange.getResponseHeaders().get(headerName).iterator(); + //} + string = headerValues.get(valueIdx++); + charIndex = 0; + // fall thru + } + case STATE_HDR_VAL: { + length = string.length(); + while (charIndex < length) { + if (buffer.hasRemaining()) { + buffer.put((byte) string.charAt(charIndex++)); + } else { + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerValues = headerValues; + this.charIndex = charIndex; + this.fiCookie = fiCookie; + this.valueIdx = valueIdx; + return STATE_HDR_VAL; + } + } while (buffer.hasRemaining()); + buffer.clear(); + } + } + charIndex = 0; + if (valueIdx == headerValues.size()) { + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_EOL_CR; + } + buffer.put((byte) 13); // CR + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_EOL_LF; + } + buffer.put((byte) 10); // LF + if ((fiCookie = headers.fiNextNonEmpty(fiCookie)) != -1L) { + headerValues = headers.fiCurrent(fiCookie); + valueIdx = 0; + state = STATE_HDR_NAME; + break; + } else { + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_FINAL_CR; + } + buffer.put((byte) 13); // CR + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_FINAL_LF; + } + buffer.put((byte) 10); // LF + this.fiCookie = -1; + this.valueIdx = 0; + this.string = null; + buffer.flip(); + //for performance reasons we use a gather write if there is user data + if (userData == null) { + do { + res = next.write(buffer); + if (res == 0) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } else if(userData instanceof ByteBuffer) { + ByteBuffer[] b = {buffer, (ByteBuffer) userData}; + do { + long r = next.write(b, 0, b.length); + if (r == 0 && buffer.hasRemaining()) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } else { + ByteBuffer[] b = new ByteBuffer[1 + len]; + b[0] = buffer; + System.arraycopy(userData, pos, b, 1, len); + do { + long r = next.write(b, 0, b.length); + if (r == 0 && buffer.hasRemaining()) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } + bufferDone(); + return STATE_BODY; + } + // not reached + } + // fall thru + } + // Clean-up states + case STATE_HDR_EOL_CR: { + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_EOL_CR; + } + buffer.put((byte) 13); // CR + } + case STATE_HDR_EOL_LF: { + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_EOL_LF; + } + buffer.put((byte) 10); // LF + if (valueIdx < headerValues.size()) { + state = STATE_HDR_NAME; + break; + } else if ((fiCookie = headers.fiNextNonEmpty(fiCookie)) != -1L) { + headerValues = headers.fiCurrent(fiCookie); + valueIdx = 0; + state = STATE_HDR_NAME; + break; + } + // fall thru + } + case STATE_HDR_FINAL_CR: { + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_FINAL_CR; + } + buffer.put((byte) 13); // CR + // fall thru + } + case STATE_HDR_FINAL_LF: { + if (!buffer.hasRemaining()) { + if (flushHeaderBuffer(buffer, string, headerValues, charIndex, fiCookie, valueIdx)) + return STATE_HDR_FINAL_LF; + } + buffer.put((byte) 10); // LF + this.fiCookie = -1L; + this.valueIdx = 0; + this.string = null; + buffer.flip(); + //for performance reasons we use a gather write if there is user data + if (userData == null) { + do { + res = next.write(buffer); + if (res == 0) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } else if(userData instanceof ByteBuffer) { + ByteBuffer[] b = {buffer, (ByteBuffer) userData}; + do { + long r = next.write(b, 0, b.length); + if (r == 0 && buffer.hasRemaining()) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } else { + ByteBuffer[] b = new ByteBuffer[1 + len]; + b[0] = buffer; + System.arraycopy(userData, pos, b, 1, len); + do { + long r = next.write(b, 0, b.length); + if (r == 0 && buffer.hasRemaining()) { + return STATE_BUF_FLUSH; + } + } while (buffer.hasRemaining()); + } + // fall thru + } + case STATE_BUF_FLUSH: { + // buffer was successfully flushed above + bufferDone(); + return STATE_BODY; + } + default: { + throw new IllegalStateException(); + } + } + } + } + + private boolean flushHeaderBuffer(ByteBuffer buffer, String string, HeaderValues headerValues, int charIndex, long fiCookie, int valueIdx) throws IOException { + int res; + buffer.flip(); + do { + res = next.write(buffer); + if (res == 0) { + this.string = string; + this.headerValues = headerValues; + this.charIndex = charIndex; + this.fiCookie = fiCookie; + this.valueIdx = valueIdx; + return true; + } + } while (buffer.hasRemaining()); + buffer.clear(); + return false; + } + + public int write(final ByteBuffer src) throws IOException { + int oldState = this.state; + int state = oldState & MASK_STATE; + int alreadyWritten = 0; + int originalRemaining = -1; + try { + if (state != 0) { + originalRemaining = src.remaining(); + state = processWrite(state, src, -1, -1); + if (state != 0) { + return 0; + } + alreadyWritten = originalRemaining - src.remaining(); + if (allAreSet(oldState, FLAG_SHUTDOWN)) { + next.terminateWrites(); + throw new ClosedChannelException(); + } + } + if (alreadyWritten != originalRemaining) { + return next.write(src) + alreadyWritten; + } + return alreadyWritten; + } finally { + this.state = oldState & ~MASK_STATE | state; + } + } + + public long write(final ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if (length == 0) { + return 0L; + } + int oldVal = state; + int state = oldVal & MASK_STATE; + try { + if (state != 0) { + long rem = Buffers.remaining(srcs, offset, length); + state = processWrite(state, srcs, offset, length); + + long ret = rem - Buffers.remaining(srcs, offset, length); + if (state != 0) { + return ret; + } + if (allAreSet(oldVal, FLAG_SHUTDOWN)) { + next.terminateWrites(); + throw new ClosedChannelException(); + } + //we don't attempt to write again + return ret; + } + return length == 1 ? next.write(srcs[offset]) : next.write(srcs, offset, length); + } finally { + this.state = oldVal & ~MASK_STATE | state; + } + } + + public long transferFrom(final FileChannel src, final long position, final long count) throws IOException { + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + public long transferFrom(final StreamSourceChannel source, final long count, final ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + public boolean flush() throws IOException { + int oldVal = state; + int state = oldVal & MASK_STATE; + try { + if (state != 0) { + state = processWrite(state, null, -1, -1); + if (state != 0) { + return false; + } + if (allAreSet(oldVal, FLAG_SHUTDOWN)) { + next.terminateWrites(); + // fall out to the flush + } + } + return next.flush(); + } finally { + this.state = oldVal & ~MASK_STATE | state; + } + } + + + public void terminateWrites() throws IOException { + int oldVal = this.state; + if (allAreClear(oldVal, MASK_STATE)) { + next.terminateWrites(); + return; + } + this.state = oldVal | FLAG_SHUTDOWN; + } + + public void truncateWrites() throws IOException { + int oldVal = this.state; + if (allAreClear(oldVal, MASK_STATE)) { + try { + next.truncateWrites(); + } finally { + if (pooledBuffer != null) { + bufferDone(); + } + } + return; + } + this.state = oldVal & ~MASK_STATE | FLAG_SHUTDOWN | STATE_BODY; + throw new TruncatedResponseException(); + } + + public XnioWorker getWorker() { + return next.getWorker(); + } + + void freeBuffers() { + done = true; + if(pooledBuffer != null) { + bufferDone(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpServerConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpServerConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpServerConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,256 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.UndertowMessages; +import io.undertow.conduits.ReadDataStreamSourceConduit; +import io.undertow.server.AbstractServerConnection; +import io.undertow.server.ConduitWrapper; +import io.undertow.server.ConnectionSSLSessionInfo; +import io.undertow.server.Connectors; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.ServerConnection; +import io.undertow.util.ConduitFactory; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.ImmediatePooled; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.SslChannel; +import org.xnio.conduits.StreamSinkConduit; + +import javax.net.ssl.SSLSession; +import java.nio.ByteBuffer; + +/** + * A server-side HTTP connection. + *

+ * Note that the lifecycle of the server connection is tied to the underlying TCP connection. Even if the channel + * is upgraded the connection is not considered closed until the upgraded channel is closed. + * + * @author David M. Lloyd + */ +public final class HttpServerConnection extends AbstractServerConnection { + + private SSLSessionInfo sslSessionInfo; + private HttpReadListener readListener; + private PipeliningBufferingStreamSinkConduit pipelineBuffer; + private HttpResponseConduit responseConduit; + private ServerFixedLengthStreamSinkConduit fixedLengthStreamSinkConduit; + private ReadDataStreamSourceConduit readDataStreamSourceConduit; + + private HttpUpgradeListener upgradeListener; + + public HttpServerConnection(StreamConnection channel, final Pool bufferPool, final HttpHandler rootHandler, final OptionMap undertowOptions, final int bufferSize) { + super(channel, bufferPool, rootHandler, undertowOptions, bufferSize); + if (channel instanceof SslChannel) { + sslSessionInfo = new ConnectionSSLSessionInfo(((SslChannel) channel), this); + } + this.responseConduit = new HttpResponseConduit(channel.getSinkChannel().getConduit(), bufferPool); + + fixedLengthStreamSinkConduit = new ServerFixedLengthStreamSinkConduit(responseConduit, false, false); + readDataStreamSourceConduit = new ReadDataStreamSourceConduit(channel.getSourceChannel().getConduit(), this); + //todo: do this without an allocation + addCloseListener(new CloseListener() { + @Override + public void closed(ServerConnection connection) { + if(getExtraBytes() != null) { + getExtraBytes().free(); + } + responseConduit.freeBuffers(); + } + }); + + } + + @Override + public HttpServerExchange sendOutOfBandResponse(HttpServerExchange exchange) { + if (exchange == null || !HttpContinue.requiresContinueResponse(exchange)) { + throw UndertowMessages.MESSAGES.outOfBandResponseOnlyAllowedFor100Continue(); + } + final ConduitState state = resetChannel(); + HttpServerExchange newExchange = new HttpServerExchange(this); + for (HttpString header : exchange.getRequestHeaders().getHeaderNames()) { + newExchange.getRequestHeaders().putAll(header, exchange.getRequestHeaders().get(header)); + } + newExchange.setProtocol(exchange.getProtocol()); + newExchange.setRequestMethod(exchange.getRequestMethod()); + exchange.setRequestURI(exchange.getRequestURI(), exchange.isHostIncludedInRequestURI()); + exchange.setRequestPath(exchange.getRequestPath()); + exchange.setRelativePath(exchange.getRelativePath()); + newExchange.getRequestHeaders().put(Headers.CONNECTION, Headers.KEEP_ALIVE.toString()); + newExchange.getRequestHeaders().put(Headers.CONTENT_LENGTH, 0); + newExchange.setPersistent(true); + + Connectors.terminateRequest(newExchange); + newExchange.addResponseWrapper(new ConduitWrapper() { + @Override + public StreamSinkConduit wrap(ConduitFactory factory, HttpServerExchange exchange) { + + ServerFixedLengthStreamSinkConduit fixed = new ServerFixedLengthStreamSinkConduit(new HttpResponseConduit(getSinkChannel().getConduit(), getBufferPool(), exchange), false, false); + fixed.reset(0, exchange); + return fixed; + } + }); + + //we restore the read channel immediately, as this out of band response has no read side + channel.getSourceChannel().setConduit(source(state)); + newExchange.addExchangeCompleteListener(new ExchangeCompletionListener() { + @Override + public void exchangeEvent(HttpServerExchange exchange, NextListener nextListener) { + restoreChannel(state); + } + }); + return newExchange; + } + + @Override + public void terminateRequestChannel(HttpServerExchange exchange) { + + } + + /** + * Pushes back the given data. This should only be used by transfer coding handlers that have read past + * the end of the request when handling pipelined requests + * + * @param unget The buffer to push back + */ + public void ungetRequestBytes(final Pooled unget) { + if (getExtraBytes() == null) { + setExtraBytes(unget); + } else { + Pooled eb = getExtraBytes(); + ByteBuffer buf = eb.getResource(); + final ByteBuffer ugBuffer = unget.getResource(); + + if (ugBuffer.limit() - ugBuffer.remaining() > buf.remaining()) { + //stuff the existing data after the data we are ungetting + ugBuffer.compact(); + ugBuffer.put(buf); + ugBuffer.flip(); + eb.free(); + setExtraBytes(unget); + } else { + //TODO: this is horrible, but should not happen often + final byte[] data = new byte[ugBuffer.remaining() + buf.remaining()]; + int first = ugBuffer.remaining(); + ugBuffer.get(data, 0, ugBuffer.remaining()); + buf.get(data, first, buf.remaining()); + eb.free(); + unget.free(); + final ByteBuffer newBuffer = ByteBuffer.wrap(data); + setExtraBytes(new ImmediatePooled<>(newBuffer)); + } + } + } + + @Override + public SSLSessionInfo getSslSessionInfo() { + return sslSessionInfo; + } + + @Override + public void setSslSessionInfo(SSLSessionInfo sessionInfo) { + this.sslSessionInfo = sessionInfo; + } + + public SSLSession getSslSession() { + if (channel instanceof SslChannel) { + return ((SslChannel) channel).getSslSession(); + } + return null; + } + + @Override + protected StreamConnection upgradeChannel() { + clearChannel(); + if (extraBytes != null) { + channel.getSourceChannel().setConduit(new ReadDataStreamSourceConduit(channel.getSourceChannel().getConduit(), this)); + } + return channel; + } + + @Override + protected StreamSinkConduit getSinkConduit(HttpServerExchange exchange, StreamSinkConduit conduit) { + return HttpTransferEncoding.createSinkConduit(exchange); + } + + @Override + protected boolean isUpgradeSupported() { + return true; + } + + void setReadListener(HttpReadListener readListener) { + this.readListener = readListener; + } + + @Override + protected void exchangeComplete(HttpServerExchange exchange) { + if (pipelineBuffer == null) { + readListener.exchangeComplete(exchange); + } else { + pipelineBuffer.exchangeComplete(exchange); + } + } + + HttpReadListener getReadListener() { + return readListener; + } + + ReadDataStreamSourceConduit getReadDataStreamSourceConduit() { + return readDataStreamSourceConduit; + } + + public PipeliningBufferingStreamSinkConduit getPipelineBuffer() { + return pipelineBuffer; + } + + public HttpResponseConduit getResponseConduit() { + return responseConduit; + } + + ServerFixedLengthStreamSinkConduit getFixedLengthStreamSinkConduit() { + return fixedLengthStreamSinkConduit; + } + + protected HttpUpgradeListener getUpgradeListener() { + return upgradeListener; + } + + @Override + protected void setUpgradeListener(HttpUpgradeListener upgradeListener) { + this.upgradeListener = upgradeListener; + } + + void setCurrentExchange(HttpServerExchange exchange) { + this.current = exchange; + } + + public void setPipelineBuffer(PipeliningBufferingStreamSinkConduit pipelineBuffer) { + this.pipelineBuffer = pipelineBuffer; + this.responseConduit = new HttpResponseConduit(pipelineBuffer, bufferPool); + this.fixedLengthStreamSinkConduit = new ServerFixedLengthStreamSinkConduit(responseConduit, false, false); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpTransferEncoding.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpTransferEncoding.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/HttpTransferEncoding.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,346 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.conduits.ChunkedStreamSinkConduit; +import io.undertow.conduits.ChunkedStreamSourceConduit; +import io.undertow.conduits.ConduitListener; +import io.undertow.conduits.FinishableStreamSinkConduit; +import io.undertow.conduits.FixedLengthStreamSourceConduit; +import io.undertow.conduits.HeadStreamSinkConduit; +import io.undertow.conduits.PreChunkedStreamSinkConduit; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.DateUtils; +import io.undertow.util.HeaderMap; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.Methods; +import org.jboss.logging.Logger; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceConduit; + +/** + * Class that is responsible for HTTP transfer encoding, this could be part of the {@link HttpReadListener}, + * but is separated out for clarity. + *

+ * For more info see http://tools.ietf.org/html/rfc2616#section-4.4 + * + * @author Stuart Douglas + * @author David M. Lloyd + */ +class HttpTransferEncoding { + + private static final Logger log = Logger.getLogger("io.undertow.server.handler.transfer-encoding"); + + + /** + * Construct a new instance. + */ + private HttpTransferEncoding() { + } + + public static void setupRequest(final HttpServerExchange exchange) { + final HeaderMap requestHeaders = exchange.getRequestHeaders(); + final String connectionHeader = requestHeaders.getFirst(Headers.CONNECTION); + final String transferEncodingHeader = requestHeaders.getLast(Headers.TRANSFER_ENCODING); + final String contentLengthHeader = requestHeaders.getFirst(Headers.CONTENT_LENGTH); + + final HttpServerConnection connection = (HttpServerConnection) exchange.getConnection(); + //if we are already using the pipelineing buffer add it to the exchange + PipeliningBufferingStreamSinkConduit pipeliningBuffer = connection.getPipelineBuffer(); + if (pipeliningBuffer != null) { + pipeliningBuffer.setupPipelineBuffer(exchange); + } + ConduitStreamSourceChannel sourceChannel = connection.getChannel().getSourceChannel(); + sourceChannel.setConduit(connection.getReadDataStreamSourceConduit()); + + boolean persistentConnection = persistentConnection(exchange, connectionHeader); + + if (transferEncodingHeader == null && contentLengthHeader == null) { + if (persistentConnection + && connection.getExtraBytes() != null + && pipeliningBuffer == null + && connection.getUndertowOptions().get(UndertowOptions.BUFFER_PIPELINED_DATA, false)) { + pipeliningBuffer = new PipeliningBufferingStreamSinkConduit(connection.getOriginalSinkConduit(), connection.getBufferPool()); + connection.setPipelineBuffer(pipeliningBuffer); + pipeliningBuffer.setupPipelineBuffer(exchange); + } + // no content - immediately start the next request, returning an empty stream for this one + Connectors.terminateRequest(exchange); + } else { + persistentConnection = handleRequestEncoding(exchange, transferEncodingHeader, contentLengthHeader, connection, pipeliningBuffer, persistentConnection); + } + + exchange.setPersistent(persistentConnection); + + if (!exchange.isRequestComplete() || connection.getExtraBytes() != null) { + //if there is more data we suspend reads + sourceChannel.setReadListener(null); + sourceChannel.suspendReads(); + } + + } + + private static boolean handleRequestEncoding(final HttpServerExchange exchange, String transferEncodingHeader, String contentLengthHeader, HttpServerConnection connection, PipeliningBufferingStreamSinkConduit pipeliningBuffer, boolean persistentConnection) { + + HttpString transferEncoding = Headers.IDENTITY; + if (transferEncodingHeader != null) { + transferEncoding = new HttpString(transferEncodingHeader); + } + if (transferEncodingHeader != null && !transferEncoding.equals(Headers.IDENTITY)) { + ConduitStreamSourceChannel sourceChannel = ((HttpServerConnection) exchange.getConnection()).getChannel().getSourceChannel(); + sourceChannel.setConduit(new ChunkedStreamSourceConduit(sourceChannel.getConduit(), exchange, chunkedDrainListener(exchange))); + } else if (contentLengthHeader != null) { + final long contentLength; + contentLength = parsePositiveLong(contentLengthHeader); + if (contentLength == 0L) { + log.trace("No content, starting next request"); + // no content - immediately start the next request, returning an empty stream for this one + Connectors.terminateRequest(exchange); + } else { + // fixed-length content - add a wrapper for a fixed-length stream + ConduitStreamSourceChannel sourceChannel = ((HttpServerConnection) exchange.getConnection()).getChannel().getSourceChannel(); + sourceChannel.setConduit(fixedLengthStreamSourceConduitWrapper(contentLength, sourceChannel.getConduit(), exchange)); + } + } else if (transferEncodingHeader != null) { + //identity transfer encoding + log.trace("Connection not persistent (no content length and identity transfer encoding)"); + // make it not persistent + persistentConnection = false; + } else if (persistentConnection) { + //we have no content and a persistent request. This may mean we need to use the pipelining buffer to improve + //performance + if (connection.getExtraBytes() != null + && pipeliningBuffer == null + && connection.getUndertowOptions().get(UndertowOptions.BUFFER_PIPELINED_DATA, false)) { + pipeliningBuffer = new PipeliningBufferingStreamSinkConduit(connection.getOriginalSinkConduit(), connection.getBufferPool()); + connection.setPipelineBuffer(pipeliningBuffer); + pipeliningBuffer.setupPipelineBuffer(exchange); + } + + // no content - immediately start the next request, returning an empty stream for this one + Connectors.terminateRequest(exchange); + } else { + //assume there is no content + //we still know there is no content + Connectors.terminateRequest(exchange); + } + return persistentConnection; + } + + private static boolean persistentConnection(HttpServerExchange exchange, String connectionHeader) { + if (exchange.isHttp11()) { + return !(connectionHeader != null && Headers.CLOSE.equalToString(connectionHeader)); + } else if (exchange.isHttp10()) { + if (connectionHeader != null) { + if (Headers.KEEP_ALIVE.equals(new HttpString(connectionHeader))) { + return true; + } + } + } + log.trace("Connection not persistent"); + return false; + } + + private static StreamSourceConduit fixedLengthStreamSourceConduitWrapper(final long contentLength, final StreamSourceConduit conduit, final HttpServerExchange exchange) { + return new FixedLengthStreamSourceConduit(conduit, contentLength, fixedLengthDrainListener(exchange), exchange); + } + + private static ConduitListener fixedLengthDrainListener(final HttpServerExchange exchange) { + return new ConduitListener() { + public void handleEvent(final FixedLengthStreamSourceConduit fixedLengthConduit) { + long remaining = fixedLengthConduit.getRemaining(); + if (remaining > 0L) { + UndertowLogger.REQUEST_LOGGER.requestWasNotFullyConsumed(); + exchange.setPersistent(false); + } + Connectors.terminateRequest(exchange); + } + }; + } + + private static ConduitListener chunkedDrainListener(final HttpServerExchange exchange) { + return new ConduitListener() { + public void handleEvent(final ChunkedStreamSourceConduit chunkedStreamSourceConduit) { + if (!chunkedStreamSourceConduit.isFinished()) { + UndertowLogger.REQUEST_LOGGER.requestWasNotFullyConsumed(); + exchange.setPersistent(false); + } + Connectors.terminateRequest(exchange); + } + }; + } + + private static ConduitListener terminateResponseListener(final HttpServerExchange exchange) { + return new ConduitListener() { + public void handleEvent(final StreamSinkConduit channel) { + Connectors.terminateResponse(exchange); + } + }; + } + + static StreamSinkConduit createSinkConduit(final HttpServerExchange exchange) { + DateUtils.addDateHeaderIfRequired(exchange); + + boolean headRequest = exchange.getRequestMethod().equals(Methods.HEAD); + HttpServerConnection serverConnection = (HttpServerConnection) exchange.getConnection(); + + HttpResponseConduit responseConduit = serverConnection.getResponseConduit(); + responseConduit.reset(exchange); + StreamSinkConduit channel = responseConduit; + if (headRequest) { + //if this is a head request we add a head channel underneath the content encoding channel + //this will just discard the data + //we still go through with the rest of the logic, to make sure all headers are set correctly + channel = new HeadStreamSinkConduit(channel, terminateResponseListener(exchange)); + } + + final HeaderMap responseHeaders = exchange.getResponseHeaders(); + // test to see if we're still persistent + String connection = responseHeaders.getFirst(Headers.CONNECTION); + if (!exchange.isPersistent()) { + responseHeaders.put(Headers.CONNECTION, Headers.CLOSE.toString()); + } else if (exchange.isPersistent() && connection != null) { + if (HttpString.tryFromString(connection).equals(Headers.CLOSE)) { + exchange.setPersistent(false); + } + } else if (exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALWAYS_SET_KEEP_ALIVE, true)) { + responseHeaders.put(Headers.CONNECTION, Headers.KEEP_ALIVE.toString()); + } + //according to the HTTP RFC we should ignore content length if a transfer coding is specified + final String transferEncodingHeader = responseHeaders.getLast(Headers.TRANSFER_ENCODING); + if(transferEncodingHeader == null) { + final String contentLengthHeader = responseHeaders.getFirst(Headers.CONTENT_LENGTH); + if (contentLengthHeader != null) { + StreamSinkConduit res = handleFixedLength(exchange, headRequest, channel, responseHeaders, contentLengthHeader, serverConnection); + if (res != null) { + return res; + } + } + } + return handleResponseConduit(exchange, headRequest, channel, responseHeaders, terminateResponseListener(exchange), transferEncodingHeader); + } + + private static StreamSinkConduit handleFixedLength(HttpServerExchange exchange, boolean headRequest, StreamSinkConduit channel, HeaderMap responseHeaders, String contentLengthHeader, HttpServerConnection connection) { + try { + final long contentLength = parsePositiveLong(contentLengthHeader); + if (headRequest) { + return channel; + } + // fixed-length response + ServerFixedLengthStreamSinkConduit fixed = connection.getFixedLengthStreamSinkConduit(); + fixed.reset(contentLength, exchange); + return fixed; + } catch (NumberFormatException e) { + //we just fix it for them + responseHeaders.remove(Headers.CONTENT_LENGTH); + } + return null; + } + + private static StreamSinkConduit handleResponseConduit(HttpServerExchange exchange, boolean headRequest, StreamSinkConduit channel, HeaderMap responseHeaders, ConduitListener finishListener, String transferEncodingHeader) { + + if (transferEncodingHeader == null) { + if (exchange.isHttp11()) { + if (exchange.isPersistent()) { + responseHeaders.put(Headers.TRANSFER_ENCODING, Headers.CHUNKED.toString()); + + if (headRequest) { + return channel; + } + return new ChunkedStreamSinkConduit(channel, exchange.getConnection().getBufferPool(), true, !exchange.isPersistent(), responseHeaders, finishListener, exchange); + } else { + if (headRequest) { + return channel; + } + return new FinishableStreamSinkConduit(channel, finishListener); + } + } else { + exchange.setPersistent(false); + responseHeaders.put(Headers.CONNECTION, Headers.CLOSE.toString()); + if (headRequest) { + return channel; + } + return new FinishableStreamSinkConduit(channel, finishListener); + } + } else { + //moved outside because this is rarely used + //and makes the method small enough to be inlined + return handleExplicitTransferEncoding(exchange, channel, finishListener, responseHeaders, transferEncodingHeader, headRequest); + } + } + + private static StreamSinkConduit handleExplicitTransferEncoding(HttpServerExchange exchange, StreamSinkConduit channel, ConduitListener finishListener, HeaderMap responseHeaders, String transferEncodingHeader, boolean headRequest) { + HttpString transferEncoding = new HttpString(transferEncodingHeader); + if (transferEncoding.equals(Headers.CHUNKED)) { + if (headRequest) { + return channel; + } + Boolean preChunked = exchange.getAttachment(HttpAttachments.PRE_CHUNKED_RESPONSE); + if(preChunked != null && preChunked) { + return new PreChunkedStreamSinkConduit(channel, finishListener, exchange); + } else { + return new ChunkedStreamSinkConduit(channel, exchange.getConnection().getBufferPool(), true, !exchange.isPersistent(), responseHeaders, finishListener, exchange); + } + } else { + + if (headRequest) { + return channel; + } + log.trace("Cancelling persistence because response is identity with no content length"); + // make it not persistent - very unfortunate for the next request handler really... + exchange.setPersistent(false); + responseHeaders.put(Headers.CONNECTION, Headers.CLOSE.toString()); + return new FinishableStreamSinkConduit(channel, terminateResponseListener(exchange)); + } + } + + /** + * fast long parsing algorithm + * + * @param str The string + * @return The long + */ + public static long parsePositiveLong(String str) { + long value = 0; + final int length = str.length(); + + if (length == 0) { + throw new NumberFormatException(str); + } + + long multiplier = 1; + for (int i = length - 1; i >= 0; --i) { + char c = str.charAt(i); + + if (c < '0' || c > '9') { + throw new NumberFormatException(str); + } + long digit = c - '0'; + value += digit * multiplier; + multiplier *= 10; + } + return value; + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/ParseState.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/ParseState.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/ParseState.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,141 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.util.HttpString; + +import java.util.HashMap; + +/** + * The current state of the tokenizer state machine. This class is mutable and not thread safe. + *

+ * As the machine changes state this class is updated rather than allocating a new one each time. + * + * fields are not private to allow for efficient putfield / getfield access + * + * Fields can mean different things depending on the current state. This means that names may + * not always reflect complete functionality. + * + * This class is re-used for requests on the same connection. + * + * @author Stuart Douglas + */ +class ParseState { + + //parsing states + public static final int VERB = 0; + public static final int PATH = 1; + public static final int PATH_PARAMETERS = 2; + public static final int QUERY_PARAMETERS = 3; + public static final int VERSION = 4; + public static final int AFTER_VERSION = 5; + public static final int HEADER = 6; + public static final int HEADER_VALUE = 7; + public static final int PARSE_COMPLETE = 8; + + /** + * The actual state of request parsing + */ + int state; + + /** + * The current state in the tokenizer state machine. + */ + int parseState; + + /** + * If this state is a prefix or terminal match state this is set to the string + * that is a candidate to be matched + */ + HttpString current; + + /** + * The bytes version of {@link #current} + */ + byte[] currentBytes; + + /** + * If this state is a prefix match state then this holds the current position in the string. + * + */ + int pos; + + boolean urlDecodeRequired = false; + + /** + * If this is in {@link io.undertow.annotationprocessor.AbstractParserGenerator#NO_STATE} then this holds the current token that has been read so far. + */ + final StringBuilder stringBuilder = new StringBuilder(); + + /** + * This has different meanings depending on the current state. + * + * In state {@link #HEADER} it is a the first character of the header, that was read by + * {@link #HEADER_VALUE} to see if this was a continuation. + * + * In state {@link #HEADER_VALUE} if represents the last character that was seen. + * + */ + byte leftOver; + + + /** + * This is used to store the next header value when parsing header key / value pairs, + */ + HttpString nextHeader; + + String nextQueryParam; + + int mapCount; + + final StringBuilder decodeBuffer = new StringBuilder(); + + /** + * In general browsers will often send the same header with every request. This cache allows us to re-use the resulting + * strings. + */ + final HashMap headerValuesCache = new HashMap<>(); + + public ParseState() { + this.parseState = 0; + this.pos = 0; + } + + public boolean isComplete() { + return state == PARSE_COMPLETE; + } + + public final void parseComplete(){ + state = PARSE_COMPLETE; + } + + public void reset() { + this.state = 0; + this.parseState = 0; + this.current = null; + this.currentBytes = null; + this.pos = 0; + this.leftOver = 0; + this.urlDecodeRequired = false; + this.stringBuilder.setLength(0); + this.nextHeader = null; + this.nextQueryParam = null; + this.mapCount = 0; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/PipeliningBufferingStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/PipeliningBufferingStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/PipeliningBufferingStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,344 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +import io.undertow.UndertowLogger; +import io.undertow.server.HttpServerExchange; +import org.xnio.Buffers; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.AbstractStreamSinkConduit; +import org.xnio.conduits.ConduitWritableByteChannel; +import org.xnio.conduits.Conduits; +import org.xnio.conduits.StreamSinkConduit; + +import static org.xnio.Bits.allAreClear; +import static org.xnio.Bits.anyAreClear; +import static org.xnio.Bits.anyAreSet; + +/** + * A buffer that is used when processing pipelined requests, that allows the server to + * buffer multiple responses into a single write() call. + *

+ * This can improve performance when pipelining requests. + * + * @author Stuart Douglas + */ +public class PipeliningBufferingStreamSinkConduit extends AbstractStreamSinkConduit { + /** + * If this channel is shutdown + */ + private static final int SHUTDOWN = 1; + private static final int DELEGATE_SHUTDOWN = 1 << 1; + private static final int FLUSHING = 1 << 3; + + private int state; + + private final Pool pool; + private Pooled buffer; + + public PipeliningBufferingStreamSinkConduit(StreamSinkConduit next, final Pool pool) { + super(next); + this.pool = pool; + } + + @Override + public long transferFrom(FileChannel src, long position, long count) throws IOException { + if (anyAreSet(state, SHUTDOWN)) { + throw new ClosedChannelException(); + } + return src.transferTo(position, count, new ConduitWritableByteChannel(this)); + } + + @Override + public long transferFrom(StreamSourceChannel source, long count, ByteBuffer throughBuffer) throws IOException { + return IoUtils.transfer(source, count, throughBuffer, new ConduitWritableByteChannel(this)); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + if (anyAreSet(state, SHUTDOWN)) { + throw new ClosedChannelException(); + } + if (anyAreSet(state, FLUSHING)) { + boolean res = flushBuffer(); + if (!res) { + return 0; + } + } + Pooled pooled = this.buffer; + if (pooled == null) { + this.buffer = pooled = pool.allocate(); + } + final ByteBuffer buffer = pooled.getResource(); + + long total = Buffers.remaining(srcs, offset, length); + + if (buffer.remaining() > total) { + long put = total; + Buffers.copy(buffer, srcs, offset, length); + return put; + } else { + return flushBufferWithUserData(srcs, offset, length); + } + } + + @Override + public int write(ByteBuffer src) throws IOException { + if (anyAreSet(state, SHUTDOWN)) { + throw new ClosedChannelException(); + } + if (anyAreSet(state, FLUSHING)) { + boolean res = flushBuffer(); + if (!res) { + return 0; + } + } + Pooled pooled = this.buffer; + if (pooled == null) { + this.buffer = pooled = pool.allocate(); + } + final ByteBuffer buffer = pooled.getResource(); + if (buffer.remaining() > src.remaining()) { + int put = src.remaining(); + buffer.put(src); + return put; + } else { + return (int) flushBufferWithUserData(new ByteBuffer[]{src}, 0, 1); + } + } + + @Override + public int writeFinal(ByteBuffer src) throws IOException { + return Conduits.writeFinalBasic(this, src); + } + + @Override + public long writeFinal(ByteBuffer[] srcs, int offset, int length) throws IOException { + return Conduits.writeFinalBasic(this, srcs, offset, length); + } + + private long flushBufferWithUserData(final ByteBuffer[] byteBuffers, int offset, int length) throws IOException { + final ByteBuffer byteBuffer = buffer.getResource(); + if (byteBuffer.position() == 0) { + try { + return next.write(byteBuffers, offset, length); + } finally { + buffer.free(); + buffer = null; + } + } + + if (!anyAreSet(state, FLUSHING)) { + state |= FLUSHING; + byteBuffer.flip(); + } + int originalBufferedRemaining = byteBuffer.remaining(); + long toWrite = originalBufferedRemaining; + ByteBuffer[] writeBufs = new ByteBuffer[length + 1]; + writeBufs[0] = byteBuffer; + for (int i = offset; i < offset + length; ++i) { + writeBufs[i + 1 - offset] = byteBuffers[i]; + toWrite += byteBuffers[i].remaining(); + } + + long res = 0; + long written = 0; + do { + res = next.write(writeBufs, 0, writeBufs.length); + written += res; + if (res == 0) { + if (written > originalBufferedRemaining) { + buffer.free(); + this.buffer = null; + state &= ~FLUSHING; + return written - originalBufferedRemaining; + } + return 0; + } + } while (written < toWrite); + buffer.free(); + this.buffer = null; + state &= ~FLUSHING; + return written - originalBufferedRemaining; + } + + /** + * Flushes the cached data. + *

+ * This should be called when a read thread fails to read any more request data, to make sure that any + * buffered data is flushed after the last pipelined request. + *

+ * If this returns false the read thread should suspend reads and resume writes + * + * @return true If the flush succeeded, false otherwise + * @throws IOException + */ + public boolean flushPipelinedData() throws IOException { + if (buffer == null || (buffer.getResource().position() == 0 && allAreClear(state, FLUSHING))) { + return next.flush(); + } + return flushBuffer(); + } + + /** + * Gets the channel wrapper that implements the buffering + * + * @return The channel wrapper + */ + public void setupPipelineBuffer(final HttpServerExchange exchange) { + ((HttpServerConnection) exchange.getConnection()).getChannel().getSinkChannel().setConduit(this); + } + + private boolean flushBuffer() throws IOException { + if (buffer == null) { + return next.flush(); + } + final ByteBuffer byteBuffer = buffer.getResource(); + if (!anyAreSet(state, FLUSHING)) { + state |= FLUSHING; + byteBuffer.flip(); + } + while (byteBuffer.hasRemaining()) { + if (next.write(byteBuffer) == 0) { + return false; + } + } + if (!next.flush()) { + return false; + } + buffer.free(); + this.buffer = null; + state &= ~FLUSHING; + return true; + } + + @Override + public void awaitWritable(long time, TimeUnit timeUnit) throws IOException { + if (buffer != null) { + if (buffer.getResource().hasRemaining()) { + return; + } + } + next.awaitWritable(time, timeUnit); + } + + @Override + public void awaitWritable() throws IOException { + if (buffer != null) { + if (buffer.getResource().hasRemaining()) { + return; + } + next.awaitWritable(); + } + } + + @Override + public boolean flush() throws IOException { + if (anyAreSet(state, SHUTDOWN)) { + if (!flushBuffer()) { + return false; + } + if (anyAreSet(state, SHUTDOWN) && + anyAreClear(state, DELEGATE_SHUTDOWN)) { + state |= DELEGATE_SHUTDOWN; + next.terminateWrites(); + } + return next.flush(); + } + return true; + } + + @Override + public void terminateWrites() throws IOException { + state |= SHUTDOWN; + if (buffer == null) { + state |= DELEGATE_SHUTDOWN; + next.terminateWrites(); + } + } + + public void truncateWrites() throws IOException { + try { + next.truncateWrites(); + } finally { + if (buffer != null) { + buffer.free(); + } + } + } + + public void exchangeComplete(final HttpServerExchange exchange) { + //if we ever fail to read then we flush the pipeline buffer + //this relies on us always doing an eager read when starting a request, + //rather than waiting to be notified of data being available + final HttpServerConnection connection = (HttpServerConnection) exchange.getConnection(); + if (connection.getExtraBytes() == null || exchange.isUpgrade()) { + performFlush(exchange, connection); + } else { + connection.getReadListener().exchangeComplete(exchange); + } + } + + void performFlush(final HttpServerExchange exchange, final HttpServerConnection connection) { + try { + final HttpServerConnection.ConduitState oldState = connection.resetChannel(); + if (!flushPipelinedData()) { + final StreamConnection channel = connection.getChannel(); + channel.getSinkChannel().setWriteListener(new ChannelListener() { + @Override + public void handleEvent(Channel c) { + try { + if (flushPipelinedData()) { + channel.getSinkChannel().setWriteListener(null); + channel.getSinkChannel().suspendWrites(); + connection.restoreChannel(oldState); + connection.getReadListener().exchangeComplete(exchange); + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } + } + }); + connection.getChannel().getSinkChannel().resumeWrites(); + return; + } else { + connection.restoreChannel(oldState); + connection.getReadListener().exchangeComplete(exchange); + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(connection.getChannel()); + } + } + +} + Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http/ServerFixedLengthStreamSinkConduit.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http/ServerFixedLengthStreamSinkConduit.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http/ServerFixedLengthStreamSinkConduit.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http; + +import io.undertow.conduits.AbstractFixedLengthStreamSinkConduit; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import org.xnio.conduits.StreamSinkConduit; + +/** + * @author Stuart Douglas + */ +public class ServerFixedLengthStreamSinkConduit extends AbstractFixedLengthStreamSinkConduit { + + private HttpServerExchange exchange; + + /** + * Construct a new instance. + * + * @param next the next channel + * @param configurable {@code true} if this instance should pass configuration to the next + * @param propagateClose {@code true} if this instance should pass close to the next + */ + public ServerFixedLengthStreamSinkConduit(StreamSinkConduit next, boolean configurable, boolean propagateClose) { + super(next, 1, configurable, propagateClose); + } + + void reset(long contentLength, HttpServerExchange exchange) { + this.exchange = exchange; + super.reset(contentLength, !exchange.isPersistent()); + } + + @Override + protected void channelFinished() { + Connectors.terminateResponse(exchange); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2OpenListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2OpenListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2OpenListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,229 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http2; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import javax.net.ssl.SSLEngine; +import org.eclipse.jetty.alpn.ALPN; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.PushBackStreamSourceConduit; +import org.xnio.ssl.JsseXnioSsl; +import org.xnio.ssl.SslConnection; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.server.HttpHandler; +import io.undertow.server.OpenListener; +import io.undertow.server.protocol.http.HttpOpenListener; +import io.undertow.util.ImmediatePooled; + + +/** + * Open listener for HTTP2 server + * + * @author Stuart Douglas + */ +public final class Http2OpenListener implements ChannelListener, OpenListener { + + private static final String PROTOCOL_KEY = Http2OpenListener.class.getName() + ".protocol"; + + private static final String HTTP2 = "h2-14"; + private static final String HTTP_1_1 = "http/1.1"; + private final Pool bufferPool; + private final int bufferSize; + + private volatile HttpHandler rootHandler; + + private volatile OptionMap undertowOptions; + private final HttpOpenListener delegate; + + public Http2OpenListener(final Pool pool, final int bufferSize) { + this(pool, OptionMap.EMPTY, bufferSize, null); + } + + public Http2OpenListener(final Pool pool, final OptionMap undertowOptions, final int bufferSize) { + this(pool, undertowOptions, bufferSize, null); + } + + public Http2OpenListener(final Pool pool, final int bufferSize, HttpOpenListener httpDelegate) { + this(pool, OptionMap.EMPTY, bufferSize, httpDelegate); + } + + public Http2OpenListener(final Pool pool, final OptionMap undertowOptions, final int bufferSize, HttpOpenListener httpDelegate) { + this.undertowOptions = undertowOptions; + this.bufferPool = pool; + this.bufferSize = bufferSize; + this.delegate = httpDelegate; + } + + public void handleEvent(final StreamConnection channel) { + if (UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) { + UndertowLogger.REQUEST_LOGGER.tracef("Opened connection with %s", channel.getPeerAddress()); + } + final PotentialHttp2Connection potentialConnection = new PotentialHttp2Connection(channel); + channel.getSourceChannel().setReadListener(potentialConnection); + final SSLEngine sslEngine = JsseXnioSsl.getSslEngine((SslConnection) channel); + String existing = (String) sslEngine.getSession().getValue(PROTOCOL_KEY); + //resuming an existing session, no need for ALPN + if (existing != null) { + UndertowLogger.REQUEST_LOGGER.debug("Resuming existing session, not doing NPN negotiation"); + if (existing.equals(HTTP2)) { + Http2Channel sc = new Http2Channel(channel, bufferPool, new ImmediatePooled<>(ByteBuffer.wrap(new byte[0])), false, false, undertowOptions); + sc.getReceiveSetter().set(new Http2ReceiveListener(rootHandler, getUndertowOptions(), bufferSize)); + sc.resumeReceives(); + } else { + if (delegate == null) { + UndertowLogger.REQUEST_IO_LOGGER.couldNotInitiateHttp2Connection(); + IoUtils.safeClose(channel); + return; + } + channel.getSourceChannel().setReadListener(null); + delegate.handleEvent(channel); + } + } else { + ALPN.put(sslEngine, new ALPN.ServerProvider() { + @Override + public void unsupported() { + potentialConnection.selected = HTTP_1_1; + } + + @Override + public String select(List strings) { + ALPN.remove(sslEngine); + for (String s : strings) { + if (s.equals(HTTP2)) { + potentialConnection.selected = s; + sslEngine.getSession().putValue(PROTOCOL_KEY, s); + return s; + } + } + sslEngine.getSession().putValue(PROTOCOL_KEY, HTTP_1_1); + potentialConnection.selected = HTTP_1_1; + return HTTP_1_1; + } + }); + potentialConnection.handleEvent(channel.getSourceChannel()); + } + } + + @Override + public HttpHandler getRootHandler() { + return rootHandler; + } + + @Override + public void setRootHandler(final HttpHandler rootHandler) { + this.rootHandler = rootHandler; + if (delegate != null) { + delegate.setRootHandler(rootHandler); + } + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public void setUndertowOptions(final OptionMap undertowOptions) { + if (undertowOptions == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("undertowOptions"); + } + this.undertowOptions = undertowOptions; + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } + + private class PotentialHttp2Connection implements ChannelListener { + private String selected; + private final StreamConnection channel; + + private PotentialHttp2Connection(StreamConnection channel) { + this.channel = channel; + } + + @Override + public void handleEvent(StreamSourceChannel source) { + Pooled buffer = bufferPool.allocate(); + boolean free = true; + try { + while (true) { + int res = channel.getSourceChannel().read(buffer.getResource()); + if (res == -1) { + IoUtils.safeClose(channel); + return; + } + buffer.getResource().flip(); + if (HTTP2.equals(selected)) { + + //cool, we have a Http2 connection. + Http2Channel channel = new Http2Channel(this.channel, bufferPool, buffer, false, false, undertowOptions); + Integer idleTimeout = undertowOptions.get(UndertowOptions.IDLE_TIMEOUT); + if (idleTimeout != null && idleTimeout > 0) { + channel.setIdleTimeout(idleTimeout); + } + free = false; + channel.getReceiveSetter().set(new Http2ReceiveListener(rootHandler, getUndertowOptions(), bufferSize)); + channel.resumeReceives(); + return; + } else if (HTTP_1_1.equals(selected) || res > 0) { + if (delegate == null) { + UndertowLogger.REQUEST_IO_LOGGER.couldNotInitiateHttp2Connection(); + IoUtils.safeClose(channel); + return; + } + channel.getSourceChannel().setReadListener(null); + if (res > 0) { + PushBackStreamSourceConduit pushBackStreamSourceConduit = new PushBackStreamSourceConduit(channel.getSourceChannel().getConduit()); + channel.getSourceChannel().setConduit(pushBackStreamSourceConduit); + pushBackStreamSourceConduit.pushBack(buffer); + free = false; + } + delegate.handleEvent(channel); + return; + } else if (res == 0) { + channel.getSourceChannel().resumeReads(); + return; + } + } + + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } finally { + if (free) { + buffer.free(); + } + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2ReceiveListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2ReceiveListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2ReceiveListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,236 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http2; + +import java.io.IOException; +import javax.net.ssl.SSLSession; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.protocols.http2.AbstractHttp2StreamSourceChannel; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.protocols.http2.Http2DataStreamSinkChannel; +import io.undertow.protocols.http2.Http2HeadersStreamSinkChannel; +import io.undertow.protocols.http2.Http2StreamSourceChannel; +import io.undertow.server.Connectors; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.HeaderMap; +import io.undertow.util.HeaderValues; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.URLUtils; + +/** + * The recieve listener for a Http2 connection. + *

+ * A new instance is created per connection. + * + * @author Stuart Douglas + */ +public class Http2ReceiveListener implements ChannelListener { + + private static final HttpString METHOD = new HttpString(":method"); + private static final HttpString PATH = new HttpString(":path"); + private static final HttpString SCHEME = new HttpString(":scheme"); + private static final HttpString VERSION = new HttpString(":version"); + private static final HttpString HOST = new HttpString(":host"); + + private final HttpHandler rootHandler; + private final long maxEntitySize; + private final OptionMap undertowOptions; + private final String encoding; + private final boolean decode; + private final StringBuilder decodeBuffer = new StringBuilder(); + private final boolean allowEncodingSlash; + private final int bufferSize; + + + public Http2ReceiveListener(HttpHandler rootHandler, OptionMap undertowOptions, int bufferSize) { + this.rootHandler = rootHandler; + this.undertowOptions = undertowOptions; + this.bufferSize = bufferSize; + this.maxEntitySize = undertowOptions.get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); + this.allowEncodingSlash = undertowOptions.get(UndertowOptions.ALLOW_ENCODED_SLASH, false); + this.decode = undertowOptions.get(UndertowOptions.DECODE_URL, true); + if (undertowOptions.get(UndertowOptions.DECODE_URL, true)) { + this.encoding = undertowOptions.get(UndertowOptions.URL_CHARSET, "UTF-8"); + } else { + this.encoding = null; + } + } + + @Override + public void handleEvent(Http2Channel channel) { + + try { + final AbstractHttp2StreamSourceChannel frame = channel.receive(); + if (frame == null) { + return; + } + if (frame instanceof Http2StreamSourceChannel) { + //we have a request + final Http2StreamSourceChannel dataChannel = (Http2StreamSourceChannel) frame; + final Http2ServerConnection connection = new Http2ServerConnection(channel, dataChannel, undertowOptions, bufferSize); + + + final HttpServerExchange exchange = new HttpServerExchange(connection, dataChannel.getHeaders(), dataChannel.getResponseChannel().getHeaders(), maxEntitySize); + dataChannel.setMaxStreamSize(maxEntitySize); + exchange.setRequestScheme(exchange.getRequestHeaders().getFirst(SCHEME)); + exchange.setProtocol(new HttpString(exchange.getRequestHeaders().getFirst(VERSION))); + exchange.setRequestMethod(new HttpString(exchange.getRequestHeaders().getFirst(METHOD))); + exchange.getRequestHeaders().put(Headers.HOST, exchange.getRequestHeaders().getFirst(HOST)); + final String path = exchange.getRequestHeaders().getFirst(PATH); + setRequestPath(exchange, path, encoding, allowEncodingSlash, decodeBuffer); + + SSLSession session = channel.getSslSession(); + if(session != null) { + connection.setSslSessionInfo(new Http2SslSessionInfo(channel)); + } + dataChannel.getResponseChannel().setCompletionListener(new ChannelListener() { + @Override + public void handleEvent(Http2DataStreamSinkChannel channel) { + Connectors.terminateResponse(exchange); + } + }); + if(!dataChannel.isOpen()) { + Connectors.terminateRequest(exchange); + } else { + dataChannel.setCompletionListener(new ChannelListener() { + @Override + public void handleEvent(Http2StreamSourceChannel channel) { + Connectors.terminateRequest(exchange); + } + }); + } + + Connectors.executeRootHandler(rootHandler, exchange); + } + + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } + } + + /** + * Handles the initial request when the exchange was started by a HTTP ugprade. + * + * + * @param initial The initial upgrade request that started the HTTP2 connection + */ + void handleInitialRequest(HttpServerExchange initial, Http2Channel channel) { + + //we have a request + Http2HeadersStreamSinkChannel sink = channel.createInitialUpgradeResponseStream(); + final Http2ServerConnection connection = new Http2ServerConnection(channel, sink, undertowOptions, bufferSize); + + HeaderMap requestHeaders = new HeaderMap(); + for(HeaderValues hv : initial.getRequestHeaders()) { + requestHeaders.putAll(hv.getHeaderName(), hv); + } + final HttpServerExchange exchange = new HttpServerExchange(connection, requestHeaders, sink.getHeaders(), maxEntitySize); + exchange.setRequestScheme(initial.getRequestScheme()); + exchange.setProtocol(initial.getProtocol()); + exchange.setRequestMethod(initial.getRequestMethod()); + setRequestPath(exchange, initial.getRequestURI(), encoding, allowEncodingSlash, decodeBuffer); + + SSLSession session = channel.getSslSession(); + if(session != null) { + connection.setSslSessionInfo(new Http2SslSessionInfo(channel)); + } + Connectors.terminateRequest(exchange); + sink.setCompletionListener(new ChannelListener() { + @Override + public void handleEvent(Http2DataStreamSinkChannel channel) { + Connectors.terminateResponse(exchange); + } + }); + Connectors.executeRootHandler(rootHandler, exchange); + } + + /** + * Sets the request path and query parameters, decoding to the requested charset. + * + * @param exchange The exchange + * @param encodedPath The encoded path + * @param charset The charset + */ + private void setRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, final boolean allowEncodedSlash, StringBuilder decodeBuffer) { + boolean requiresDecode = false; + for (int i = 0; i < encodedPath.length(); ++i) { + char c = encodedPath.charAt(i); + if (c == '?') { + String part; + String encodedPart = encodedPath.substring(0, i); + if (requiresDecode) { + part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, decodeBuffer); + } else { + part = encodedPart; + } + exchange.setRequestPath(part); + exchange.setRelativePath(part); + exchange.setRequestURI(encodedPart); + final String qs = encodedPath.substring(i + 1); + exchange.setQueryString(qs); + URLUtils.parseQueryString(qs, exchange, encoding, decode); + return; + } else if(c == ';') { + String part; + String encodedPart = encodedPath.substring(0, i); + if (requiresDecode) { + part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, decodeBuffer); + } else { + part = encodedPart; + } + exchange.setRequestPath(part); + exchange.setRelativePath(part); + exchange.setRequestURI(encodedPart); + for(int j = i; j < encodedPath.length(); ++j) { + if (encodedPath.charAt(j) == '?') { + String pathParams = encodedPath.substring(i + 1, j); + URLUtils.parsePathParms(pathParams, exchange, encoding, decode); + String qs = encodedPath.substring(j + 1); + exchange.setQueryString(qs); + URLUtils.parseQueryString(qs, exchange, encoding, decode); + return; + } + } + URLUtils.parsePathParms(encodedPath.substring(i + 1), exchange, encoding, decode); + return; + } else if(c == '%' || c == '+') { + requiresDecode = true; + } + } + + String part; + if (requiresDecode) { + part = URLUtils.decode(encodedPath, charset, allowEncodedSlash, decodeBuffer); + } else { + part = encodedPath; + } + exchange.setRequestPath(part); + exchange.setRelativePath(part); + exchange.setRequestURI(encodedPath); + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2ServerConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2ServerConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2ServerConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,285 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http2; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; +import org.xnio.ChannelListener; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.ConnectedChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSinkChannelWrappingConduit; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceChannelWrappingConduit; +import org.xnio.conduits.StreamSourceConduit; + +import io.undertow.UndertowMessages; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.protocols.http2.Http2DataStreamSinkChannel; +import io.undertow.protocols.http2.Http2StreamSourceChannel; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.ServerConnection; +import io.undertow.util.AttachmentKey; +import io.undertow.util.AttachmentList; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; + +/** + * A server connection. There is one connection per request + * + * + * TODO: how are we going to deal with attachments? + * @author Stuart Douglas + */ +public class Http2ServerConnection extends ServerConnection { + + private static final HttpString STATUS = new HttpString(":status"); + private static final HttpString VERSION = new HttpString(":version"); + + private final Http2Channel channel; + private final Http2StreamSourceChannel requestChannel; + private final Http2DataStreamSinkChannel responseChannel; + private final ConduitStreamSinkChannel conduitStreamSinkChannel; + private final ConduitStreamSourceChannel conduitStreamSourceChannel; + private final StreamSinkConduit originalSinkConduit; + private final StreamSourceConduit originalSourceConduit; + private final OptionMap undertowOptions; + private final int bufferSize; + private SSLSessionInfo sessionInfo; + + public Http2ServerConnection(Http2Channel channel, Http2StreamSourceChannel requestChannel, OptionMap undertowOptions, int bufferSize) { + this.channel = channel; + this.requestChannel = requestChannel; + this.undertowOptions = undertowOptions; + this.bufferSize = bufferSize; + responseChannel = requestChannel.getResponseChannel(); + originalSinkConduit = new StreamSinkChannelWrappingConduit(responseChannel); + originalSourceConduit = new StreamSourceChannelWrappingConduit(requestChannel); + this.conduitStreamSinkChannel = new ConduitStreamSinkChannel(responseChannel, originalSinkConduit); + this.conduitStreamSourceChannel = new ConduitStreamSourceChannel(channel, originalSourceConduit); + } + + /** + * Channel that is used when the request is already half closed + * @param channel + * @param undertowOptions + * @param bufferSize + */ + public Http2ServerConnection(Http2Channel channel, Http2DataStreamSinkChannel sinkChannel, OptionMap undertowOptions, int bufferSize) { + this.channel = channel; + this.requestChannel = null; + this.undertowOptions = undertowOptions; + this.bufferSize = bufferSize; + responseChannel = sinkChannel; + originalSinkConduit = new StreamSinkChannelWrappingConduit(responseChannel); + originalSourceConduit = new StreamSourceChannelWrappingConduit(requestChannel); + this.conduitStreamSinkChannel = new ConduitStreamSinkChannel(responseChannel, originalSinkConduit); + this.conduitStreamSourceChannel = null; + } + + @Override + public Pool getBufferPool() { + return channel.getBufferPool(); + } + + @Override + public XnioWorker getWorker() { + return channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return channel.getIoThread(); + } + + @Override + public HttpServerExchange sendOutOfBandResponse(HttpServerExchange exchange) { + //Http2 does not really seem to support HTTP 100-continue + throw new RuntimeException("Not yet implemented"); + } + + @Override + public void terminateRequestChannel(HttpServerExchange exchange) { + //todo: should we RST_STREAM in this case + //channel.sendRstStream(responseChannel.getStreamId(), Http2Channel.RST_STATUS_CANCEL); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option option) throws IOException { + return null; + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return null; + } + + @Override + public void close() throws IOException { + channel.close(); + } + + @Override + public SocketAddress getPeerAddress() { + return channel.getPeerAddress(); + } + + @Override + public A getPeerAddress(Class type) { + return channel.getPeerAddress(type); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return channel.getCloseSetter(); + } + + @Override + public SocketAddress getLocalAddress() { + return channel.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return channel.getLocalAddress(type); + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public int getBufferSize() { + return bufferSize; + } + + @Override + public SSLSessionInfo getSslSessionInfo() { + return sessionInfo; + } + + @Override + public void setSslSessionInfo(SSLSessionInfo sessionInfo) { + this.sessionInfo = sessionInfo; + } + + @Override + public void addCloseListener(final CloseListener listener) { + channel.addCloseTask(new ChannelListener() { + @Override + public void handleEvent(Http2Channel channel) { + listener.closed(Http2ServerConnection.this); + } + }); + } + + @Override + protected StreamConnection upgradeChannel() { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + protected ConduitStreamSinkChannel getSinkChannel() { + return conduitStreamSinkChannel; + } + + @Override + protected ConduitStreamSourceChannel getSourceChannel() { + return conduitStreamSourceChannel; + } + + @Override + protected StreamSinkConduit getSinkConduit(HttpServerExchange exchange, StreamSinkConduit conduit) { + HeaderMap headers = responseChannel.getHeaders(); + + headers.add(STATUS, exchange.getResponseCode() + " " + StatusCodes.getReason(exchange.getResponseCode())); + headers.add(VERSION, exchange.getProtocol().toString()); + Connectors.flattenCookies(exchange); + return originalSinkConduit; + } + + @Override + protected boolean isUpgradeSupported() { + return false; + } + + @Override + protected void exchangeComplete(HttpServerExchange exchange) { + } + + @Override + protected void setUpgradeListener(HttpUpgradeListener upgradeListener) { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + protected void maxEntitySizeUpdated(HttpServerExchange exchange) { + if(requestChannel != null) { + requestChannel.setMaxStreamSize(exchange.getMaxEntitySize()); + } + } + + @Override + public void addToAttachmentList(AttachmentKey> key, T value) { + channel.addToAttachmentList(key, value); + } + + @Override + public T removeAttachment(AttachmentKey key) { + return channel.removeAttachment(key); + } + + @Override + public T putAttachment(AttachmentKey key, T value) { + return channel.putAttachment(key, value); + } + + @Override + public List getAttachmentList(AttachmentKey> key) { + return channel.getAttachmentList(key); + } + + @Override + public T getAttachment(AttachmentKey key) { + return channel.getAttachment(key); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2SslSessionInfo.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2SslSessionInfo.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2SslSessionInfo.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,92 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http2; + +import java.io.IOException; +import java.security.cert.Certificate; +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.X509Certificate; +import org.xnio.Options; +import org.xnio.SslClientAuthMode; + +import io.undertow.UndertowMessages; +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.RenegotiationRequiredException; +import io.undertow.server.SSLSessionInfo; + +/** + * @author Stuart Douglas + */ +class Http2SslSessionInfo implements SSLSessionInfo { + + private final Http2Channel channel; + + public Http2SslSessionInfo(Http2Channel channel) { + this.channel = channel; + } + + @Override + public byte[] getSessionId() { + return channel.getSslSession().getId(); + } + + @Override + public String getCipherSuite() { + return channel.getSslSession().getCipherSuite(); + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException, RenegotiationRequiredException { + try { + return channel.getSslSession().getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + throw new RenegotiationRequiredException(); + } + } catch (IOException e1) { + //ignore, will not actually happen + } + throw e; + } + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException, RenegotiationRequiredException { + try { + return channel.getSslSession().getPeerCertificateChain(); + } catch (SSLPeerUnverifiedException e) { + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + throw new RenegotiationRequiredException(); + } + } catch (IOException e1) { + //ignore, will not actually happen + } + throw e; + } + } + @Override + public void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClientAuthMode) throws IOException { + throw UndertowMessages.MESSAGES.renegotiationNotSupported(); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2UpgradeHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2UpgradeHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/http2/Http2UpgradeHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,84 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.http2; + +import java.nio.ByteBuffer; + +import org.xnio.OptionMap; +import org.xnio.StreamConnection; + +import io.undertow.protocols.http2.Http2Channel; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.util.FlexBase64; +import io.undertow.util.Headers; + +/** + * Upgrade listener for HTTP2, this allows connections to be established using the upgrade + * mechanism as detailed in Section 3.2. This should always be the first handler in a handler + * chain. + * + * + * @author Stuart Douglas + */ +public class Http2UpgradeHandler implements HttpHandler { + + private final HttpHandler next; + + public Http2UpgradeHandler(HttpHandler next) { + this.next = next; + } + + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + String upgrade = exchange.getRequestHeaders().getFirst(Headers.UPGRADE); + if(upgrade != null && upgrade.equals(Http2Channel.CLEARTEXT_UPGRADE_STRING)) { + String settings = exchange.getRequestHeaders().getFirst("HTTP2-Settings"); + if(settings != null) { + //required by spec + final ByteBuffer settingsFrame = FlexBase64.decode(settings); + exchange.upgradeChannel(new HttpUpgradeListener() { + @Override + public void handleUpgrade(StreamConnection streamConnection, HttpServerExchange exchange) { + OptionMap undertowOptions = exchange.getConnection().getUndertowOptions(); + Http2Channel channel = new Http2Channel(streamConnection, exchange.getConnection().getBufferPool(), null, false, true, settingsFrame, undertowOptions); + Http2ReceiveListener receiveListener = new Http2ReceiveListener(new HttpHandler() { + @Override + public void handleRequest(HttpServerExchange exchange) throws Exception { + //if this header is present we don't actually process the rest of the handler chain + //as the request was only to create the initial request + if(exchange.getRequestHeaders().contains("X-HTTP2-connect-only")) { + exchange.endExchange(); + return; + } + next.handleRequest(exchange); + } + }, undertowOptions, exchange.getConnection().getBufferSize()); + channel.getReceiveSetter().set(receiveListener); + receiveListener.handleInitialRequest(exchange, channel); + channel.resumeReceives(); + } + }); + return; + } + } + next.handleRequest(exchange); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyOpenListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyOpenListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyOpenListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,240 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.spdy; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.List; +import javax.net.ssl.SSLEngine; +import org.eclipse.jetty.alpn.ALPN; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSourceChannel; +import org.xnio.conduits.PushBackStreamSourceConduit; +import org.xnio.ssl.JsseXnioSsl; +import org.xnio.ssl.SslConnection; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.protocols.spdy.SpdyChannel; +import io.undertow.server.HttpHandler; +import io.undertow.server.OpenListener; +import io.undertow.server.protocol.http.HttpOpenListener; +import io.undertow.util.ImmediatePooled; + + +/** + * Open listener for SPDY server + * + * @author David M. Lloyd + */ +public final class SpdyOpenListener implements ChannelListener, OpenListener { + + private static final String PROTOCOL_KEY = SpdyOpenListener.class.getName() + ".protocol"; + + private static final String SPDY_3 = "spdy/3"; + private static final String SPDY_3_1 = "spdy/3.1"; + private static final String HTTP_1_1 = "http/1.1"; + private final Pool bufferPool; + private final Pool heapBufferPool; + private final int bufferSize; + + private volatile HttpHandler rootHandler; + + private volatile OptionMap undertowOptions; + private final HttpOpenListener delegate; + + public SpdyOpenListener(final Pool pool, final Pool heapBufferPool, final int bufferSize) { + this(pool, heapBufferPool, OptionMap.EMPTY, bufferSize, null); + } + + public SpdyOpenListener(final Pool pool, final Pool heapBufferPool, final OptionMap undertowOptions, final int bufferSize) { + this(pool, heapBufferPool, undertowOptions, bufferSize, null); + } + + public SpdyOpenListener(final Pool pool, final Pool heapBufferPool, final int bufferSize, HttpOpenListener httpDelegate) { + this(pool, heapBufferPool, OptionMap.EMPTY, bufferSize, httpDelegate); + } + + public SpdyOpenListener(final Pool pool, final Pool heapBufferPool, final OptionMap undertowOptions, final int bufferSize, HttpOpenListener httpDelegate) { + this.undertowOptions = undertowOptions; + this.bufferPool = pool; + this.bufferSize = bufferSize; + this.delegate = httpDelegate; + this.heapBufferPool = heapBufferPool; + Pooled buff = heapBufferPool.allocate(); + try { + if (!buff.getResource().hasArray()) { + throw UndertowMessages.MESSAGES.mustProvideHeapBuffer(); + } + } finally { + buff.free(); + } + } + + public void handleEvent(final StreamConnection channel) { + if (UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) { + UndertowLogger.REQUEST_LOGGER.tracef("Opened connection with %s", channel.getPeerAddress()); + } + final PotentialSPDYConnection potentialConnection = new PotentialSPDYConnection(channel); + channel.getSourceChannel().setReadListener(potentialConnection); + final SSLEngine sslEngine = JsseXnioSsl.getSslEngine((SslConnection) channel); + String existing = (String) sslEngine.getSession().getValue(PROTOCOL_KEY); + //resuming an existing session, no need for NPN + if (existing != null) { + UndertowLogger.REQUEST_LOGGER.debug("Resuming existing session, not doing NPN negotiation"); + if (existing.equals(SPDY_3_1) || existing.equals(SPDY_3)) { + SpdyChannel sc = new SpdyChannel(channel, bufferPool, new ImmediatePooled<>(ByteBuffer.wrap(new byte[0])), heapBufferPool, false); + sc.getReceiveSetter().set(new SpdyReceiveListener(rootHandler, getUndertowOptions(), bufferSize)); + sc.resumeReceives(); + } else { + if (delegate == null) { + UndertowLogger.REQUEST_IO_LOGGER.couldNotInitiateSpdyConnection(); + IoUtils.safeClose(channel); + return; + } + channel.getSourceChannel().setReadListener(null); + delegate.handleEvent(channel); + } + } else { + ALPN.put(sslEngine, new ALPN.ServerProvider() { + @Override + public void unsupported() { + potentialConnection.selected = HTTP_1_1; + } + + @Override + public String select(List strings) { + ALPN.remove(sslEngine); + for (String s : strings) { + if (s.equals(SPDY_3_1)) { + potentialConnection.selected = s; + sslEngine.getSession().putValue(PROTOCOL_KEY, s); + return s; + } + } + sslEngine.getSession().putValue(PROTOCOL_KEY, HTTP_1_1); + potentialConnection.selected = HTTP_1_1; + return HTTP_1_1; + } + }); + potentialConnection.handleEvent(channel.getSourceChannel()); + } + } + + @Override + public HttpHandler getRootHandler() { + return rootHandler; + } + + @Override + public void setRootHandler(final HttpHandler rootHandler) { + this.rootHandler = rootHandler; + if (delegate != null) { + delegate.setRootHandler(rootHandler); + } + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public void setUndertowOptions(final OptionMap undertowOptions) { + if (undertowOptions == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("undertowOptions"); + } + this.undertowOptions = undertowOptions; + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } + + private class PotentialSPDYConnection implements ChannelListener { + private String selected; + private final StreamConnection channel; + + private PotentialSPDYConnection(StreamConnection channel) { + this.channel = channel; + } + + @Override + public void handleEvent(StreamSourceChannel source) { + Pooled buffer = bufferPool.allocate(); + boolean free = true; + try { + while (true) { + int res = channel.getSourceChannel().read(buffer.getResource()); + if (res == -1) { + IoUtils.safeClose(channel); + return; + } + buffer.getResource().flip(); + if (SPDY_3.equals(selected) || SPDY_3_1.equals(selected)) { + + //cool, we have a spdy connection. + SpdyChannel channel = new SpdyChannel(this.channel, bufferPool, buffer, heapBufferPool, false); + Integer idleTimeout = undertowOptions.get(UndertowOptions.IDLE_TIMEOUT); + if (idleTimeout != null && idleTimeout > 0) { + channel.setIdleTimeout(idleTimeout); + } + free = false; + channel.getReceiveSetter().set(new SpdyReceiveListener(rootHandler, getUndertowOptions(), bufferSize)); + channel.resumeReceives(); + return; + } else if (HTTP_1_1.equals(selected) || res > 0) { + if (delegate == null) { + UndertowLogger.REQUEST_IO_LOGGER.couldNotInitiateSpdyConnection(); + IoUtils.safeClose(channel); + return; + } + channel.getSourceChannel().setReadListener(null); + if (res > 0) { + PushBackStreamSourceConduit pushBackStreamSourceConduit = new PushBackStreamSourceConduit(channel.getSourceChannel().getConduit()); + channel.getSourceChannel().setConduit(pushBackStreamSourceConduit); + pushBackStreamSourceConduit.pushBack(buffer); + free = false; + } + delegate.handleEvent(channel); + return; + } else if (res == 0) { + channel.getSourceChannel().resumeReads(); + return; + } + } + + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } finally { + if (free) { + buffer.free(); + } + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyPlainOpenListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyPlainOpenListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyPlainOpenListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,112 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.spdy; + +import java.nio.ByteBuffer; +import org.xnio.ChannelListener; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.UndertowOptions; +import io.undertow.protocols.spdy.SpdyChannel; +import io.undertow.server.HttpHandler; +import io.undertow.server.OpenListener; + + +/** + * Open listener for SPDY that uses direct connections rather than ALPN. Not used 'in the wild', but + * useful for using SPDY in a proxy situation where the overhead of SSL is not desirable. + * + * @author David M. Lloyd + */ +public final class SpdyPlainOpenListener implements ChannelListener, OpenListener { + + private final Pool bufferPool; + private final Pool heapBufferPool; + private final int bufferSize; + + private volatile HttpHandler rootHandler; + + private volatile OptionMap undertowOptions; + + public SpdyPlainOpenListener(final Pool pool, final Pool heapBufferPool, final int bufferSize) { + this(pool, heapBufferPool, OptionMap.EMPTY, bufferSize); + } + + public SpdyPlainOpenListener(final Pool pool, final Pool heapBufferPool, final OptionMap undertowOptions, final int bufferSize) { + this.undertowOptions = undertowOptions; + this.bufferPool = pool; + this.bufferSize = bufferSize; + this.heapBufferPool = heapBufferPool; + Pooled buff = heapBufferPool.allocate(); + try { + if (!buff.getResource().hasArray()) { + throw UndertowMessages.MESSAGES.mustProvideHeapBuffer(); + } + } finally { + buff.free(); + } + } + + public void handleEvent(final StreamConnection channel) { + if (UndertowLogger.REQUEST_LOGGER.isTraceEnabled()) { + UndertowLogger.REQUEST_LOGGER.tracef("Opened connection with %s", channel.getPeerAddress()); + } + SpdyChannel spdy = new SpdyChannel(channel, bufferPool, null, heapBufferPool, false); + Integer idleTimeout = undertowOptions.get(UndertowOptions.IDLE_TIMEOUT); + if (idleTimeout != null && idleTimeout > 0) { + spdy.setIdleTimeout(idleTimeout); + } + spdy.getReceiveSetter().set(new SpdyReceiveListener(rootHandler, getUndertowOptions(), bufferSize)); + spdy.resumeReceives(); + + } + + @Override + public HttpHandler getRootHandler() { + return rootHandler; + } + + @Override + public void setRootHandler(final HttpHandler rootHandler) { + this.rootHandler = rootHandler; + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public void setUndertowOptions(final OptionMap undertowOptions) { + if (undertowOptions == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("undertowOptions"); + } + this.undertowOptions = undertowOptions; + } + + @Override + public Pool getBufferPool() { + return bufferPool; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyReceiveListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyReceiveListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyReceiveListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,209 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.spdy; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowOptions; +import io.undertow.server.Connectors; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.protocols.spdy.SpdyChannel; +import io.undertow.protocols.spdy.SpdyPingStreamSourceChannel; +import io.undertow.protocols.spdy.SpdyStreamSourceChannel; +import io.undertow.protocols.spdy.SpdySynReplyStreamSinkChannel; +import io.undertow.protocols.spdy.SpdySynStreamStreamSourceChannel; +import io.undertow.util.Headers; +import io.undertow.util.HttpString; +import io.undertow.util.URLUtils; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.OptionMap; + +import javax.net.ssl.SSLSession; +import java.io.IOException; + +/** + * The recieve listener for a SPDY connection. + *

+ * A new instance is created per connection. + * + * @author Stuart Douglas + */ +public class SpdyReceiveListener implements ChannelListener { + + private static final HttpString METHOD = new HttpString(":method"); + private static final HttpString PATH = new HttpString(":path"); + private static final HttpString SCHEME = new HttpString(":scheme"); + private static final HttpString VERSION = new HttpString(":version"); + private static final HttpString HOST = new HttpString(":host"); + + private final HttpHandler rootHandler; + private final long maxEntitySize; + private final OptionMap undertowOptions; + private final String encoding; + private final boolean decode; + private final StringBuilder decodeBuffer = new StringBuilder(); + private final boolean allowEncodingSlash; + private final int bufferSize; + + + public SpdyReceiveListener(HttpHandler rootHandler, OptionMap undertowOptions, int bufferSize) { + this.rootHandler = rootHandler; + this.undertowOptions = undertowOptions; + this.bufferSize = bufferSize; + this.maxEntitySize = undertowOptions.get(UndertowOptions.MAX_ENTITY_SIZE, UndertowOptions.DEFAULT_MAX_ENTITY_SIZE); + this.allowEncodingSlash = undertowOptions.get(UndertowOptions.ALLOW_ENCODED_SLASH, false); + this.decode = undertowOptions.get(UndertowOptions.DECODE_URL, true); + if (undertowOptions.get(UndertowOptions.DECODE_URL, true)) { + this.encoding = undertowOptions.get(UndertowOptions.URL_CHARSET, "UTF-8"); + } else { + this.encoding = null; + } + } + + @Override + public void handleEvent(SpdyChannel channel) { + + try { + final SpdyStreamSourceChannel frame = channel.receive(); + if (frame == null) { + return; + } + if (frame instanceof SpdyPingStreamSourceChannel) { + handlePing((SpdyPingStreamSourceChannel) frame); + } else if (frame instanceof SpdySynStreamStreamSourceChannel) { + //we have a request + final SpdySynStreamStreamSourceChannel dataChannel = (SpdySynStreamStreamSourceChannel) frame; + final SpdyServerConnection connection = new SpdyServerConnection(channel, dataChannel, undertowOptions, bufferSize); + + + final HttpServerExchange exchange = new HttpServerExchange(connection, dataChannel.getHeaders(), dataChannel.getResponseChannel().getHeaders(), maxEntitySize); + dataChannel.setMaxStreamSize(maxEntitySize); + exchange.setRequestScheme(exchange.getRequestHeaders().getFirst(SCHEME)); + exchange.setProtocol(new HttpString(exchange.getRequestHeaders().getFirst(VERSION))); + exchange.setRequestMethod(new HttpString(exchange.getRequestHeaders().getFirst(METHOD))); + exchange.getRequestHeaders().put(Headers.HOST, exchange.getRequestHeaders().getFirst(HOST)); + final String path = exchange.getRequestHeaders().getFirst(PATH); + setRequestPath(exchange, path, encoding, allowEncodingSlash, decodeBuffer); + + SSLSession session = channel.getSslSession(); + if(session != null) { + connection.setSslSessionInfo(new SpdySslSessionInfo(channel)); + } + dataChannel.getResponseChannel().setCompletionListener(new ChannelListener() { + @Override + public void handleEvent(SpdySynReplyStreamSinkChannel channel) { + Connectors.terminateResponse(exchange); + } + }); + if(!dataChannel.isOpen()) { + Connectors.terminateRequest(exchange); + } else { + dataChannel.setCompletionListener(new ChannelListener() { + @Override + public void handleEvent(SpdySynStreamStreamSourceChannel channel) { + Connectors.terminateRequest(exchange); + } + }); + } + + Connectors.executeRootHandler(rootHandler, exchange); + } + + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } + } + + private void handlePing(SpdyPingStreamSourceChannel frame) { + int id = frame.getId(); + if (id % 2 == 1) { + //client side ping, return it + frame.getSpdyChannel().sendPing(id); + } + } + + + /** + * Sets the request path and query parameters, decoding to the requested charset. + * + * @param exchange The exchange + * @param encodedPath The encoded path + * @param charset The charset + */ + private void setRequestPath(final HttpServerExchange exchange, final String encodedPath, final String charset, final boolean allowEncodedSlash, StringBuilder decodeBuffer) { + boolean requiresDecode = false; + for (int i = 0; i < encodedPath.length(); ++i) { + char c = encodedPath.charAt(i); + if (c == '?') { + String part; + String encodedPart = encodedPath.substring(0, i); + if (requiresDecode) { + part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, decodeBuffer); + } else { + part = encodedPart; + } + exchange.setRequestPath(part); + exchange.setRelativePath(part); + exchange.setRequestURI(encodedPart); + final String qs = encodedPath.substring(i + 1); + exchange.setQueryString(qs); + URLUtils.parseQueryString(qs, exchange, encoding, decode); + return; + } else if(c == ';') { + String part; + String encodedPart = encodedPath.substring(0, i); + if (requiresDecode) { + part = URLUtils.decode(encodedPart, charset, allowEncodedSlash, decodeBuffer); + } else { + part = encodedPart; + } + exchange.setRequestPath(part); + exchange.setRelativePath(part); + exchange.setRequestURI(encodedPart); + for(int j = i; j < encodedPath.length(); ++j) { + if (encodedPath.charAt(j) == '?') { + String pathParams = encodedPath.substring(i + 1, j); + URLUtils.parsePathParms(pathParams, exchange, encoding, decode); + String qs = encodedPath.substring(j + 1); + exchange.setQueryString(qs); + URLUtils.parseQueryString(qs, exchange, encoding, decode); + return; + } + } + URLUtils.parsePathParms(encodedPath.substring(i + 1), exchange, encoding, decode); + return; + } else if(c == '%' || c == '+') { + requiresDecode = true; + } + } + + String part; + if (requiresDecode) { + part = URLUtils.decode(encodedPath, charset, allowEncodedSlash, decodeBuffer); + } else { + part = encodedPath; + } + exchange.setRequestPath(part); + exchange.setRelativePath(part); + exchange.setRequestURI(encodedPath); + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyServerConnection.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyServerConnection.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdyServerConnection.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,265 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.spdy; + +import io.undertow.UndertowMessages; +import io.undertow.server.Connectors; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.SSLSessionInfo; +import io.undertow.server.ServerConnection; +import io.undertow.protocols.spdy.SpdyChannel; +import io.undertow.protocols.spdy.SpdySynReplyStreamSinkChannel; +import io.undertow.protocols.spdy.SpdySynStreamStreamSourceChannel; +import io.undertow.util.AttachmentKey; +import io.undertow.util.AttachmentList; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import io.undertow.util.StatusCodes; +import org.xnio.ChannelListener; +import org.xnio.Option; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.ConnectedChannel; +import org.xnio.conduits.ConduitStreamSinkChannel; +import org.xnio.conduits.ConduitStreamSourceChannel; +import org.xnio.conduits.StreamSinkChannelWrappingConduit; +import org.xnio.conduits.StreamSinkConduit; +import org.xnio.conduits.StreamSourceChannelWrappingConduit; +import org.xnio.conduits.StreamSourceConduit; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.List; + +/** + * A server connection. There is one connection per request + * + * + * TODO: how are we going to deal with attachments? + * @author Stuart Douglas + */ +public class SpdyServerConnection extends ServerConnection { + + private static final HttpString STATUS = new HttpString(":status"); + private static final HttpString VERSION = new HttpString(":version"); + + private final SpdyChannel channel; + private final SpdySynStreamStreamSourceChannel requestChannel; + private final SpdySynReplyStreamSinkChannel responseChannel; + private final ConduitStreamSinkChannel conduitStreamSinkChannel; + private final ConduitStreamSourceChannel conduitStreamSourceChannel; + private final StreamSinkConduit originalSinkConduit; + private final StreamSourceConduit originalSourceConduit; + private final OptionMap undertowOptions; + private final int bufferSize; + private SSLSessionInfo sessionInfo; + + public SpdyServerConnection(SpdyChannel channel, SpdySynStreamStreamSourceChannel requestChannel, OptionMap undertowOptions, int bufferSize) { + this.channel = channel; + this.requestChannel = requestChannel; + this.undertowOptions = undertowOptions; + this.bufferSize = bufferSize; + responseChannel = requestChannel.getResponseChannel(); + originalSinkConduit = new StreamSinkChannelWrappingConduit(responseChannel); + originalSourceConduit = new StreamSourceChannelWrappingConduit(requestChannel); + this.conduitStreamSinkChannel = new ConduitStreamSinkChannel(responseChannel, originalSinkConduit); + this.conduitStreamSourceChannel = new ConduitStreamSourceChannel(requestChannel, originalSourceConduit); + } + + @Override + public Pool getBufferPool() { + return channel.getBufferPool(); + } + + @Override + public XnioWorker getWorker() { + return channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return channel.getIoThread(); + } + + @Override + public HttpServerExchange sendOutOfBandResponse(HttpServerExchange exchange) { + //SPDY does not really seem to support HTTP 100-continue + throw new RuntimeException("Not yet implemented"); + } + + @Override + public void terminateRequestChannel(HttpServerExchange exchange) { + //todo: should we RST_STREAM in this case + //channel.sendRstStream(responseChannel.getStreamId(), SpdyChannel.RST_STATUS_CANCEL); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public boolean supportsOption(Option option) { + return false; + } + + @Override + public T getOption(Option option) throws IOException { + return null; + } + + @Override + public T setOption(Option option, T value) throws IllegalArgumentException, IOException { + return null; + } + + @Override + public void close() throws IOException { + channel.close(); + } + + @Override + public SocketAddress getPeerAddress() { + return channel.getPeerAddress(); + } + + @Override + public A getPeerAddress(Class type) { + return channel.getPeerAddress(type); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return channel.getCloseSetter(); + } + + @Override + public SocketAddress getLocalAddress() { + return channel.getLocalAddress(); + } + + @Override + public A getLocalAddress(Class type) { + return channel.getLocalAddress(type); + } + + @Override + public OptionMap getUndertowOptions() { + return undertowOptions; + } + + @Override + public int getBufferSize() { + return bufferSize; + } + + @Override + public SSLSessionInfo getSslSessionInfo() { + return sessionInfo; + } + + @Override + public void setSslSessionInfo(SSLSessionInfo sessionInfo) { + this.sessionInfo = sessionInfo; + } + + @Override + public void addCloseListener(final CloseListener listener) { + requestChannel.getSpdyChannel().addCloseTask(new ChannelListener() { + @Override + public void handleEvent(SpdyChannel channel) { + listener.closed(SpdyServerConnection.this); + } + }); + } + + @Override + protected StreamConnection upgradeChannel() { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + protected ConduitStreamSinkChannel getSinkChannel() { + return conduitStreamSinkChannel; + } + + @Override + protected ConduitStreamSourceChannel getSourceChannel() { + return conduitStreamSourceChannel; + } + + @Override + protected StreamSinkConduit getSinkConduit(HttpServerExchange exchange, StreamSinkConduit conduit) { + HeaderMap headers = responseChannel.getHeaders(); + + headers.add(STATUS, exchange.getResponseCode() + " " + StatusCodes.getReason(exchange.getResponseCode())); + headers.add(VERSION, exchange.getProtocol().toString()); + Connectors.flattenCookies(exchange); + return originalSinkConduit; + } + + @Override + protected boolean isUpgradeSupported() { + return false; + } + + @Override + protected void exchangeComplete(HttpServerExchange exchange) { + } + + @Override + protected void setUpgradeListener(HttpUpgradeListener upgradeListener) { + throw UndertowMessages.MESSAGES.upgradeNotSupported(); + } + + @Override + protected void maxEntitySizeUpdated(HttpServerExchange exchange) { + requestChannel.setMaxStreamSize(exchange.getMaxEntitySize()); + } + + @Override + public void addToAttachmentList(AttachmentKey> key, T value) { + channel.addToAttachmentList(key, value); + } + + @Override + public T removeAttachment(AttachmentKey key) { + return channel.removeAttachment(key); + } + + @Override + public T putAttachment(AttachmentKey key, T value) { + return channel.putAttachment(key, value); + } + + @Override + public List getAttachmentList(AttachmentKey> key) { + return channel.getAttachmentList(key); + } + + @Override + public T getAttachment(AttachmentKey key) { + return channel.getAttachment(key); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdySslSessionInfo.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdySslSessionInfo.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/protocol/spdy/SpdySslSessionInfo.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,92 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.protocol.spdy; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.RenegotiationRequiredException; +import io.undertow.server.SSLSessionInfo; +import io.undertow.protocols.spdy.SpdyChannel; +import org.xnio.Options; +import org.xnio.SslClientAuthMode; + +import javax.net.ssl.SSLPeerUnverifiedException; +import javax.security.cert.X509Certificate; +import java.io.IOException; +import java.security.cert.Certificate; + +/** + * @author Stuart Douglas + */ +class SpdySslSessionInfo implements SSLSessionInfo { + + private final SpdyChannel channel; + + public SpdySslSessionInfo(SpdyChannel channel) { + this.channel = channel; + } + + @Override + public byte[] getSessionId() { + return channel.getSslSession().getId(); + } + + @Override + public String getCipherSuite() { + return channel.getSslSession().getCipherSuite(); + } + + @Override + public Certificate[] getPeerCertificates() throws SSLPeerUnverifiedException, RenegotiationRequiredException { + try { + return channel.getSslSession().getPeerCertificates(); + } catch (SSLPeerUnverifiedException e) { + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + throw new RenegotiationRequiredException(); + } + } catch (IOException e1) { + //ignore, will not actually happen + } + throw e; + } + } + + @Override + public X509Certificate[] getPeerCertificateChain() throws SSLPeerUnverifiedException, RenegotiationRequiredException { + try { + return channel.getSslSession().getPeerCertificateChain(); + } catch (SSLPeerUnverifiedException e) { + try { + SslClientAuthMode sslClientAuthMode = channel.getOption(Options.SSL_CLIENT_AUTH_MODE); + if (sslClientAuthMode == SslClientAuthMode.NOT_REQUESTED) { + throw new RenegotiationRequiredException(); + } + } catch (IOException e1) { + //ignore, will not actually happen + } + throw e; + } + } + @Override + public void renegotiate(HttpServerExchange exchange, SslClientAuthMode sslClientAuthMode) throws IOException { + throw UndertowMessages.MESSAGES.renegotiationNotSupported(); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/session/InMemorySessionManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/InMemorySessionManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/InMemorySessionManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,447 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import io.undertow.UndertowLogger; +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import io.undertow.util.ConcurrentDirectDeque; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import org.xnio.XnioExecutor; +import org.xnio.XnioWorker; + +/** + * The default in memory session manager. This basically just stores sessions in an in memory hash map. + *

+ * + * @author Stuart Douglas + */ +public class InMemorySessionManager implements SessionManager { + + private volatile SessionIdGenerator sessionIdGenerator = new SecureRandomSessionIdGenerator(); + + private final ConcurrentMap sessions; + + private final SessionListeners sessionListeners = new SessionListeners(); + + /** + * 30 minute default + */ + private volatile int defaultSessionTimeout = 30 * 60; + + private final int maxSize; + + private final ConcurrentDirectDeque evictionQueue; + + private final String deploymentName; + + public InMemorySessionManager(String deploymentName, int maxSessions) { + this.deploymentName = deploymentName; + this.sessions = new ConcurrentHashMap<>(); + this.maxSize = maxSessions; + ConcurrentDirectDeque evictionQueue = null; + if (maxSessions > 0) { + evictionQueue = ConcurrentDirectDeque.newInstance(); + } + this.evictionQueue = evictionQueue; + } + + public InMemorySessionManager(String id) { + this(id, -1); + } + + @Override + public String getDeploymentName() { + return this.deploymentName; + } + + @Override + public void start() { + + } + + @Override + public void stop() { + for (Map.Entry session : sessions.entrySet()) { + session.getValue().session.destroy(); + sessionListeners.sessionDestroyed(session.getValue().session, null, SessionListener.SessionDestroyedReason.UNDEPLOY); + } + sessions.clear(); + } + + @Override + public Session createSession(final HttpServerExchange serverExchange, final SessionConfig config) { + if (evictionQueue != null) { + while (sessions.size() >= maxSize && !evictionQueue.isEmpty()) { + String key = evictionQueue.poll(); + UndertowLogger.REQUEST_LOGGER.debugf("Removing session %s as max size has been hit", key); + InMemorySession toRemove = sessions.get(key); + if (toRemove != null) { + toRemove.session.invalidate(null, SessionListener.SessionDestroyedReason.TIMEOUT); //todo: better reason + } + } + } + if (config == null) { + throw UndertowMessages.MESSAGES.couldNotFindSessionCookieConfig(); + } + String sessionID = config.findSessionId(serverExchange); + int count = 0; + while (sessionID == null) { + sessionID = sessionIdGenerator.createSessionId(); + if(sessions.containsKey(sessionID)) { + sessionID = null; + } + if(count++ == 100) { + //this should never happen + //but we guard against pathalogical session id generators to prevent an infinite loop + throw UndertowMessages.MESSAGES.couldNotGenerateUniqueSessionId(); + } + } + Object evictionToken; + if (evictionQueue != null) { + evictionToken = evictionQueue.offerLastAndReturnToken(sessionID); + } else { + evictionToken = null; + } + final SessionImpl session = new SessionImpl(this, sessionID, config, serverExchange.getIoThread(), serverExchange.getConnection().getWorker(), evictionToken); + InMemorySession im = new InMemorySession(session, defaultSessionTimeout); + sessions.put(sessionID, im); + config.setSessionId(serverExchange, session.getId()); + im.lastAccessed = System.currentTimeMillis(); + session.bumpTimeout(); + sessionListeners.sessionCreated(session, serverExchange); + return session; + } + + @Override + public Session getSession(final HttpServerExchange serverExchange, final SessionConfig config) { + String sessionId = config.findSessionId(serverExchange); + return getSession(sessionId); + } + + @Override + public Session getSession(String sessionId) { + if (sessionId == null) { + return null; + } + final InMemorySession sess = sessions.get(sessionId); + if (sess == null) { + return null; + } else { + return sess.session; + } + } + + + @Override + public synchronized void registerSessionListener(final SessionListener listener) { + sessionListeners.addSessionListener(listener); + } + + @Override + public synchronized void removeSessionListener(final SessionListener listener) { + sessionListeners.removeSessionListener(listener); + } + + @Override + public void setDefaultSessionTimeout(final int timeout) { + defaultSessionTimeout = timeout; + } + + @Override + public Set getTransientSessions() { + return getAllSessions(); + } + + @Override + public Set getActiveSessions() { + return getAllSessions(); + } + + @Override + public Set getAllSessions() { + return new HashSet<>(sessions.keySet()); + } + + @Override + public boolean equals(Object object) { + if (!(object instanceof SessionManager)) return false; + SessionManager manager = (SessionManager) object; + return this.deploymentName.equals(manager.getDeploymentName()); + } + + @Override + public int hashCode() { + return this.deploymentName.hashCode(); + } + + @Override + public String toString() { + return this.deploymentName; + } + + /** + * session implementation for the in memory session manager + */ + private static class SessionImpl implements Session { + + private final InMemorySessionManager sessionManager; + + private static volatile AtomicReferenceFieldUpdater evictionTokenUpdater = AtomicReferenceFieldUpdater.newUpdater(SessionImpl.class, Object.class, "evictionToken"); + + private String sessionId; + private volatile Object evictionToken; + private final SessionConfig sessionCookieConfig; + private volatile long expireTime = -1; + + final XnioExecutor executor; + final XnioWorker worker; + + XnioExecutor.Key timerCancelKey; + + Runnable cancelTask = new Runnable() { + @Override + public void run() { + worker.execute(new Runnable() { + @Override + public void run() { + long currentTime = System.currentTimeMillis(); + if(currentTime >= expireTime) { + invalidate(null, SessionListener.SessionDestroyedReason.TIMEOUT); + } else { + timerCancelKey = executor.executeAfter(cancelTask, expireTime - currentTime, TimeUnit.MILLISECONDS); + } + } + }); + } + }; + + private SessionImpl(InMemorySessionManager sessionManager, final String sessionId, final SessionConfig sessionCookieConfig, final XnioExecutor executor, final XnioWorker worker, final Object evictionToken) { + this.sessionManager = sessionManager; + this.sessionId = sessionId; + this.sessionCookieConfig = sessionCookieConfig; + this.executor = executor; + this.worker = worker; + this.evictionToken = evictionToken; + } + + synchronized void bumpTimeout() { + final int maxInactiveInterval = getMaxInactiveInterval(); + if (maxInactiveInterval > 0) { + long newExpireTime = System.currentTimeMillis() + (maxInactiveInterval * 1000); + if(timerCancelKey != null && (newExpireTime < expireTime)) { + // We have to re-schedule as the new maxInactiveInterval is lower than the old one + if (!timerCancelKey.remove()) { + return; + } + timerCancelKey = null; + } + expireTime = newExpireTime; + if(timerCancelKey == null) { + //+1 second, to make sure that the time has actually expired + //we don't re-schedule every time, as it is expensive + //instead when it expires we check if the timeout has been bumped, and if so we re-schedule + timerCancelKey = executor.executeAfter(cancelTask, maxInactiveInterval + 1, TimeUnit.SECONDS); + } + } + if (evictionToken != null) { + Object token = evictionToken; + if (evictionTokenUpdater.compareAndSet(this, token, null)) { + sessionManager.evictionQueue.removeToken(token); + this.evictionToken = sessionManager.evictionQueue.offerLastAndReturnToken(sessionId); + } + } + } + + + @Override + public String getId() { + return sessionId; + } + + @Override + public void requestDone(final HttpServerExchange serverExchange) { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess != null) { + sess.lastAccessed = System.currentTimeMillis(); + } + } + + @Override + public long getCreationTime() { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + return sess.creationTime; + } + + @Override + public long getLastAccessedTime() { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + return sess.lastAccessed; + } + + @Override + public void setMaxInactiveInterval(final int interval) { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + sess.maxInactiveInterval = interval; + bumpTimeout(); + } + + @Override + public int getMaxInactiveInterval() { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + return sess.maxInactiveInterval; + } + + @Override + public Object getAttribute(final String name) { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + bumpTimeout(); + return sess.attributes.get(name); + } + + @Override + public Set getAttributeNames() { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + bumpTimeout(); + return sess.attributes.keySet(); + } + + @Override + public Object setAttribute(final String name, final Object value) { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + final Object existing = sess.attributes.put(name, value); + if (existing == null) { + sessionManager.sessionListeners.attributeAdded(sess.session, name, value); + } else { + sessionManager.sessionListeners.attributeUpdated(sess.session, name, value, existing); + } + bumpTimeout(); + return existing; + } + + @Override + public Object removeAttribute(final String name) { + final InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + throw UndertowMessages.MESSAGES.sessionNotFound(sessionId); + } + final Object existing = sess.attributes.remove(name); + sessionManager.sessionListeners.attributeRemoved(sess.session, name, existing); + bumpTimeout(); + return existing; + } + + @Override + public void invalidate(final HttpServerExchange exchange) { + invalidate(exchange, SessionListener.SessionDestroyedReason.INVALIDATED); + } + + synchronized void invalidate(final HttpServerExchange exchange, SessionListener.SessionDestroyedReason reason) { + if (timerCancelKey != null) { + timerCancelKey.remove(); + } + InMemorySession sess = sessionManager.sessions.get(sessionId); + if (sess == null) { + if (reason == SessionListener.SessionDestroyedReason.INVALIDATED) { + throw UndertowMessages.MESSAGES.sessionAlreadyInvalidated(); + } + return; + } + sessionManager.sessionListeners.sessionDestroyed(sess.session, exchange, reason); + sessionManager.sessions.remove(sessionId); + if (exchange != null) { + sessionCookieConfig.clearSession(exchange, this.getId()); + } + } + + @Override + public SessionManager getSessionManager() { + return sessionManager; + } + + @Override + public String changeSessionId(final HttpServerExchange exchange, final SessionConfig config) { + final String oldId = sessionId; + final InMemorySession sess = sessionManager.sessions.get(oldId); + String newId = sessionManager.sessionIdGenerator.createSessionId(); + this.sessionId = newId; + sessionManager.sessions.put(newId, sess); + sessionManager.sessions.remove(oldId); + config.setSessionId(exchange, this.getId()); + sessionManager.sessionListeners.sessionIdChanged(sess.session, oldId); + return newId; + } + + private synchronized void destroy() { + if (timerCancelKey != null) { + timerCancelKey.remove(); + } + cancelTask = null; + } + + } + + /** + * class that holds the real session data + */ + private static class InMemorySession { + + final SessionImpl session; + + InMemorySession(final SessionImpl session, int maxInactiveInterval) { + this.session = session; + creationTime = lastAccessed = System.currentTimeMillis(); + this.maxInactiveInterval = maxInactiveInterval; + } + + final ConcurrentMap attributes = new ConcurrentHashMap<>(); + volatile long lastAccessed; + final long creationTime; + volatile int maxInactiveInterval; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/session/PathParameterSessionConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/PathParameterSessionConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/PathParameterSessionConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,104 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import java.util.Deque; +import java.util.Locale; + +import io.undertow.server.HttpServerExchange; + +/** + * Session config that is based on a path parameter and URL rewriting + * + * @author Stuart Douglas + */ +public class PathParameterSessionConfig implements SessionConfig { + + private final String name; + + public PathParameterSessionConfig(final String name) { + this.name = name; + } + + public PathParameterSessionConfig() { + this(SessionCookieConfig.DEFAULT_SESSION_ID.toLowerCase(Locale.ENGLISH)); + } + + @Override + public void setSessionId(final HttpServerExchange exchange, final String sessionId) { + exchange.getPathParameters().remove(name); + exchange.addPathParam(name, sessionId); + } + + @Override + public void clearSession(final HttpServerExchange exchange, final String sessionId) { + exchange.getPathParameters().remove(name); + } + + @Override + public String findSessionId(final HttpServerExchange exchange) { + Deque stringDeque = exchange.getPathParameters().get(name); + if (stringDeque == null) { + return null; + } + return stringDeque.getFirst(); + } + + @Override + public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) { + return findSessionId(exchange) != null ? SessionCookieSource.URL : SessionCookieSource.NONE; + } + + /** + * Return the specified URL with the specified session identifier + * suitably encoded. + * + * @param url URL to be encoded with the session id + * @param sessionId Session id to be included in the encoded URL + */ + @Override + public String rewriteUrl(final String url, final String sessionId) { + if ((url == null) || (sessionId == null)) + return (url); + + String path = url; + String query = ""; + String anchor = ""; + int question = url.indexOf('?'); + if (question >= 0) { + path = url.substring(0, question); + query = url.substring(question); + } + int pound = path.indexOf('#'); + if (pound >= 0) { + anchor = path.substring(pound); + path = path.substring(0, pound); + } + StringBuilder sb = new StringBuilder(path); + if (sb.length() > 0) { // jsessionid can't be first. + sb.append(';'); + sb.append(name.toLowerCase(Locale.ENGLISH)); + sb.append('='); + sb.append(sessionId); + } + sb.append(anchor); + sb.append(query); + return (sb.toString()); + } +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SecureRandomSessionIdGenerator.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SecureRandomSessionIdGenerator.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SecureRandomSessionIdGenerator.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,97 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import java.security.SecureRandom; + +/** + * A {@link SessionIdGenerator} that uses a secure random to generate a + * session ID. + * + * On some systems this may perform poorly if not enough entropy is available, + * depending on the algorithm in use. + * + * + * @author Stuart Douglas + */ +public class SecureRandomSessionIdGenerator implements SessionIdGenerator { + + private final SecureRandom random = new SecureRandom(); + + private volatile int length = 18; + + private static final char[] SESSION_ID_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray(); + + @Override + public String createSessionId() { + final byte[] bytes = new byte[length]; + random.nextBytes(bytes); + return new String(encode(bytes)); + } + + + public int getLength() { + return length; + } + + public void setLength(final int length) { + this.length = length; + } + + /** + * Encode the bytes into a String with a slightly modified Base64-algorithm + * This code was written by Kevin Kelley + * and adapted by Thomas Peuss + * + * @param data The bytes you want to encode + * @return the encoded String + */ + private char[] encode(byte[] data) { + char[] out = new char[((data.length + 2) / 3) * 4]; + char[] alphabet = SESSION_ID_ALPHABET; + // + // 3 bytes encode to 4 chars. Output is always an even + // multiple of 4 characters. + // + for (int i = 0, index = 0; i < data.length; i += 3, index += 4) { + boolean quad = false; + boolean trip = false; + + int val = (0xFF & (int) data[i]); + val <<= 8; + if ((i + 1) < data.length) { + val |= (0xFF & (int) data[i + 1]); + trip = true; + } + val <<= 8; + if ((i + 2) < data.length) { + val |= (0xFF & (int) data[i + 2]); + quad = true; + } + out[index + 3] = alphabet[(quad ? (val & 0x3F) : 64)]; + val >>= 6; + out[index + 2] = alphabet[(trip ? (val & 0x3F) : 64)]; + val >>= 6; + out[index + 1] = alphabet[val & 0x3F]; + val >>= 6; + out[index + 0] = alphabet[val & 0x3F]; + } + return out; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/session/Session.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/Session.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/Session.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,188 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import java.util.Set; + +import io.undertow.server.HttpServerExchange; + +/** + * Represents a HTTP session. + *

+ * Many operations provide both a blocking and an asynchronous version. + *

+ * When using the async versions of operations no guarantee is made as to which threads will + * run listeners registered with this session manger. When using the blocking version the listeners are guaranteed + * to run in the calling thread. + * + * @author Stuart Douglas + */ +public interface Session { + + /** + * Returns a string containing the unique identifier assigned + * to this session. The identifier is assigned + * by the servlet container and is implementation dependent. + * + * @return a string specifying the identifier + * assigned to this session + * @throws IllegalStateException if this method is called on an + * invalidated session + */ + String getId(); + + + /** + * Called when a request is done with the session. + * + * @param serverExchange The http server exchange for this request + */ + void requestDone(final HttpServerExchange serverExchange); + + /** + * Returns the time when this session was created, measured + * in milliseconds since midnight January 1, 1970 GMT. + * + * @return a long specifying + * when this session was created, + * expressed in + * milliseconds since 1/1/1970 GMT + * @throws IllegalStateException if this method is called on an + * invalidated session + */ + long getCreationTime(); + + /** + * Returns the last time the client sent a request associated with + * this session, as the number of milliseconds since midnight + * January 1, 1970 GMT, and marked by the time the container received the request. + *

+ *

Actions that your application takes, such as getting or setting + * a value associated with the session, do not affect the access + * time. + * + * @return a long + * representing the last time + * the client sent a request associated + * with this session, expressed in + * milliseconds since 1/1/1970 GMT + * @throws IllegalStateException if this method is called on an + * invalidated session + */ + long getLastAccessedTime(); + + /** + * Specifies the time, in seconds, between client requests before the + * servlet container will invalidate this session. A negative time + * indicates the session should never timeout. + * + * @param interval An integer specifying the number + * of seconds + */ + void setMaxInactiveInterval(int interval); + + /** + * Returns the maximum time interval, in seconds, that + * the servlet container will keep this session open between + * client accesses. After this interval, the servlet container + * will invalidate the session. The maximum time interval can be set + * with the setMaxInactiveInterval method. + * A negative time indicates the session should never timeout. + * + * @return an integer specifying the number of + * seconds this session remains open + * between client requests + * @see #setMaxInactiveInterval + */ + int getMaxInactiveInterval(); + + /** + * Returns the object bound with the specified name in this session, or + * null if no object is bound under the name. + * + * @param name a string specifying the name of the object + * @return the object with the specified name + * @throws IllegalStateException if this method is called on an + * invalidated session + */ + Object getAttribute(String name); + + /** + * Returns an Set of String objects + * containing the names of all the objects bound to this session. + * + * @return an Set of + * String objects specifying the + * names of all the objects bound to + * this session + * @throws IllegalStateException if this method is called on an + * invalidated session + */ + Set getAttributeNames(); + + /** + * Binds an object to this session, using the name specified. + * If an object of the same name is already bound to the session, + * the object is replaced. + *

+ *

+ *

+ *

If the value passed in is null, this has the same effect as calling + * removeAttribute(). + * + * @param name the name to which the object is bound; + * cannot be null + * @param value the object to be bound + * @return An IOFuture containing the previous value + * @throws IllegalStateException if this method is called on an invalidated session + */ + Object setAttribute(final String name, Object value); + + /** + * Removes the object bound with the specified name from + * this session. If the session does not have an object + * bound with the specified name, this method does nothing. + * + * @param name the name of the object to remove from this session + * @throws IllegalStateException if this method is called on an + * invalidated session + */ + Object removeAttribute(final String name); + + /** + * Invalidates this session then unbinds any objects bound + * to it. + * + * @throws IllegalStateException if this method is called on an + * already invalidated session + */ + void invalidate(final HttpServerExchange exchange); + + /** + * @return The session manager that is associated with this session + */ + SessionManager getSessionManager(); + + /** + * Generate a new session id for this session, and return the new id. + * + * @return The new session ID + */ + String changeSessionId(final HttpServerExchange exchange, final SessionConfig config); +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionAttachmentHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionAttachmentHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionAttachmentHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,118 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import io.undertow.Handlers; +import io.undertow.UndertowMessages; +import io.undertow.server.ExchangeCompletionListener; +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.ResponseCodeHandler; + +/** + * Handler that attaches the session to the request. + *

+ * This handler is also the place where session cookie configuration properties are configured. + *

+ * note: this approach is not used by Servlet, which has its own session handlers + * + * @author Stuart Douglas + */ +public class SessionAttachmentHandler implements HttpHandler { + + private volatile HttpHandler next = ResponseCodeHandler.HANDLE_404; + + private volatile SessionManager sessionManager; + + private final SessionConfig sessionConfig; + + public SessionAttachmentHandler(final SessionManager sessionManager, final SessionConfig sessionConfig) { + this.sessionConfig = sessionConfig; + if (sessionManager == null) { + throw UndertowMessages.MESSAGES.sessionManagerMustNotBeNull(); + } + this.sessionManager = sessionManager; + } + + public SessionAttachmentHandler(final HttpHandler next, final SessionManager sessionManager, final SessionConfig sessionConfig) { + this.sessionConfig = sessionConfig; + if (sessionManager == null) { + throw UndertowMessages.MESSAGES.sessionManagerMustNotBeNull(); + } + this.next = next; + this.sessionManager = sessionManager; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + exchange.putAttachment(SessionManager.ATTACHMENT_KEY, sessionManager); + exchange.putAttachment(SessionConfig.ATTACHMENT_KEY, sessionConfig); + final UpdateLastAccessTimeListener handler = new UpdateLastAccessTimeListener(sessionConfig, sessionManager); + exchange.addExchangeCompleteListener(handler); + next.handleRequest(exchange); + + } + + + public HttpHandler getNext() { + return next; + } + + public SessionAttachmentHandler setNext(final HttpHandler next) { + Handlers.handlerNotNull(next); + this.next = next; + return this; + } + + public SessionManager getSessionManager() { + return sessionManager; + } + + public SessionAttachmentHandler setSessionManager(final SessionManager sessionManager) { + if (sessionManager == null) { + throw UndertowMessages.MESSAGES.sessionManagerMustNotBeNull(); + } + this.sessionManager = sessionManager; + return this; + } + + private static class UpdateLastAccessTimeListener implements ExchangeCompletionListener { + + private final SessionConfig sessionConfig; + private final SessionManager sessionManager; + + private UpdateLastAccessTimeListener(final SessionConfig sessionConfig, final SessionManager sessionManager) { + this.sessionConfig = sessionConfig; + this.sessionManager = sessionManager; + } + + @Override + public void exchangeEvent(final HttpServerExchange exchange, final NextListener next) { + try { + final Session session = sessionManager.getSession(exchange, sessionConfig); + if (session != null) { + session.requestDone(exchange); + } + } finally { + next.proceed(); + } + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,82 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +/** + * Interface that abstracts the process of attaching a session to an exchange. This includes both the HTTP side of + * attachment such as setting a cookie, as well as actually attaching the session to the exchange for use by later + * handlers. + * + *

+ * Generally this will just set a cookie. + * + * @author Stuart Douglas + */ +public interface SessionConfig { + + AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(SessionConfig.class); + + /** + * Attaches the session to the exchange. The method should attach the exchange under an attachment key, + * and should also modify the exchange to allow the session to be re-attached on the next request. + *

+ * Generally this will involve setting a cookie + *

+ * Once a session has been attached it must be possible to retrieve it via + * {@link #findSessionId(io.undertow.server.HttpServerExchange)} + * + * + * @param exchange The exchange + * @param sessionId The session + */ + void setSessionId(final HttpServerExchange exchange, final String sessionId); + + /** + * Clears this session from the exchange, removing the attachment and making any changes to the response necessary, + * such as clearing cookies. + * + * @param exchange The exchange + * @param sessionId The session id + */ + void clearSession(final HttpServerExchange exchange, final String sessionId); + + /** + * Retrieves a session id of an existing session from an exchange. + * + * @param exchange The exchange + * @return The session id, or null + */ + String findSessionId(final HttpServerExchange exchange); + + SessionCookieSource sessionCookieSource(final HttpServerExchange exchange); + + String rewriteUrl(final String originalUrl, final String sessionId); + + enum SessionCookieSource { + URL, + COOKIE, + SSL, + OTHER, + NONE + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionCookieConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionCookieConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionCookieConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,169 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import java.util.Map; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.CookieImpl; + +/** + * Encapsulation of session cookie configuration. This removes the need for the session manager to + * know about cookie configuration. + * + * @author Stuart Douglas + */ +public class SessionCookieConfig implements SessionConfig { + + public static final String DEFAULT_SESSION_ID = "JSESSIONID"; + + private String cookieName = DEFAULT_SESSION_ID; + private String path = "/"; + private String domain; + private boolean discard; + private boolean secure; + private boolean httpOnly; + private int maxAge; + private String comment; + + + @Override + public String rewriteUrl(final String originalUrl, final String sessionId) { + return originalUrl; + } + + @Override + public void setSessionId(final HttpServerExchange exchange, final String sessionId) { + Cookie cookie = new CookieImpl(cookieName, sessionId) + .setPath(path) + .setDomain(domain) + .setDiscard(discard) + .setSecure(secure) + .setHttpOnly(httpOnly) + .setComment(comment); + if (maxAge > 0) { + cookie.setMaxAge(maxAge); + } + exchange.setResponseCookie(cookie); + exchange.getRequestCookies().put(cookieName, cookie); + } + + @Override + public void clearSession(final HttpServerExchange exchange, final String sessionId) { + Cookie cookie = new CookieImpl(cookieName, sessionId) + .setPath(path) + .setDomain(domain) + .setDiscard(discard) + .setSecure(secure) + .setHttpOnly(httpOnly) + .setMaxAge(0); + exchange.setResponseCookie(cookie); + exchange.getRequestCookies().remove(cookieName); + } + + @Override + public String findSessionId(final HttpServerExchange exchange) { + Map cookies = exchange.getRequestCookies(); + if (cookies != null) { + Cookie sessionId = cookies.get(cookieName); + if (sessionId != null) { + return sessionId.getValue(); + } + } + return null; + } + + @Override + public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) { + return findSessionId(exchange) != null ? SessionCookieSource.COOKIE : SessionCookieSource.NONE; + } + + public String getCookieName() { + return cookieName; + } + + public SessionCookieConfig setCookieName(final String cookieName) { + this.cookieName = cookieName; + return this; + } + + public String getPath() { + return path; + } + + public SessionCookieConfig setPath(final String path) { + this.path = path; + return this; + } + + public String getDomain() { + return domain; + } + + public SessionCookieConfig setDomain(final String domain) { + this.domain = domain; + return this; + } + + public boolean isDiscard() { + return discard; + } + + public SessionCookieConfig setDiscard(final boolean discard) { + this.discard = discard; + return this; + } + + public boolean isSecure() { + return secure; + } + + public SessionCookieConfig setSecure(final boolean secure) { + this.secure = secure; + return this; + } + + public boolean isHttpOnly() { + return httpOnly; + } + + public SessionCookieConfig setHttpOnly(final boolean httpOnly) { + this.httpOnly = httpOnly; + return this; + } + + public int getMaxAge() { + return maxAge; + } + + public SessionCookieConfig setMaxAge(final int maxAge) { + this.maxAge = maxAge; + return this; + } + + public String getComment() { + return comment; + } + + public SessionCookieConfig setComment(final String comment) { + this.comment = comment; + return this; + } +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionIdGenerator.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionIdGenerator.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionIdGenerator.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,34 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +/** + * + * Strategy for generating session ID's. + * + * The session manager is not required to support pluggable session + * id generation, it is an optional feature. + * + * @author Stuart Douglas + */ +public interface SessionIdGenerator { + + String createSessionId(); + +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,60 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import io.undertow.server.HttpServerExchange; + +/** + * + * A listener for session events. + * + * + * @author Stuart Douglas + */ +public interface SessionListener { + + /** + * Called when a session is created + * @param session The new session + * @param exchange The {@link HttpServerExchange} that created the session + */ + void sessionCreated(final Session session, final HttpServerExchange exchange); + + /** + * Called when a session is destroyed + * @param session The new session + * @param exchange The {@link HttpServerExchange} that destroyed the session, or null if the session timed out + * @param reason The reason why the session was expired + */ + void sessionDestroyed(final Session session, final HttpServerExchange exchange, SessionDestroyedReason reason); + + void attributeAdded(final Session session, final String name, final Object value); + + void attributeUpdated(final Session session, final String name, final Object newValue, final Object oldValue); + + void attributeRemoved(final Session session, final String name,final Object oldValue); + + void sessionIdChanged(final Session session, final String oldSessionId); + + enum SessionDestroyedReason { + INVALIDATED, + TIMEOUT, + UNDEPLOY, + } +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionListeners.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionListeners.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionListeners.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,91 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import java.util.ArrayList; +import java.util.List; +import java.util.ListIterator; +import java.util.concurrent.CopyOnWriteArrayList; + +import io.undertow.server.HttpServerExchange; + +/** + * Utility class that maintains the session listeners. + * + * + * @author Stuart Douglas + */ +public class SessionListeners { + + private final List sessionListeners = new CopyOnWriteArrayList<>(); + + public void addSessionListener(final SessionListener listener) { + this.sessionListeners.add(listener); + } + + public boolean removeSessionListener(final SessionListener listener) { + return this.sessionListeners.remove(listener); + } + + public void clear() { + this.sessionListeners.clear(); + } + + public void sessionCreated(final Session session, final HttpServerExchange exchange) { + for (SessionListener listener : sessionListeners) { + listener.sessionCreated(session, exchange); + } + } + + public void sessionDestroyed(final Session session, final HttpServerExchange exchange, SessionListener.SessionDestroyedReason reason) { + // We need to create our own snapshot to safely iterate over a concurrent list in reverse + List listeners = new ArrayList<>(sessionListeners); + ListIterator iterator = listeners.listIterator(listeners.size()); + while (iterator.hasPrevious()) { + iterator.previous().sessionDestroyed(session, exchange, reason); + } + } + + public void attributeAdded(final Session session, final String name, final Object value) { + for (SessionListener listener : sessionListeners) { + listener.attributeAdded(session, name, value); + } + + } + + public void attributeUpdated(final Session session, final String name, final Object newValue, final Object oldValue) { + for (SessionListener listener : sessionListeners) { + listener.attributeUpdated(session, name, newValue, oldValue); + } + + } + + public void attributeRemoved(final Session session, final String name, final Object oldValue) { + for (SessionListener listener : sessionListeners) { + listener.attributeRemoved(session, name, oldValue); + } + } + + public void sessionIdChanged(final Session session, final String oldSessionId) { + for (SessionListener listener : sessionListeners) { + listener.sessionIdChanged(session, oldSessionId); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SessionManager.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SessionManager.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SessionManager.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,125 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import io.undertow.server.HttpServerExchange; +import io.undertow.util.AttachmentKey; + +import java.util.Set; + +/** + * Interface that manages sessions. + *

+ * The session manager is responsible for maintaining session state. + *

+ * As part of session creation the session manager MUST attempt to retrieve the {@link SessionCookieConfig} from + * the {@link HttpServerExchange} and use it to set the session cookie. The frees up the session manager from + * needing to know details of the cookie configuration. When invalidating a session the session manager MUST + * also use this to clear the session cookie. + * + * @author Stuart Douglas + */ +public interface SessionManager { + + AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(SessionManager.class); + + /** + * Uniquely identifies this session manager + * @return a unique identifier + */ + String getDeploymentName(); + + /** + * Starts the session manager + */ + void start(); + + /** + * stops the session manager + */ + void stop(); + + /** + * Creates a new session. Any {@link SessionListener}s registered with this manager will be notified + * of the session creation. + *

+ * This method *MUST* call {@link SessionConfig#findSessionId(io.undertow.server.HttpServerExchange)} (io.undertow.server.HttpServerExchange)} first to + * determine if an existing session ID is present in the exchange. If this id is present then it must be used + * as the new session ID. If a session with this ID already exists then an {@link IllegalStateException} must be + * thrown. + *

+ *

+ * This requirement exists to allow forwards across servlet contexts to work correctly. + * + * @return The created session + */ + Session createSession(final HttpServerExchange serverExchange, final SessionConfig sessionCookieConfig); + + /** + * @return An IoFuture that can be used to retrieve the session, or an IoFuture that will return null if not found + */ + Session getSession(final HttpServerExchange serverExchange, final SessionConfig sessionCookieConfig); + + /** + * Retrieves a session with the given session id + * + * @param sessionId The session ID + * @return The session, or null if it does not exist + */ + Session getSession(final String sessionId); + + /** + * Registers a session listener for the session manager + * + * @param listener The listener + */ + void registerSessionListener(final SessionListener listener); + + /** + * Removes a session listener from the session manager + * + * @param listener the listener + */ + void removeSessionListener(final SessionListener listener); + + /** + * Sets the default session timeout + * + * @param timeout the timeout + */ + void setDefaultSessionTimeout(final int timeout); + + /** + * Returns the identifiers of those sessions that would be lost upon + * shutdown of this node + */ + Set getTransientSessions(); + + /** + * Returns the identifiers of those sessions that are active on this + * node, excluding passivated sessions + */ + Set getActiveSessions(); + + /** + * Returns the identifiers of all sessions, including both active and + * passive + */ + Set getAllSessions(); +} Index: 3rdParty_sources/undertow/io/undertow/server/session/SslSessionConfig.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/server/session/SslSessionConfig.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/server/session/SslSessionConfig.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,167 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.server.session; + +import io.undertow.server.HttpServerExchange; +import io.undertow.server.SSLSessionInfo; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; + +/** + * Session config that stores the session ID in the current SSL session. + *

+ * It allows for a fallback to be provided for non-ssl connections + * + * @author Stuart Douglas + */ +public class SslSessionConfig implements SessionConfig { + + private final SessionConfig fallbackSessionConfig; + private final Map sessions = new HashMap<>(); + private final Map reverse = new HashMap<>(); + + public SslSessionConfig(final SessionConfig fallbackSessionConfig, SessionManager sessionManager) { + this.fallbackSessionConfig = fallbackSessionConfig; + sessionManager.registerSessionListener(new SessionListener() { + @Override + public void sessionCreated(Session session, HttpServerExchange exchange) { + } + + @Override + public void sessionDestroyed(Session session, HttpServerExchange exchange, SessionDestroyedReason reason) { + synchronized (SslSessionConfig.this) { + Key sid = reverse.remove(session.getId()); + if (sid != null) { + sessions.remove(sid); + } + } + } + + @Override + public void attributeAdded(Session session, String name, Object value) { + } + + @Override + public void attributeUpdated(Session session, String name, Object newValue, Object oldValue) { + } + + @Override + public void attributeRemoved(Session session, String name, Object oldValue) { + } + + @Override + public void sessionIdChanged(Session session, String oldSessionId) { + synchronized (SslSessionConfig.this) { + Key sid = reverse.remove(session.getId()); + if (sid != null) { + sessions.remove(sid); + } + } + } + }); + } + + public SslSessionConfig(SessionManager sessionManager) { + this(null, sessionManager); + } + + @Override + public void setSessionId(final HttpServerExchange exchange, final String sessionId) { + SSLSessionInfo sslSession = exchange.getConnection().getSslSessionInfo(); + if (sslSession == null) { + if (fallbackSessionConfig != null) { + fallbackSessionConfig.setSessionId(exchange, sessionId); + } + } else { + Key key = new Key(sslSession.getSessionId()); + synchronized (this) { + sessions.put(key, sessionId); + reverse.put(sessionId, key); + } + } + } + + @Override + public void clearSession(final HttpServerExchange exchange, final String sessionId) { + SSLSessionInfo sslSession = exchange.getConnection().getSslSessionInfo(); + if (sslSession == null) { + if (fallbackSessionConfig != null) { + fallbackSessionConfig.clearSession(exchange, sessionId); + } + } else { + synchronized (this) { + Key sid = reverse.remove(sessionId); + if (sid != null) { + sessions.remove(sid); + } + } + } + } + + @Override + public String findSessionId(final HttpServerExchange exchange) { + SSLSessionInfo sslSession = exchange.getConnection().getSslSessionInfo(); + if (sslSession == null) { + if (fallbackSessionConfig != null) { + return fallbackSessionConfig.findSessionId(exchange); + } + } else { + synchronized (this) { + return sessions.get(new Key(sslSession.getSessionId())); + } + } + return null; + } + + @Override + public SessionCookieSource sessionCookieSource(HttpServerExchange exchange) { + return findSessionId(exchange) != null ? SessionCookieSource.SSL : SessionCookieSource.NONE; + } + + @Override + public String rewriteUrl(final String originalUrl, final String sessionId) { + return originalUrl; + } + + private static final class Key { + private final byte[] id; + + private Key(byte[] id) { + this.id = id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Key key = (Key) o; + + if (!Arrays.equals(id, key.id)) return false; + + return true; + } + + @Override + public int hashCode() { + return id != null ? Arrays.hashCode(id) : 0; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/AbstractAttachable.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/AbstractAttachable.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/AbstractAttachable.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,113 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; + +import io.undertow.UndertowMessages; + +/** + * A thing which can have named attachments. + * + * @author David M. Lloyd + */ +public abstract class AbstractAttachable implements Attachable { + + private Map, Object> attachments; + + /** + * {@inheritDoc} + */ + @Override + public T getAttachment(final AttachmentKey key) { + if (key == null || attachments == null) { + return null; + } + return key.cast(attachments.get(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public List getAttachmentList(AttachmentKey> key) { + if (key == null || attachments == null) { + return Collections.emptyList(); + } + List list = key.cast(attachments.get(key)); + if (list == null) { + return Collections.emptyList(); + } + return list; + } + + /** + * {@inheritDoc} + */ + @Override + public T putAttachment(final AttachmentKey key, final T value) { + if (key == null) { + throw UndertowMessages.MESSAGES.argumentCannotBeNull("key"); + } + if(attachments == null) { + attachments = createAttachmentMap(); + } + return key.cast(attachments.put(key, key.cast(value))); + } + + protected Map, Object> createAttachmentMap() { + return new IdentityHashMap<>(5); + } + + /** + * {@inheritDoc} + */ + @Override + public T removeAttachment(final AttachmentKey key) { + if (key == null || attachments == null) { + return null; + } + return key.cast(attachments.remove(key)); + } + + /** + * {@inheritDoc} + */ + @Override + public void addToAttachmentList(final AttachmentKey> key, final T value) { + if (key != null) { + if(attachments == null) { + attachments = createAttachmentMap(); + } + final Map, Object> attachments = this.attachments; + final AttachmentList list = key.cast(attachments.get(key)); + if (list == null) { + final AttachmentList newList = new AttachmentList<>(((ListAttachmentKey) key).getValueClass()); + attachments.put(key, newList); + newList.add(value); + } else { + list.add(value); + } + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/Attachable.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Attachable.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Attachable.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.List; + +/** + * A thing which can have named attachments. + * + * @author David M. Lloyd + */ +public interface Attachable { + + /** + * Get an attachment value. If no attachment exists for this key, {@code null} is returned. + * + * @param key the attachment key + * @param the value type + * @return the value, or {@code null} if there is none + */ + T getAttachment(AttachmentKey key); + + /** + * Gets a list attachment value. If not attachment exists for this key an empty list is returned + * + * @param the value type + * @param key the attachment key + * @return the value, or an empty list if there is none + */ + List getAttachmentList(AttachmentKey> key); + + /** + * Set an attachment value. If an attachment for this key was already set, return the original value. If the value being set + * is {@code null}, the attachment key is removed. + * + * @param key the attachment key + * @param value the new value + * @param the value type + * @return the old value, or {@code null} if there was none + */ + T putAttachment(AttachmentKey key, T value); + + /** + * Remove an attachment, returning its previous value. + * + * @param key the attachment key + * @param the value type + * @return the old value, or {@code null} if there was none + */ + T removeAttachment(AttachmentKey key); + /** + * Add a value to a list-typed attachment key. If the key is not mapped, add such a mapping. + * + * @param key the attachment key + * @param value the value to add + * @param the list value type + */ + void addToAttachmentList(AttachmentKey> key, T value); +} Index: 3rdParty_sources/undertow/io/undertow/util/AttachmentKey.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/AttachmentKey.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/AttachmentKey.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,91 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * @author Stuart Douglas + */ + +/** + * An immutable, type-safe object attachment key. Such a key has no value outside of its object identity. + * + * @param the attachment type + */ +public abstract class AttachmentKey { + + AttachmentKey() { + } + + /** + * Cast the value to the type of this attachment key. + * + * @param value the value + * @return the cast value + */ + public abstract T cast(Object value); + + /** + * Construct a new simple attachment key. + * + * @param valueClass the value class + * @param the attachment type + * @return the new instance + */ + public static AttachmentKey create(final Class valueClass) { + return new SimpleAttachmentKey(valueClass); + } + + /** + * Construct a new list attachment key. + * + * @param valueClass the list value class + * @param the list value type + * @return the new instance + */ + @SuppressWarnings("unchecked") + public static AttachmentKey> createList(final Class valueClass) { + return new ListAttachmentKey(valueClass); + } +} + +class ListAttachmentKey extends AttachmentKey> { + + private final Class valueClass; + + ListAttachmentKey(final Class valueClass) { + this.valueClass = valueClass; + } + + @SuppressWarnings({"unchecked"}) + public AttachmentList cast(final Object value) { + if (value == null) { + return null; + } + AttachmentList list = (AttachmentList) value; + final Class listValueClass = list.getValueClass(); + if (listValueClass != valueClass) { + throw new ClassCastException(); + } + return (AttachmentList) list; + } + + Class getValueClass() { + return valueClass; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/AttachmentList.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/AttachmentList.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/AttachmentList.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,156 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.RandomAccess; + +/** + * @author David M. Lloyd + */ +public final class AttachmentList implements List, RandomAccess { + + private final Class valueClass; + private final List delegate; + + public AttachmentList(final int initialCapacity, final Class valueClass) { + delegate = Collections.checkedList(new ArrayList(initialCapacity), valueClass); + this.valueClass = valueClass; + } + + public AttachmentList(final Class valueClass) { + delegate = Collections.checkedList(new ArrayList(), valueClass); + this.valueClass = valueClass; + } + + public AttachmentList(final Collection c, final Class valueClass) { + delegate = Collections.checkedList(new ArrayList(c.size()), valueClass); + delegate.addAll(c); + this.valueClass = valueClass; + } + + public Class getValueClass() { + return valueClass; + } + + public int size() { + return delegate.size(); + } + + public boolean isEmpty() { + return delegate.isEmpty(); + } + + public boolean contains(final Object o) { + return delegate.contains(o); + } + + public Iterator iterator() { + return delegate.iterator(); + } + + public Object[] toArray() { + return delegate.toArray(); + } + + public T[] toArray(final T[] a) { + return delegate.toArray(a); + } + + public boolean add(final T t) { + return delegate.add(t); + } + + public boolean remove(final Object o) { + return delegate.remove(o); + } + + public boolean containsAll(final Collection c) { + return delegate.containsAll(c); + } + + public boolean addAll(final Collection c) { + return delegate.addAll(c); + } + + public boolean addAll(final int index, final Collection c) { + return delegate.addAll(index, c); + } + + public boolean removeAll(final Collection c) { + return delegate.removeAll(c); + } + + public boolean retainAll(final Collection c) { + return delegate.retainAll(c); + } + + public void clear() { + delegate.clear(); + } + + public boolean equals(final Object o) { + return delegate.equals(o); + } + + public int hashCode() { + return delegate.hashCode(); + } + + public T get(final int index) { + return delegate.get(index); + } + + public T set(final int index, final T element) { + return delegate.set(index, element); + } + + public void add(final int index, final T element) { + delegate.add(index, element); + } + + public T remove(final int index) { + return delegate.remove(index); + } + + public int indexOf(final Object o) { + return delegate.indexOf(o); + } + + public int lastIndexOf(final Object o) { + return delegate.lastIndexOf(o); + } + + public ListIterator listIterator() { + return delegate.listIterator(); + } + + public ListIterator listIterator(final int index) { + return delegate.listIterator(index); + } + + public List subList(final int fromIndex, final int toIndex) { + return delegate.subList(fromIndex, toIndex); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/CanonicalPathUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/CanonicalPathUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/CanonicalPathUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,147 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.ArrayList; +import java.util.List; + +/** + * @author Stuart Douglas + */ +public class CanonicalPathUtils { + + + public static String canonicalize(final String path) { + int state = START; + for (int i = path.length() - 1; i >= 0; --i) { + final char c = path.charAt(i); + switch (c) { + case '/': + if (state == FIRST_SLASH) { + return realCanonicalize(path, i + 1, FIRST_SLASH); + } else if (state == ONE_DOT) { + return realCanonicalize(path, i + 2, FIRST_SLASH); + } else if (state == TWO_DOT) { + return realCanonicalize(path, i + 3, FIRST_SLASH); + } + state = FIRST_SLASH; + break; + case '.': + if (state == FIRST_SLASH || state == START) { + state = ONE_DOT; + } else if(state == ONE_DOT) { + state = TWO_DOT; + } else { + state = NORMAL; + } + break; + default: + state = NORMAL; + break; + } + } + return path; + } + + static final int START = -1; + static final int NORMAL = 0; + static final int FIRST_SLASH = 1; + static final int ONE_DOT = 2; + static final int TWO_DOT = 3; + + + private static String realCanonicalize(final String path, final int lastDot, final int initialState) { + int state = initialState; + int eatCount = 0; + int tokenEnd = path.length(); + final List parts = new ArrayList<>(); + for (int i = lastDot - 1; i >= 0; --i) { + final char c = path.charAt(i); + switch (state) { + case NORMAL: { + if (c == '/') { + state = FIRST_SLASH; + if (eatCount > 0) { + --eatCount; + tokenEnd = i; + } + } + break; + } + case FIRST_SLASH: { + if (c == '.') { + state = ONE_DOT; + } else if (c == '/') { + if (eatCount > 0) { + --eatCount; + tokenEnd = i; + } else { + parts.add(path.substring(i + 1, tokenEnd)); + tokenEnd = i; + } + } else { + state = NORMAL; + } + break; + } + case ONE_DOT: { + if (c == '.') { + state = TWO_DOT; + } else if (c == '/') { + if (i + 2 != tokenEnd) { + parts.add(path.substring(i + 2, tokenEnd)); + } + tokenEnd = i; + state = FIRST_SLASH; + } else { + state = NORMAL; + } + break; + } + case TWO_DOT: { + if (c == '/') { + if (i + 3 != tokenEnd) { + parts.add(path.substring(i + 3, tokenEnd)); + } + tokenEnd = i; + eatCount++; + state = FIRST_SLASH; + } else { + state = NORMAL; + } + } + } + } + final StringBuilder result = new StringBuilder(); + if (tokenEnd != 0) { + result.append(path.substring(0, tokenEnd)); + } + for (int i = parts.size() - 1; i >= 0; --i) { + result.append(parts.get(i)); + } + if(result.length() == 0) { + return "/"; + } + return result.toString(); + } + + private CanonicalPathUtils() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/util/Certificates.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Certificates.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Certificates.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import javax.security.cert.CertificateEncodingException; +import javax.security.cert.X509Certificate; + +/** + * Utility class for dealing with certificates + * + * @author Stuart Douglas + */ +public class Certificates { + public static final java.lang.String BEGIN_CERT = "-----BEGIN CERTIFICATE-----"; + + public static final java.lang.String END_CERT = "-----END CERTIFICATE-----"; + + public static String toPem(final X509Certificate certificate) throws CertificateEncodingException { + final StringBuilder builder = new StringBuilder(); + builder.append(BEGIN_CERT); + builder.append('\n'); + builder.append(FlexBase64.encodeString(certificate.getEncoded(), true)); + builder.append('\n'); + builder.append(END_CERT); + return builder.toString(); + } + + private Certificates() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ChaninedHandlerWrapper.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ChaninedHandlerWrapper.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ChaninedHandlerWrapper.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,47 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.List; + +import io.undertow.server.HandlerWrapper; +import io.undertow.server.HttpHandler; + +/** + * Handler wrapper that chains several handler wrappers together. + * + * @author Stuart Douglas + */ +public class ChaninedHandlerWrapper implements HandlerWrapper { + + private final List handlers; + + public ChaninedHandlerWrapper(List handlers) { + this.handlers = handlers; + } + + @Override + public HttpHandler wrap(HttpHandler handler) { + HttpHandler cur = handler; + for(HandlerWrapper h : handlers) { + cur = h.wrap(cur); + } + return cur; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ClosingChannelExceptionHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ClosingChannelExceptionHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ClosingChannelExceptionHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,50 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.Closeable; +import java.io.IOException; +import java.nio.channels.Channel; +import org.xnio.ChannelExceptionHandler; +import org.xnio.IoUtils; + +import io.undertow.UndertowLogger; + +/** + * + * Channel exception handler that closes the channel, logs a debug level + * message and closes arbitrary other resources. + * + * @author Stuart Douglas + */ +public class ClosingChannelExceptionHandler implements ChannelExceptionHandler { + + private final Closeable[] closable; + + public ClosingChannelExceptionHandler(Closeable... closable) { + this.closable = closable; + } + + @Override + public void handleException(T t, IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(t); + IoUtils.safeClose(closable); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ConcurrentDirectDeque.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ConcurrentDirectDeque.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ConcurrentDirectDeque.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.lang.reflect.Constructor; +import java.util.AbstractCollection; +import java.util.Deque; + +/** + * A concurrent deque that allows direct item removal without traversal. + * + * @author Jason T. Greene + */ +public abstract class ConcurrentDirectDeque extends AbstractCollection implements Deque, java.io.Serializable { + private static final Constructor CONSTRUCTOR; + + static { + boolean fast = false; + try { + new FastConcurrentDirectDeque(); + fast = true; + } catch (Throwable t) { + } + + Class klazz = fast ? FastConcurrentDirectDeque.class : PortableConcurrentDirectDeque.class; + try { + CONSTRUCTOR = klazz.getConstructor(); + } catch (NoSuchMethodException e) { + throw new NoSuchMethodError(e.getMessage()); + } + } + + public static ConcurrentDirectDeque newInstance() { + try { + return CONSTRUCTOR.newInstance(); + } catch (Exception e) { + throw new IllegalStateException(e); + } + } + + public abstract Object offerFirstAndReturnToken(E e); + + public abstract Object offerLastAndReturnToken(E e); + + public abstract void removeToken(Object token); +} Index: 3rdParty_sources/undertow/io/undertow/util/ConduitFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ConduitFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ConduitFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,34 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import org.xnio.conduits.Conduit; + +/** + * @author Stuart Douglas + */ +public interface ConduitFactory { + + /** + * Create the channel instance. + * + * @return the channel instance + */ + C create(); +} Index: 3rdParty_sources/undertow/io/undertow/util/Cookies.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Cookies.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Cookies.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,316 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowMessages; +import io.undertow.server.handlers.Cookie; +import io.undertow.server.handlers.CookieImpl; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; + +/** + * Class that contains utility methods for dealing with cookies. + * + * @author Stuart Douglas + * @author Andre Dietisheim + */ +public class Cookies { + + public static final String DOMAIN = "$Domain"; + public static final String VERSION = "$Version"; + public static final String PATH = "$Path"; + + /** + * Parses a "Set-Cookie:" response header value into its cookie representation. The header value is parsed according to the + * syntax that's defined in RFC2109: + * + *

+     * 
+     *  set-cookie      =       "Set-Cookie:" cookies
+     *   cookies         =       1#cookie
+     *   cookie          =       NAME "=" VALUE *(";" cookie-av)
+     *   NAME            =       attr
+     *   VALUE           =       value
+     *   cookie-av       =       "Comment" "=" value
+     *                   |       "Domain" "=" value
+     *                   |       "Max-Age" "=" value
+     *                   |       "Path" "=" value
+     *                   |       "Secure"
+     *                   |       "Version" "=" 1*DIGIT
+     *
+     * 
+     * 
+ * + * @param headerValue The header value + * @return The cookie + * + * @see Cookie + * @see rfc2109 + */ + public static Cookie parseSetCookieHeader(final String headerValue) { + + String key = null; + CookieImpl cookie = null; + int state = 0; + int current = 0; + for (int i = 0; i < headerValue.length(); ++i) { + char c = headerValue.charAt(i); + switch (state) { + case 0: { + //reading key + if (c == '=') { + key = headerValue.substring(current, i); + current = i + 1; + state = 1; + } else if ((c == ';' || c == ' ') && current == i) { + current++; + } else if (c == ';') { + if (cookie == null) { + throw UndertowMessages.MESSAGES.couldNotParseCookie(headerValue); + } else { + handleValue(cookie, headerValue.substring(current, i), null); + } + current = i + 1; + } + break; + } + case 1: { + if (c == ';') { + if (cookie == null) { + cookie = new CookieImpl(key, headerValue.substring(current, i)); + } else { + handleValue(cookie, key, headerValue.substring(current, i)); + } + state = 0; + current = i + 1; + key = null; + } else if (c == '"' && current == i) { + current++; + state = 2; + } + break; + } + case 2: { + if (c == '"') { + if (cookie == null) { + cookie = new CookieImpl(key, headerValue.substring(current, i)); + } else { + handleValue(cookie, key, headerValue.substring(current, i)); + } + state = 0; + current = i + 1; + key = null; + } + break; + } + } + } + if (key == null) { + if (current != headerValue.length()) { + handleValue(cookie, headerValue.substring(current, headerValue.length()), null); + } + } else { + if (current != headerValue.length()) { + handleValue(cookie, key, headerValue.substring(current, headerValue.length())); + } else { + handleValue(cookie, key, null); + } + } + + return cookie; + } + + private static void handleValue(CookieImpl cookie, String key, String value) { + if (key.equalsIgnoreCase("path")) { + cookie.setPath(value); + } else if (key.equalsIgnoreCase("domain")) { + cookie.setDomain(value); + } else if (key.equalsIgnoreCase("max-age")) { + cookie.setMaxAge(Integer.parseInt(value)); + } else if (key.equalsIgnoreCase("expires")) { + cookie.setExpires(DateUtils.parseDate(value)); + } else if (key.equalsIgnoreCase("discard")) { + cookie.setDiscard(true); + } else if (key.equalsIgnoreCase("secure")) { + cookie.setSecure(true); + } else if (key.equalsIgnoreCase("httpOnly")) { + cookie.setHttpOnly(true); + } else if (key.equalsIgnoreCase("version")) { + cookie.setVersion(Integer.parseInt(value)); + } else if (key.equalsIgnoreCase("comment")) { + cookie.setComment(value); + } + //otherwise ignore this key-value pair + } + + /** + /** + * Parses the cookies from a list of "Cookie:" header values. The cookie header values are parsed according to RFC2109 that + * defines the following syntax: + * + *
+     * 
+     * cookie          =  "Cookie:" cookie-version
+     *                    1*((";" | ",") cookie-value)
+     * cookie-value    =  NAME "=" VALUE [";" path] [";" domain]
+     * cookie-version  =  "$Version" "=" value
+     * NAME            =  attr
+     * VALUE           =  value
+     * path            =  "$Path" "=" value
+     * domain          =  "$Domain" "=" value
+     * 
+     * 
+ * + * @param maxCookies The maximum number of cookies. Used to prevent hash collision attacks + * @param allowEqualInValue if true equal characters are allowed in cookie values + * @param cookies The cookie values to parse + * @return A pared cookie map + * + * @see Cookie + * @see rfc2109 + */ + public static Map parseRequestCookies(int maxCookies, boolean allowEqualInValue, List cookies) { + + if (cookies == null) { + return new TreeMap<>(); + } + final Map parsedCookies = new TreeMap<>(); + + for (String cookie : cookies) { + parseCookie(cookie, parsedCookies, maxCookies, allowEqualInValue); + } + return parsedCookies; + } + + private static void parseCookie(final String cookie, final Map parsedCookies, int maxCookies, boolean allowEqualInValue) { + int state = 0; + String name = null; + int start = 0; + int cookieCount = parsedCookies.size(); + final Map cookies = new HashMap<>(); + final Map additional = new HashMap<>(); + for (int i = 0; i < cookie.length(); ++i) { + char c = cookie.charAt(i); + switch (state) { + case 0: { + //eat leading whitespace + if (c == ' ' || c == '\t' || c == ';') { + start = i + 1; + break; + } + state = 1; + //fall through + } + case 1: { + //extract key + if (c == '=') { + name = cookie.substring(start, i); + start = i + 1; + state = 2; + } else if (c == ';') { + cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + state = 0; + start = i + 1; + } + break; + } + case 2: { + //extract value + if (c == ';') { + cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + state = 0; + start = i + 1; + } else if (c == '"') { + state = 3; + start = i + 1; + } else if (!allowEqualInValue && c == '=') { + cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + state = 4; + start = i + 1; + } + break; + } + case 3: { + //extract quoted value + if (c == '"') { + cookieCount = createCookie(name, cookie.substring(start, i), maxCookies, cookieCount, cookies, additional); + state = 0; + start = i + 1; + } + break; + } + case 4: { + //skip value portion behind '=' + if (c == ';') { + state = 0; + } + start = i + 1; + break; + } + } + } + if (state == 2) { + createCookie(name, cookie.substring(start), maxCookies, cookieCount, cookies, additional); + } + + for (final Map.Entry entry : cookies.entrySet()) { + Cookie c = new CookieImpl(entry.getKey(), entry.getValue()); + String domain = additional.get(DOMAIN); + if (domain != null) { + c.setDomain(domain); + } + String version = additional.get(VERSION); + if (version != null) { + c.setVersion(Integer.parseInt(version)); + } + String path = additional.get(PATH); + if (path != null) { + c.setPath(path); + } + parsedCookies.put(c.getName(), c); + } + } + + private static int createCookie(final String name, final String value, int maxCookies, int cookieCount, + final Map cookies, final Map additional) { + if (name.charAt(0) == '$') { + if(additional.containsKey(name)) { + return cookieCount; + } + additional.put(name, value); + return cookieCount; + } else { + if (cookieCount == maxCookies) { + throw UndertowMessages.MESSAGES.tooManyCookies(maxCookies); + } + if(cookies.containsKey(name)) { + return cookieCount; + } + cookies.put(name, value); + return ++cookieCount; + } + } + + private Cookies() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/util/CopyOnWriteMap.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/CopyOnWriteMap.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/CopyOnWriteMap.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,170 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; + +/** + * A basic copy on write map. It simply delegates to an underlying map, that is swapped out + * every time the map is updated. + * + * Note: this is not a secure map. It should not be used in situations where the map is populated + * from user input. + * + * @author Stuart Douglas + */ +public class CopyOnWriteMap implements ConcurrentMap { + + private volatile Map delegate = Collections.emptyMap(); + + public CopyOnWriteMap() { + } + + public CopyOnWriteMap(Map existing) { + this.delegate = new HashMap<>(existing); + } + + @Override + public synchronized V putIfAbsent(K key, V value) { + final Map delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + return existing; + } + putInternal(key, value); + return null; + } + + @Override + public synchronized boolean remove(Object key, Object value) { + final Map delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(value)) { + removeInternal(key); + return true; + } + return false; + } + + @Override + public synchronized boolean replace(K key, V oldValue, V newValue) { + final Map delegate = this.delegate; + V existing = delegate.get(key); + if(existing.equals(oldValue)) { + putInternal(key, newValue); + return true; + } + return false; + } + + @Override + public V replace(K key, V value) { + final Map delegate = this.delegate; + V existing = delegate.get(key); + if(existing != null) { + putInternal(key, value); + return existing; + } + return null; + } + + @Override + public int size() { + return delegate.size(); + } + + @Override + public boolean isEmpty() { + return delegate.isEmpty(); + } + + @Override + public boolean containsKey(Object key) { + return delegate.containsKey(key); + } + + @Override + public boolean containsValue(Object value) { + return delegate.containsValue(value); + } + + @Override + public V get(Object key) { + return delegate.get(key); + } + + @Override + public synchronized V put(K key, V value) { + return putInternal(key, value); + } + + @Override + public synchronized V remove(Object key) { + return removeInternal(key); + } + + @Override + public synchronized void putAll(Map m) { + final Map delegate = new HashMap<>(this.delegate); + for(Entry e : m.entrySet()) { + delegate.put(e.getKey(), e.getValue()); + } + this.delegate = delegate; + } + + @Override + public synchronized void clear() { + delegate = Collections.emptyMap(); + } + + @Override + public Set keySet() { + return delegate.keySet(); + } + + @Override + public Collection values() { + return delegate.values(); + } + + @Override + public Set> entrySet() { + return delegate.entrySet(); + } + + //must be called under lock + private V putInternal(final K key, final V value) { + final Map delegate = new HashMap<>(this.delegate); + V existing = delegate.put(key, value); + this.delegate = delegate; + return existing; + } + + public V removeInternal(final Object key) { + final Map delegate = new HashMap<>(this.delegate); + V existing = delegate.remove(key); + this.delegate = delegate; + return existing; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/DateUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/DateUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/DateUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,275 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowOptions; +import io.undertow.server.HttpServerExchange; + +import java.text.ParsePosition; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.TimeZone; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; + +/** + * Utility for parsing and generating dates + * + * @author Stuart Douglas + */ +public class DateUtils { + + private static final Locale LOCALE_US = Locale.US; + + private static final TimeZone GMT_ZONE = TimeZone.getTimeZone("GMT"); + + private static final String RFC1123_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; + + private static final AtomicReference cachedDateString = new AtomicReference<>(); + + /** + * Thread local cache of this date format. This is technically a small memory leak, however + * in practice it is fine, as it will only be used by server threads. + *

+ * This is the most common date format, which is why we cache it. + */ + private static final ThreadLocal RFC1123_PATTERN_FORMAT = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + SimpleDateFormat df = new SimpleDateFormat(RFC1123_PATTERN, LOCALE_US); + df.setTimeZone(GMT_ZONE); + return df; + } + }; + + /** + * Invalidates the current date + */ + private static final Runnable INVALIDATE_TASK = new Runnable() { + @Override + public void run() { + cachedDateString.set(null); + } + }; + + private static final String RFC1036_PATTERN = "EEEEEEEEE, dd-MMM-yy HH:mm:ss z"; + + private static final String ASCITIME_PATTERN = "EEE MMM d HH:mm:ss yyyyy"; + + private static final String OLD_COOKIE_PATTERN = "EEE, dd-MMM-yyyy HH:mm:ss z"; + + + private static final String COMMON_LOG_PATTERN = "dd/MMM/yyyy:HH:mm:ss Z"; + + + private static final ThreadLocal COMMON_LOG_PATTERN_FORMAT = new ThreadLocal() { + @Override + protected SimpleDateFormat initialValue() { + SimpleDateFormat df = new SimpleDateFormat(COMMON_LOG_PATTERN, LOCALE_US); + return df; + } + }; + + /** + * Converts a date to a format suitable for use in a HTTP request + * + * @param date The date + * @return The RFC-1123 formatted date + */ + public static String toDateString(final Date date) { + return RFC1123_PATTERN_FORMAT.get().format(date); + } + + + public static String toOldCookieDateString(final Date date) { + SimpleDateFormat dateFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); + dateFormat.setTimeZone(GMT_ZONE); + return dateFormat.format(date); + } + + public static String toCommonLogFormat(final Date date) { + return COMMON_LOG_PATTERN_FORMAT.get().format(date); + } + + /** + * Attempts to pass a HTTP date. + * + * @param date The date to parse + * @return The parsed date, or null if parsing failed + */ + public static Date parseDate(final String date) { + + /* + IE9 sends a superflous lenght parameter after date in the + If-Modified-Since header, which needs to be stripped before + parsing. + + */ + + final int semicolonIndex = date.indexOf(';'); + final String trimmedDate = semicolonIndex >= 0 ? date.substring(0, semicolonIndex) : date; + + ParsePosition pp = new ParsePosition(0); + SimpleDateFormat dateFormat = RFC1123_PATTERN_FORMAT.get(); + Date val = dateFormat.parse(trimmedDate, pp); + if (val != null && pp.getIndex() == trimmedDate.length()) { + return val; + } + + pp = new ParsePosition(0); + dateFormat = new SimpleDateFormat(RFC1036_PATTERN, LOCALE_US); + dateFormat.setTimeZone(GMT_ZONE); + val = dateFormat.parse(trimmedDate, pp); + if (val != null && pp.getIndex() == trimmedDate.length()) { + return val; + } + + pp = new ParsePosition(0); + dateFormat = new SimpleDateFormat(ASCITIME_PATTERN, LOCALE_US); + dateFormat.setTimeZone(GMT_ZONE); + val = dateFormat.parse(trimmedDate, pp); + if (val != null && pp.getIndex() == trimmedDate.length()) { + return val; + } + + pp = new ParsePosition(0); + dateFormat = new SimpleDateFormat(OLD_COOKIE_PATTERN, LOCALE_US); + dateFormat.setTimeZone(GMT_ZONE); + val = dateFormat.parse(trimmedDate, pp); + if (val != null && pp.getIndex() == trimmedDate.length()) { + return val; + } + + return null; + } + + /** + * Handles the if-modified-since header. returns true if the request should proceed, false otherwise + * + * @param exchange the exchange + * @param lastModified The last modified date + * @return + */ + public static boolean handleIfModifiedSince(final HttpServerExchange exchange, final Date lastModified) { + if (lastModified == null) { + return true; + } + String modifiedSince = exchange.getRequestHeaders().getFirst(Headers.IF_MODIFIED_SINCE); + if (modifiedSince == null) { + return true; + } + Date modDate = parseDate(modifiedSince); + if (modDate == null) { + return true; + } + return lastModified.after(modDate); + } + + /** + * Handles the if-modified-since header. returns true if the request should proceed, false otherwise + * + * @param modifiedSince the modified since date + * @param lastModified The last modified date + * @return + */ + public static boolean handleIfModifiedSince(final String modifiedSince, final Date lastModified) { + if (lastModified == null) { + return true; + } + if (modifiedSince == null) { + return true; + } + Date modDate = parseDate(modifiedSince); + if (modDate == null) { + return true; + } + return lastModified.after(modDate); + } + + /** + * Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise + * + * @param exchange the exchange + * @param lastModified The last modified date + * @return + */ + public static boolean handleIfUnmodifiedSince(final HttpServerExchange exchange, final Date lastModified) { + if (lastModified == null) { + return true; + } + String modifiedSince = exchange.getRequestHeaders().getFirst(Headers.IF_UNMODIFIED_SINCE); + if (modifiedSince == null) { + return true; + } + Date modDate = parseDate(modifiedSince); + if (modDate == null) { + return true; + } + return lastModified.before(modDate); + } + + /** + * Handles the if-unmodified-since header. returns true if the request should proceed, false otherwise + * + * @param modifiedSince the if unmodified since date + * @param lastModified The last modified date + * @return + */ + public static boolean handleIfUnmodifiedSince(final String modifiedSince, final Date lastModified) { + if (lastModified == null) { + return true; + } + if (modifiedSince == null) { + return true; + } + Date modDate = parseDate(modifiedSince); + if (modDate == null) { + return true; + } + return lastModified.after(modDate); + } + + public static void addDateHeaderIfRequired(HttpServerExchange exchange) { + HeaderMap responseHeaders = exchange.getResponseHeaders(); + if (exchange.getConnection().getUndertowOptions().get(UndertowOptions.ALWAYS_SET_DATE, true) && !responseHeaders.contains(Headers.DATE)) { + String dateString = cachedDateString.get(); + if (dateString != null) { + responseHeaders.put(Headers.DATE, dateString); + } else { + //set the time and register a timer to invalidate it + //note that this is racey, it does not matter if multiple threads do this + //the perf cost of synchronizing would be more than the perf cost of multiple threads running it + long realTime = System.currentTimeMillis(); + long mod = realTime % 1000; + long toGo = 1000 - mod; + dateString = DateUtils.toDateString(new Date(realTime)); + if (cachedDateString.compareAndSet(null, dateString)) { + exchange.getConnection().getIoThread().executeAfter(INVALIDATE_TASK, toGo, TimeUnit.MILLISECONDS); + } + responseHeaders.put(Headers.DATE, dateString); + } + } + } + + private DateUtils() { + + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/ETag.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ETag.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ETag.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,70 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * @author Stuart Douglas + */ +public class ETag { + + private final boolean weak; + private final String tag; + + public ETag(final boolean weak, final String tag) { + this.weak = weak; + this.tag = tag; + } + + public boolean isWeak() { + return weak; + } + + public String getTag() { + return tag; + } + + @Override + public String toString() { + if(weak) { + return "W/\"" + tag + "\""; + } else { + return "\"" + tag + "\""; + } + } + + @Override + public boolean equals(final Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + final ETag eTag = (ETag) o; + + if (weak != eTag.weak) return false; + if (tag != null ? !tag.equals(eTag.tag) : eTag.tag != null) return false; + + return true; + } + + @Override + public int hashCode() { + int result = (weak ? 1 : 0); + result = 31 * result + (tag != null ? tag.hashCode() : 0); + return result; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ETagUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ETagUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ETagUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,305 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import io.undertow.server.HttpServerExchange; + +/** + * @author Stuart Douglas + */ +public class ETagUtils { + + private static final char COMMA = ','; + private static final char QUOTE = '"'; + private static final char W = 'W'; + private static final char SLASH = '/'; + + /** + * Handles the if-match header. returns true if the request should proceed, false otherwise + * + * @param exchange the exchange + * @param etags The etags + * @return + */ + public static boolean handleIfMatch(final HttpServerExchange exchange, final ETag etag, boolean allowWeak) { + return handleIfMatch(exchange, Collections.singletonList(etag), allowWeak); + } + + /** + * Handles the if-match header. returns true if the request should proceed, false otherwise + * + * @param exchange the exchange + * @param etags The etags + * @return + */ + public static boolean handleIfMatch(final HttpServerExchange exchange, final List etags, boolean allowWeak) { + return handleIfMatch(exchange.getRequestHeaders().getFirst(Headers.IF_MATCH), etags, allowWeak); + } + + /** + * Handles the if-match header. returns true if the request should proceed, false otherwise + * + * @param ifMatch The if match header + * @param etags The etags + * @return + */ + public static boolean handleIfMatch(final String ifMatch, final ETag etag, boolean allowWeak) { + return handleIfMatch(ifMatch, Collections.singletonList(etag), allowWeak); + } + + /** + * Handles the if-match header. returns true if the request should proceed, false otherwise + * + * @param ifMatch The ifMatch header + * @param etags The etags + * @return + */ + public static boolean handleIfMatch(final String ifMatch, final List etags, boolean allowWeak) { + if (ifMatch == null) { + return true; + } + List parts = parseETagList(ifMatch); + for (ETag part : parts) { + if (part.getTag().equals("*")) { + return true; //todo: how to tell if there is a current entity for the request + } + if (part.isWeak() && !allowWeak) { + continue; + } + for (ETag tag : etags) { + if (tag != null) { + if (tag.isWeak() && !allowWeak) { + continue; + } + if (tag.getTag().equals(part.getTag())) { + return true; + } + } + } + } + return false; + } + + + /** + * Handles the if-none-match header. returns true if the request should proceed, false otherwise + * + * @param exchange the exchange + * @param etags The etags + * @return + */ + public static boolean handleIfNoneMatch(final HttpServerExchange exchange, final ETag etag, boolean allowWeak) { + return handleIfNoneMatch(exchange, Collections.singletonList(etag), allowWeak); + } + + /** + * Handles the if-none-match header. returns true if the request should proceed, false otherwise + * + * @param exchange the exchange + * @param etags The etags + * @return + */ + public static boolean handleIfNoneMatch(final HttpServerExchange exchange, final List etags, boolean allowWeak) { + return handleIfNoneMatch(exchange.getRequestHeaders().getFirst(Headers.IF_NONE_MATCH), etags, allowWeak); + } + + /** + * Handles the if-none-match header. returns true if the request should proceed, false otherwise + * + * @param ifNoneMatch the header + * @param etags The etags + * @return + */ + public static boolean handleIfNoneMatch(final String ifNoneMatch, final ETag etag, boolean allowWeak) { + return handleIfNoneMatch(ifNoneMatch, Collections.singletonList(etag), allowWeak); + } + + /** + * Handles the if-none-match header. returns true if the request should proceed, false otherwise + * + * @param ifNoneMatch the header + * @param etags The etags + * @return + */ + public static boolean handleIfNoneMatch(final String ifNoneMatch, final List etags, boolean allowWeak) { + if (ifNoneMatch == null) { + return true; + } + List parts = parseETagList(ifNoneMatch); + for (ETag part : parts) { + if (part.getTag().equals("*")) { + return false; + } + if (part.isWeak() && !allowWeak) { + continue; + } + for (ETag tag : etags) { + if (tag != null) { + if (tag.isWeak() && !allowWeak) { + continue; + } + if (tag.getTag().equals(part.getTag())) { + return false; + } + } + } + } + return true; + } + + public static List parseETagList(final String header) { + char[] headerChars = header.toCharArray(); + + // The LinkedHashMap is used so that the parameter order can also be retained. + List response = new ArrayList<>(); + + SearchingFor searchingFor = SearchingFor.START_OF_VALUE; + String currentToken = null; + int valueStart = 0; + boolean weak = false; + boolean malformed = false; + + for (int i = 0; i < headerChars.length; i++) { + switch (searchingFor) { + case START_OF_VALUE: + if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) { + if (headerChars[i] == QUOTE) { + valueStart = i + 1; + searchingFor = SearchingFor.LAST_QUOTE; + weak = false; + malformed = false; + } else if (headerChars[i] == W) { + searchingFor = SearchingFor.WEAK_SLASH; + } + } + break; + case WEAK_SLASH: + if (headerChars[i] == QUOTE) { + valueStart = i + 1; + searchingFor = SearchingFor.LAST_QUOTE; + weak = true; + malformed = false; + } else if (headerChars[i] != SLASH) { + malformed = true; + searchingFor = SearchingFor.END_OF_VALUE; + } + break; + case LAST_QUOTE: + if (headerChars[i] == QUOTE) { + String value = String.valueOf(headerChars, valueStart, i - valueStart); + response.add(new ETag(weak, value.trim())); + searchingFor = SearchingFor.START_OF_VALUE; + } + break; + case END_OF_VALUE: + if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) { + if (!malformed) { + String value = String.valueOf(headerChars, valueStart, i - valueStart); + response.add(new ETag(weak, value.trim())); + searchingFor = SearchingFor.START_OF_VALUE; + } + } + break; + } + } + + if (searchingFor == SearchingFor.END_OF_VALUE || searchingFor == SearchingFor.LAST_QUOTE) { + if (!malformed) { + // Special case where we reached the end of the array containing the header values. + String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart); + response.add(new ETag(weak, value.trim())); + } + } + + return response; + } + + /** + * @param exchange The exchange + * @return The ETag for the exchange, or null if the etag is not set + */ + public static ETag getETag(final HttpServerExchange exchange) { + final String tag = exchange.getResponseHeaders().getFirst(Headers.ETAG); + if (tag == null) { + return null; + } + char[] headerChars = tag.toCharArray(); + SearchingFor searchingFor = SearchingFor.START_OF_VALUE; + int valueStart = 0; + boolean weak = false; + boolean malformed = false; + for (int i = 0; i < headerChars.length; i++) { + switch (searchingFor) { + case START_OF_VALUE: + if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) { + if (headerChars[i] == QUOTE) { + valueStart = i + 1; + searchingFor = SearchingFor.LAST_QUOTE; + weak = false; + malformed = false; + } else if (headerChars[i] == W) { + searchingFor = SearchingFor.WEAK_SLASH; + } + } + break; + case WEAK_SLASH: + if (headerChars[i] == QUOTE) { + valueStart = i + 1; + searchingFor = SearchingFor.LAST_QUOTE; + weak = true; + malformed = false; + } else if (headerChars[i] != SLASH) { + return null; //malformed + } + break; + case LAST_QUOTE: + if (headerChars[i] == QUOTE) { + String value = String.valueOf(headerChars, valueStart, i - valueStart); + return new ETag(weak, value.trim()); + } + break; + case END_OF_VALUE: + if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) { + if (!malformed) { + String value = String.valueOf(headerChars, valueStart, i - valueStart); + return new ETag(weak, value.trim()); + } + } + break; + } + } + if (searchingFor == SearchingFor.END_OF_VALUE || searchingFor == SearchingFor.LAST_QUOTE) { + if (!malformed) { + // Special case where we reached the end of the array containing the header values. + String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart); + return new ETag(weak, value.trim()); + } + } + + return null; + } + + enum SearchingFor { + START_OF_VALUE, LAST_QUOTE, END_OF_VALUE, WEAK_SLASH; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/FastConcurrentDirectDeque.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/FastConcurrentDirectDeque.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/FastConcurrentDirectDeque.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,1498 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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. + */ + +/* + * Written by Doug Lea and Martin Buchholz with assistance from members of + * JCP JSR-166 Expert Group and released to the public domain, as explained + * at http://creativecommons.org/publicdomain/zero/1.0/ + */ + +package io.undertow.util; + +import java.io.Serializable; +import java.lang.reflect.Field; +import java.security.PrivilegedAction; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; +import java.util.NoSuchElementException; + +import sun.misc.Unsafe; + +/** + * A modified version of ConcurrentLinkedDequeue which includes direct + * removal. Like the original, it relies on Unsafe for better performance. + * + * More specifically, an unbounded concurrent {@linkplain Deque deque} based on linked nodes. + * Concurrent insertion, removal, and access operations execute safely + * across multiple threads. + * A {@code ConcurrentLinkedDeque} is an appropriate choice when + * many threads will share access to a common collection. + * Like most other concurrent collection implementations, this class + * does not permit the use of {@code null} elements. + * + *

Iterators are weakly consistent, returning elements + * reflecting the state of the deque at some point at or since the + * creation of the iterator. They do not throw {@link + * java.util.ConcurrentModificationException + * ConcurrentModificationException}, and may proceed concurrently with + * other operations. + * + *

Beware that, unlike in most collections, the {@code size} method + * is NOT a constant-time operation. Because of the + * asynchronous nature of these deques, determining the current number + * of elements requires a traversal of the elements, and so may report + * inaccurate results if this collection is modified during traversal. + * Additionally, the bulk operations {@code addAll}, + * {@code removeAll}, {@code retainAll}, {@code containsAll}, + * {@code equals}, and {@code toArray} are not guaranteed + * to be performed atomically. For example, an iterator operating + * concurrently with an {@code addAll} operation might view only some + * of the added elements. + * + *

This class and its iterator implement all of the optional + * methods of the {@link Deque} and {@link Iterator} interfaces. + * + *

Memory consistency effects: As with other concurrent collections, + * actions in a thread prior to placing an object into a + * {@code ConcurrentLinkedDeque} + * happen-before + * actions subsequent to the access or removal of that element from + * the {@code ConcurrentLinkedDeque} in another thread. + * + *

This class is a member of the + * + * Java Collections Framework. + * + * @since 1.7 + * @author Doug Lea + * @author Martin Buchholz + * @author Jason T. Grene + * @param the type of elements held in this collection + */ + +public class FastConcurrentDirectDeque + extends ConcurrentDirectDeque implements Deque, Serializable { + + /* + * This is an implementation of a concurrent lock-free deque + * supporting interior removes but not interior insertions, as + * required to support the entire Deque interface. + * + * We extend the techniques developed for ConcurrentLinkedQueue and + * LinkedTransferQueue (see the internal docs for those classes). + * Understanding the ConcurrentLinkedQueue implementation is a + * prerequisite for understanding the implementation of this class. + * + * The data structure is a symmetrical doubly-linked "GC-robust" + * linked list of nodes. We minimize the number of volatile writes + * using two techniques: advancing multiple hops with a single CAS + * and mixing volatile and non-volatile writes of the same memory + * locations. + * + * A node contains the expected E ("item") and links to predecessor + * ("prev") and successor ("next") nodes: + * + * class Node { volatile Node prev, next; volatile E item; } + * + * A node p is considered "live" if it contains a non-null item + * (p.item != null). When an item is CASed to null, the item is + * atomically logically deleted from the collection. + * + * At any time, there is precisely one "first" node with a null + * prev reference that terminates any chain of prev references + * starting at a live node. Similarly there is precisely one + * "last" node terminating any chain of next references starting at + * a live node. The "first" and "last" nodes may or may not be live. + * The "first" and "last" nodes are always mutually reachable. + * + * A new element is added atomically by CASing the null prev or + * next reference in the first or last node to a fresh node + * containing the element. The element's node atomically becomes + * "live" at that point. + * + * A node is considered "active" if it is a live node, or the + * first or last node. Active nodes cannot be unlinked. + * + * A "self-link" is a next or prev reference that is the same node: + * p.prev == p or p.next == p + * Self-links are used in the node unlinking process. Active nodes + * never have self-links. + * + * A node p is active if and only if: + * + * p.item != null || + * (p.prev == null && p.next != p) || + * (p.next == null && p.prev != p) + * + * The deque object has two node references, "head" and "tail". + * The head and tail are only approximations to the first and last + * nodes of the deque. The first node can always be found by + * following prev pointers from head; likewise for tail. However, + * it is permissible for head and tail to be referring to deleted + * nodes that have been unlinked and so may not be reachable from + * any live node. + * + * There are 3 stages of node deletion; + * "logical deletion", "unlinking", and "gc-unlinking". + * + * 1. "logical deletion" by CASing item to null atomically removes + * the element from the collection, and makes the containing node + * eligible for unlinking. + * + * 2. "unlinking" makes a deleted node unreachable from active + * nodes, and thus eventually reclaimable by GC. Unlinked nodes + * may remain reachable indefinitely from an iterator. + * + * Physical node unlinking is merely an optimization (albeit a + * critical one), and so can be performed at our convenience. At + * any time, the set of live nodes maintained by prev and next + * links are identical, that is, the live nodes found via next + * links from the first node is equal to the elements found via + * prev links from the last node. However, this is not true for + * nodes that have already been logically deleted - such nodes may + * be reachable in one direction only. + * + * 3. "gc-unlinking" takes unlinking further by making active + * nodes unreachable from deleted nodes, making it easier for the + * GC to reclaim future deleted nodes. This step makes the data + * structure "gc-robust", as first described in detail by Boehm + * (http://portal.acm.org/citation.cfm?doid=503272.503282). + * + * GC-unlinked nodes may remain reachable indefinitely from an + * iterator, but unlike unlinked nodes, are never reachable from + * head or tail. + * + * Making the data structure GC-robust will eliminate the risk of + * unbounded memory retention with conservative GCs and is likely + * to improve performance with generational GCs. + * + * When a node is dequeued at either end, e.g. via poll(), we would + * like to break any references from the node to active nodes. We + * develop further the use of self-links that was very effective in + * other concurrent collection classes. The idea is to replace + * prev and next pointers with special values that are interpreted + * to mean off-the-list-at-one-end. These are approximations, but + * good enough to preserve the properties we want in our + * traversals, e.g. we guarantee that a traversal will never visit + * the same element twice, but we don't guarantee whether a + * traversal that runs out of elements will be able to see more + * elements later after enqueues at that end. Doing gc-unlinking + * safely is particularly tricky, since any node can be in use + * indefinitely (for example by an iterator). We must ensure that + * the nodes pointed at by head/tail never get gc-unlinked, since + * head/tail are needed to get "back on track" by other nodes that + * are gc-unlinked. gc-unlinking accounts for much of the + * implementation complexity. + * + * Since neither unlinking nor gc-unlinking are necessary for + * correctness, there are many implementation choices regarding + * frequency (eagerness) of these operations. Since volatile + * reads are likely to be much cheaper than CASes, saving CASes by + * unlinking multiple adjacent nodes at a time may be a win. + * gc-unlinking can be performed rarely and still be effective, + * since it is most important that long chains of deleted nodes + * are occasionally broken. + * + * The actual representation we use is that p.next == p means to + * goto the first node (which in turn is reached by following prev + * pointers from head), and p.next == null && p.prev == p means + * that the iteration is at an end and that p is a (static final) + * dummy node, NEXT_TERMINATOR, and not the last active node. + * Finishing the iteration when encountering such a TERMINATOR is + * good enough for read-only traversals, so such traversals can use + * p.next == null as the termination condition. When we need to + * find the last (active) node, for enqueueing a new node, we need + * to check whether we have reached a TERMINATOR node; if so, + * restart traversal from tail. + * + * The implementation is completely directionally symmetrical, + * except that most public methods that iterate through the list + * follow next pointers ("forward" direction). + * + * We believe (without full proof) that all single-element deque + * operations (e.g., addFirst, peekLast, pollLast) are linearizable + * (see Herlihy and Shavit's book). However, some combinations of + * operations are known not to be linearizable. In particular, + * when an addFirst(A) is racing with pollFirst() removing B, it is + * possible for an observer iterating over the elements to observe + * A B C and subsequently observe A C, even though no interior + * removes are ever performed. Nevertheless, iterators behave + * reasonably, providing the "weakly consistent" guarantees. + * + * Empirically, microbenchmarks suggest that this class adds about + * 40% overhead relative to ConcurrentLinkedQueue, which feels as + * good as we can hope for. + */ + + private static final long serialVersionUID = 876323262645176354L; + + /** + * A node from which the first node on list (that is, the unique node p + * with p.prev == null && p.next != p) can be reached in O(1) time. + * Invariants: + * - the first node is always O(1) reachable from head via prev links + * - all live nodes are reachable from the first node via succ() + * - head != null + * - (tmp = head).next != tmp || tmp != head + * - head is never gc-unlinked (but may be unlinked) + * Non-invariants: + * - head.item may or may not be null + * - head may not be reachable from the first or last node, or from tail + */ + private transient volatile Node head; + + /** + * A node from which the last node on list (that is, the unique node p + * with p.next == null && p.prev != p) can be reached in O(1) time. + * Invariants: + * - the last node is always O(1) reachable from tail via next links + * - all live nodes are reachable from the last node via pred() + * - tail != null + * - tail is never gc-unlinked (but may be unlinked) + * Non-invariants: + * - tail.item may or may not be null + * - tail may not be reachable from the first or last node, or from head + */ + private transient volatile Node tail; + + private static final Node PREV_TERMINATOR, NEXT_TERMINATOR; + + @SuppressWarnings("unchecked") + Node prevTerminator() { + return (Node) PREV_TERMINATOR; + } + + @SuppressWarnings("unchecked") + Node nextTerminator() { + return (Node) NEXT_TERMINATOR; + } + + static final class Node { + volatile Node prev; + volatile E item; + volatile Node next; + + Node() { // default constructor for NEXT_TERMINATOR, PREV_TERMINATOR + } + + /** + * Constructs a new node. Uses relaxed write because item can + * only be seen after publication via casNext or casPrev. + */ + Node(E item) { + UNSAFE.putObject(this, itemOffset, item); + } + + boolean casItem(E cmp, E val) { + return UNSAFE.compareAndSwapObject(this, itemOffset, cmp, val); + } + + void lazySetNext(Node val) { + UNSAFE.putOrderedObject(this, nextOffset, val); + } + + boolean casNext(Node cmp, Node val) { + return UNSAFE.compareAndSwapObject(this, nextOffset, cmp, val); + } + + void lazySetPrev(Node val) { + UNSAFE.putOrderedObject(this, prevOffset, val); + } + + boolean casPrev(Node cmp, Node val) { + return UNSAFE.compareAndSwapObject(this, prevOffset, cmp, val); + } + + // Unsafe mechanics + + private static final sun.misc.Unsafe UNSAFE; + private static final long prevOffset; + private static final long itemOffset; + private static final long nextOffset; + + static { + try { + UNSAFE = getUnsafe(); + Class k = Node.class; + prevOffset = UNSAFE.objectFieldOffset + (k.getDeclaredField("prev")); + itemOffset = UNSAFE.objectFieldOffset + (k.getDeclaredField("item")); + nextOffset = UNSAFE.objectFieldOffset + (k.getDeclaredField("next")); + } catch (Exception e) { + throw new Error(e); + } + } + } + + /** + * Links e as first element. + */ + private Node linkFirst(E e) { + checkNotNull(e); + final Node newNode = new Node<>(e); + + restartFromHead: + for (;;) + for (Node h = head, p = h, q;;) { + if ((q = p.prev) != null && + (q = (p = q).prev) != null) + // Check for head updates every other hop. + // If p == q, we are sure to follow head instead. + p = (h != (h = head)) ? h : q; + else if (p.next == p) // PREV_TERMINATOR + continue restartFromHead; + else { + // p is first node + newNode.lazySetNext(p); // CAS piggyback + if (p.casPrev(null, newNode)) { + // Successful CAS is the linearization point + // for e to become an element of this deque, + // and for newNode to become "live". + if (p != h) // hop two nodes at a time + casHead(h, newNode); // Failure is OK. + return newNode; + } + // Lost CAS race to another thread; re-read prev + } + } + } + + /** + * Links e as last element. + */ + private Node linkLast(E e) { + checkNotNull(e); + final Node newNode = new Node<>(e); + + restartFromTail: + for (;;) + for (Node t = tail, p = t, q;;) { + if ((q = p.next) != null && + (q = (p = q).next) != null) + // Check for tail updates every other hop. + // If p == q, we are sure to follow tail instead. + p = (t != (t = tail)) ? t : q; + else if (p.prev == p) // NEXT_TERMINATOR + continue restartFromTail; + else { + // p is last node + newNode.lazySetPrev(p); // CAS piggyback + if (p.casNext(null, newNode)) { + // Successful CAS is the linearization point + // for e to become an element of this deque, + // and for newNode to become "live". + if (p != t) // hop two nodes at a time + casTail(t, newNode); // Failure is OK. + return newNode; + } + // Lost CAS race to another thread; re-read next + } + } + } + + private static final int HOPS = 2; + + /** + * Unlinks non-null node x. + */ + void unlink(Node x) { + // assert x != null; + // assert x.item == null; + // assert x != PREV_TERMINATOR; + // assert x != NEXT_TERMINATOR; + + final Node prev = x.prev; + final Node next = x.next; + if (prev == null) { + unlinkFirst(x, next); + } else if (next == null) { + unlinkLast(x, prev); + } else { + // Unlink interior node. + // + // This is the common case, since a series of polls at the + // same end will be "interior" removes, except perhaps for + // the first one, since end nodes cannot be unlinked. + // + // At any time, all active nodes are mutually reachable by + // following a sequence of either next or prev pointers. + // + // Our strategy is to find the unique active predecessor + // and successor of x. Try to fix up their links so that + // they point to each other, leaving x unreachable from + // active nodes. If successful, and if x has no live + // predecessor/successor, we additionally try to gc-unlink, + // leaving active nodes unreachable from x, by rechecking + // that the status of predecessor and successor are + // unchanged and ensuring that x is not reachable from + // tail/head, before setting x's prev/next links to their + // logical approximate replacements, self/TERMINATOR. + Node activePred, activeSucc; + boolean isFirst, isLast; + int hops = 1; + + // Find active predecessor + for (Node p = prev; ; ++hops) { + if (p.item != null) { + activePred = p; + isFirst = false; + break; + } + Node q = p.prev; + if (q == null) { + if (p.next == p) + return; + activePred = p; + isFirst = true; + break; + } + else if (p == q) + return; + else + p = q; + } + + // Find active successor + for (Node p = next; ; ++hops) { + if (p.item != null) { + activeSucc = p; + isLast = false; + break; + } + Node q = p.next; + if (q == null) { + if (p.prev == p) + return; + activeSucc = p; + isLast = true; + break; + } + else if (p == q) + return; + else + p = q; + } + + // TODO: better HOP heuristics + if (hops < HOPS + // always squeeze out interior deleted nodes + && (isFirst | isLast)) + return; + + // Squeeze out deleted nodes between activePred and + // activeSucc, including x. + skipDeletedSuccessors(activePred); + skipDeletedPredecessors(activeSucc); + + // Try to gc-unlink, if possible + if ((isFirst | isLast) && + + // Recheck expected state of predecessor and successor + (activePred.next == activeSucc) && + (activeSucc.prev == activePred) && + (isFirst ? activePred.prev == null : activePred.item != null) && + (isLast ? activeSucc.next == null : activeSucc.item != null)) { + + updateHead(); // Ensure x is not reachable from head + updateTail(); // Ensure x is not reachable from tail + + // Finally, actually gc-unlink + x.lazySetPrev(isFirst ? prevTerminator() : x); + x.lazySetNext(isLast ? nextTerminator() : x); + } + } + } + + /** + * Unlinks non-null first node. + */ + private void unlinkFirst(Node first, Node next) { + // assert first != null; + // assert next != null; + // assert first.item == null; + for (Node o = null, p = next, q;;) { + if (p.item != null || (q = p.next) == null) { + if (o != null && p.prev != p && first.casNext(next, p)) { + skipDeletedPredecessors(p); + if (first.prev == null && + (p.next == null || p.item != null) && + p.prev == first) { + + updateHead(); // Ensure o is not reachable from head + updateTail(); // Ensure o is not reachable from tail + + // Finally, actually gc-unlink + o.lazySetNext(o); + o.lazySetPrev(prevTerminator()); + } + } + return; + } + else if (p == q) + return; + else { + o = p; + p = q; + } + } + } + + /** + * Unlinks non-null last node. + */ + private void unlinkLast(Node last, Node prev) { + // assert last != null; + // assert prev != null; + // assert last.item == null; + for (Node o = null, p = prev, q;;) { + if (p.item != null || (q = p.prev) == null) { + if (o != null && p.next != p && last.casPrev(prev, p)) { + skipDeletedSuccessors(p); + if (last.next == null && + (p.prev == null || p.item != null) && + p.next == last) { + + updateHead(); // Ensure o is not reachable from head + updateTail(); // Ensure o is not reachable from tail + + // Finally, actually gc-unlink + o.lazySetPrev(o); + o.lazySetNext(nextTerminator()); + } + } + return; + } + else if (p == q) + return; + else { + o = p; + p = q; + } + } + } + + /** + * Guarantees that any node which was unlinked before a call to + * this method will be unreachable from head after it returns. + * Does not guarantee to eliminate slack, only that head will + * point to a node that was active while this method was running. + */ + private void updateHead() { + // Either head already points to an active node, or we keep + // trying to cas it to the first node until it does. + Node h, p, q; + restartFromHead: + while ((h = head).item == null && (p = h.prev) != null) { + for (;;) { + if ((q = p.prev) == null || + (q = (p = q).prev) == null) { + // It is possible that p is PREV_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + if (casHead(h, p)) + return; + else + continue restartFromHead; + } + else if (h != head) + continue restartFromHead; + else + p = q; + } + } + } + + /** + * Guarantees that any node which was unlinked before a call to + * this method will be unreachable from tail after it returns. + * Does not guarantee to eliminate slack, only that tail will + * point to a node that was active while this method was running. + */ + private void updateTail() { + // Either tail already points to an active node, or we keep + // trying to cas it to the last node until it does. + Node t, p, q; + restartFromTail: + while ((t = tail).item == null && (p = t.next) != null) { + for (;;) { + if ((q = p.next) == null || + (q = (p = q).next) == null) { + // It is possible that p is NEXT_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + if (casTail(t, p)) + return; + else + continue restartFromTail; + } + else if (t != tail) + continue restartFromTail; + else + p = q; + } + } + } + + private void skipDeletedPredecessors(Node x) { + whileActive: + do { + Node prev = x.prev; + // assert prev != null; + // assert x != NEXT_TERMINATOR; + // assert x != PREV_TERMINATOR; + Node p = prev; + findActive: + for (;;) { + if (p.item != null) + break findActive; + Node q = p.prev; + if (q == null) { + if (p.next == p) + continue whileActive; + break findActive; + } + else if (p == q) + continue whileActive; + else + p = q; + } + + // found active CAS target + if (prev == p || x.casPrev(prev, p)) + return; + + } while (x.item != null || x.next == null); + } + + private void skipDeletedSuccessors(Node x) { + whileActive: + do { + Node next = x.next; + // assert next != null; + // assert x != NEXT_TERMINATOR; + // assert x != PREV_TERMINATOR; + Node p = next; + findActive: + for (;;) { + if (p.item != null) + break findActive; + Node q = p.next; + if (q == null) { + if (p.prev == p) + continue whileActive; + break findActive; + } + else if (p == q) + continue whileActive; + else + p = q; + } + + // found active CAS target + if (next == p || x.casNext(next, p)) + return; + + } while (x.item != null || x.prev == null); + } + + /** + * Returns the successor of p, or the first node if p.next has been + * linked to self, which will only be true if traversing with a + * stale pointer that is now off the list. + */ + final Node succ(Node p) { + // TODO: should we skip deleted nodes here? + Node q = p.next; + return (p == q) ? first() : q; + } + + /** + * Returns the predecessor of p, or the last node if p.prev has been + * linked to self, which will only be true if traversing with a + * stale pointer that is now off the list. + */ + final Node pred(Node p) { + Node q = p.prev; + return (p == q) ? last() : q; + } + + /** + * Returns the first node, the unique node p for which: + * p.prev == null && p.next != p + * The returned node may or may not be logically deleted. + * Guarantees that head is set to the returned node. + */ + Node first() { + restartFromHead: + for (;;) + for (Node h = head, p = h, q;;) { + if ((q = p.prev) != null && + (q = (p = q).prev) != null) + // Check for head updates every other hop. + // If p == q, we are sure to follow head instead. + p = (h != (h = head)) ? h : q; + else if (p == h + // It is possible that p is PREV_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + || casHead(h, p)) + return p; + else + continue restartFromHead; + } + } + + /** + * Returns the last node, the unique node p for which: + * p.next == null && p.prev != p + * The returned node may or may not be logically deleted. + * Guarantees that tail is set to the returned node. + */ + Node last() { + restartFromTail: + for (;;) + for (Node t = tail, p = t, q;;) { + if ((q = p.next) != null && + (q = (p = q).next) != null) + // Check for tail updates every other hop. + // If p == q, we are sure to follow tail instead. + p = (t != (t = tail)) ? t : q; + else if (p == t + // It is possible that p is NEXT_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + || casTail(t, p)) + return p; + else + continue restartFromTail; + } + } + + // Minor convenience utilities + + /** + * Throws NullPointerException if argument is null. + * + * @param v the element + */ + private static void checkNotNull(Object v) { + if (v == null) + throw new NullPointerException(); + } + + /** + * Returns element unless it is null, in which case throws + * NoSuchElementException. + * + * @param v the element + * @return the element + */ + private E screenNullResult(E v) { + if (v == null) + throw new NoSuchElementException(); + return v; + } + + /** + * Creates an array list and fills it with elements of this list. + * Used by toArray. + * + * @return the arrayList + */ + private ArrayList toArrayList() { + ArrayList list = new ArrayList<>(); + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null) + list.add(item); + } + return list; + } + + /** + * Constructs an empty deque. + */ + public FastConcurrentDirectDeque() { + head = tail = new Node<>(null); + } + + /** + * Constructs a deque initially containing the elements of + * the given collection, added in traversal order of the + * collection's iterator. + * + * @param c the collection of elements to initially contain + * @throws NullPointerException if the specified collection or any + * of its elements are null + */ + public FastConcurrentDirectDeque(Collection c) { + // Copy c into a private chain of Nodes + Node h = null, t = null; + for (E e : c) { + checkNotNull(e); + Node newNode = new Node<>(e); + if (h == null) + h = t = newNode; + else { + t.lazySetNext(newNode); + newNode.lazySetPrev(t); + t = newNode; + } + } + initHeadTail(h, t); + } + + /** + * Initializes head and tail, ensuring invariants hold. + */ + private void initHeadTail(Node h, Node t) { + if (h == t) { + if (h == null) + h = t = new Node<>(null); + else { + // Avoid edge case of a single Node with non-null item. + Node newNode = new Node<>(null); + t.lazySetNext(newNode); + newNode.lazySetPrev(t); + t = newNode; + } + } + head = h; + tail = t; + } + + /** + * Inserts the specified element at the front of this deque. + * As the deque is unbounded, this method will never throw + * {@link IllegalStateException}. + * + * @throws NullPointerException if the specified element is null + */ + public void addFirst(E e) { + linkFirst(e); + } + + /** + * Inserts the specified element at the end of this deque. + * As the deque is unbounded, this method will never throw + * {@link IllegalStateException}. + * + *

This method is equivalent to {@link #add}. + * + * @throws NullPointerException if the specified element is null + */ + public void addLast(E e) { + linkLast(e); + } + + /** + * Inserts the specified element at the front of this deque. + * As the deque is unbounded, this method will never return {@code false}. + * + * @return {@code true} (as specified by {@link Deque#offerFirst}) + * @throws NullPointerException if the specified element is null + */ + public boolean offerFirst(E e) { + linkFirst(e); + return true; + } + + public Object offerFirstAndReturnToken(E e) { + return linkFirst(e); + } + + public Object offerLastAndReturnToken(E e) { + return linkLast(e); + } + + public void removeToken(Object token) { + if (!(token instanceof Node)) { + throw new IllegalArgumentException(); + } + + Node node = (Node) (token); + while (! node.casItem(node.item, null)) {} + unlink(node); + } + + /** + * Inserts the specified element at the end of this deque. + * As the deque is unbounded, this method will never return {@code false}. + * + *

This method is equivalent to {@link #add}. + * + * @return {@code true} (as specified by {@link Deque#offerLast}) + * @throws NullPointerException if the specified element is null + */ + public boolean offerLast(E e) { + linkLast(e); + return true; + } + + public E peekFirst() { + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null) + return item; + } + return null; + } + + public E peekLast() { + for (Node p = last(); p != null; p = pred(p)) { + E item = p.item; + if (item != null) + return item; + } + return null; + } + + /** + * @throws NoSuchElementException {@inheritDoc} + */ + public E getFirst() { + return screenNullResult(peekFirst()); + } + + /** + * @throws NoSuchElementException {@inheritDoc} + */ + public E getLast() { + return screenNullResult(peekLast()); + } + + public E pollFirst() { + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null && p.casItem(item, null)) { + unlink(p); + return item; + } + } + return null; + } + + public E pollLast() { + for (Node p = last(); p != null; p = pred(p)) { + E item = p.item; + if (item != null && p.casItem(item, null)) { + unlink(p); + return item; + } + } + return null; + } + + /** + * @throws NoSuchElementException {@inheritDoc} + */ + public E removeFirst() { + return screenNullResult(pollFirst()); + } + + /** + * @throws NoSuchElementException {@inheritDoc} + */ + public E removeLast() { + return screenNullResult(pollLast()); + } + + // *** Queue and stack methods *** + + /** + * Inserts the specified element at the tail of this deque. + * As the deque is unbounded, this method will never return {@code false}. + * + * @return {@code true} (as specified by {@link java.util.Queue#offer}) + * @throws NullPointerException if the specified element is null + */ + public boolean offer(E e) { + return offerLast(e); + } + + /** + * Inserts the specified element at the tail of this deque. + * As the deque is unbounded, this method will never throw + * {@link IllegalStateException} or return {@code false}. + * + * @return {@code true} (as specified by {@link Collection#add}) + * @throws NullPointerException if the specified element is null + */ + public boolean add(E e) { + return offerLast(e); + } + + public E poll() { return pollFirst(); } + public E remove() { return removeFirst(); } + public E peek() { return peekFirst(); } + public E element() { return getFirst(); } + public void push(E e) { addFirst(e); } + public E pop() { return removeFirst(); } + + /** + * Removes the first element {@code e} such that + * {@code o.equals(e)}, if such an element exists in this deque. + * If the deque does not contain the element, it is unchanged. + * + * @param o element to be removed from this deque, if present + * @return {@code true} if the deque contained the specified element + * @throws NullPointerException if the specified element is null + */ + public boolean removeFirstOccurrence(Object o) { + checkNotNull(o); + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null && o.equals(item) && p.casItem(item, null)) { + unlink(p); + return true; + } + } + return false; + } + + /** + * Removes the last element {@code e} such that + * {@code o.equals(e)}, if such an element exists in this deque. + * If the deque does not contain the element, it is unchanged. + * + * @param o element to be removed from this deque, if present + * @return {@code true} if the deque contained the specified element + * @throws NullPointerException if the specified element is null + */ + public boolean removeLastOccurrence(Object o) { + checkNotNull(o); + for (Node p = last(); p != null; p = pred(p)) { + E item = p.item; + if (item != null && o.equals(item) && p.casItem(item, null)) { + unlink(p); + return true; + } + } + return false; + } + + /** + * Returns {@code true} if this deque contains at least one + * element {@code e} such that {@code o.equals(e)}. + * + * @param o element whose presence in this deque is to be tested + * @return {@code true} if this deque contains the specified element + */ + public boolean contains(Object o) { + if (o == null) return false; + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null && o.equals(item)) + return true; + } + return false; + } + + /** + * Returns {@code true} if this collection contains no elements. + * + * @return {@code true} if this collection contains no elements + */ + public boolean isEmpty() { + return peekFirst() == null; + } + + /** + * Returns the number of elements in this deque. If this deque + * contains more than {@code Integer.MAX_VALUE} elements, it + * returns {@code Integer.MAX_VALUE}. + * + *

Beware that, unlike in most collections, this method is + * NOT a constant-time operation. Because of the + * asynchronous nature of these deques, determining the current + * number of elements requires traversing them all to count them. + * Additionally, it is possible for the size to change during + * execution of this method, in which case the returned result + * will be inaccurate. Thus, this method is typically not very + * useful in concurrent applications. + * + * @return the number of elements in this deque + */ + public int size() { + int count = 0; + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + // Collection.size() spec says to max out + if (++count == Integer.MAX_VALUE) + break; + return count; + } + + /** + * Removes the first element {@code e} such that + * {@code o.equals(e)}, if such an element exists in this deque. + * If the deque does not contain the element, it is unchanged. + * + * @param o element to be removed from this deque, if present + * @return {@code true} if the deque contained the specified element + * @throws NullPointerException if the specified element is null + */ + public boolean remove(Object o) { + return removeFirstOccurrence(o); + } + + /** + * Appends all of the elements in the specified collection to the end of + * this deque, in the order that they are returned by the specified + * collection's iterator. Attempts to {@code addAll} of a deque to + * itself result in {@code IllegalArgumentException}. + * + * @param c the elements to be inserted into this deque + * @return {@code true} if this deque changed as a result of the call + * @throws NullPointerException if the specified collection or any + * of its elements are null + * @throws IllegalArgumentException if the collection is this deque + */ + public boolean addAll(Collection c) { + if (c == this) + // As historically specified in AbstractQueue#addAll + throw new IllegalArgumentException(); + + // Copy c into a private chain of Nodes + Node beginningOfTheEnd = null, last = null; + for (E e : c) { + checkNotNull(e); + Node newNode = new Node<>(e); + if (beginningOfTheEnd == null) + beginningOfTheEnd = last = newNode; + else { + last.lazySetNext(newNode); + newNode.lazySetPrev(last); + last = newNode; + } + } + if (beginningOfTheEnd == null) + return false; + + // Atomically append the chain at the tail of this collection + restartFromTail: + for (;;) + for (Node t = tail, p = t, q;;) { + if ((q = p.next) != null && + (q = (p = q).next) != null) + // Check for tail updates every other hop. + // If p == q, we are sure to follow tail instead. + p = (t != (t = tail)) ? t : q; + else if (p.prev == p) // NEXT_TERMINATOR + continue restartFromTail; + else { + // p is last node + beginningOfTheEnd.lazySetPrev(p); // CAS piggyback + if (p.casNext(null, beginningOfTheEnd)) { + // Successful CAS is the linearization point + // for all elements to be added to this deque. + if (!casTail(t, last)) { + // Try a little harder to update tail, + // since we may be adding many elements. + t = tail; + if (last.next == null) + casTail(t, last); + } + return true; + } + // Lost CAS race to another thread; re-read next + } + } + } + + /** + * Removes all of the elements from this deque. + */ + public void clear() { + while (pollFirst() != null) { } + } + + /** + * Returns an array containing all of the elements in this deque, in + * proper sequence (from first to last element). + * + *

The returned array will be "safe" in that no references to it are + * maintained by this deque. (In other words, this method must allocate + * a new array). The caller is thus free to modify the returned array. + * + *

This method acts as bridge between array-based and collection-based + * APIs. + * + * @return an array containing all of the elements in this deque + */ + public Object[] toArray() { + return toArrayList().toArray(); + } + + /** + * Returns an array containing all of the elements in this deque, + * in proper sequence (from first to last element); the runtime + * type of the returned array is that of the specified array. If + * the deque fits in the specified array, it is returned therein. + * Otherwise, a new array is allocated with the runtime type of + * the specified array and the size of this deque. + * + *

If this deque fits in the specified array with room to spare + * (i.e., the array has more elements than this deque), the element in + * the array immediately following the end of the deque is set to + * {@code null}. + * + *

Like the {@link #toArray()} method, this method acts as + * bridge between array-based and collection-based APIs. Further, + * this method allows precise control over the runtime type of the + * output array, and may, under certain circumstances, be used to + * save allocation costs. + * + *

Suppose {@code x} is a deque known to contain only strings. + * The following code can be used to dump the deque into a newly + * allocated array of {@code String}: + * + *

 {@code String[] y = x.toArray(new String[0]);}
+ * + * Note that {@code toArray(new Object[0])} is identical in function to + * {@code toArray()}. + * + * @param a the array into which the elements of the deque are to + * be stored, if it is big enough; otherwise, a new array of the + * same runtime type is allocated for this purpose + * @return an array containing all of the elements in this deque + * @throws ArrayStoreException if the runtime type of the specified array + * is not a supertype of the runtime type of every element in + * this deque + * @throws NullPointerException if the specified array is null + */ + public T[] toArray(T[] a) { + return toArrayList().toArray(a); + } + + /** + * Returns an iterator over the elements in this deque in proper sequence. + * The elements will be returned in order from first (head) to last (tail). + * + *

The returned iterator is a "weakly consistent" iterator that + * will never throw {@link java.util.ConcurrentModificationException + * ConcurrentModificationException}, and guarantees to traverse + * elements as they existed upon construction of the iterator, and + * may (but is not guaranteed to) reflect any modifications + * subsequent to construction. + * + * @return an iterator over the elements in this deque in proper sequence + */ + public Iterator iterator() { + return new Itr(); + } + + /** + * Returns an iterator over the elements in this deque in reverse + * sequential order. The elements will be returned in order from + * last (tail) to first (head). + * + *

The returned iterator is a "weakly consistent" iterator that + * will never throw {@link java.util.ConcurrentModificationException + * ConcurrentModificationException}, and guarantees to traverse + * elements as they existed upon construction of the iterator, and + * may (but is not guaranteed to) reflect any modifications + * subsequent to construction. + * + * @return an iterator over the elements in this deque in reverse order + */ + public Iterator descendingIterator() { + return new DescendingItr(); + } + + private abstract class AbstractItr implements Iterator { + /** + * Next node to return item for. + */ + private Node nextNode; + + /** + * nextItem holds on to item fields because once we claim + * that an element exists in hasNext(), we must return it in + * the following next() call even if it was in the process of + * being removed when hasNext() was called. + */ + private E nextItem; + + /** + * Node returned by most recent call to next. Needed by remove. + * Reset to null if this element is deleted by a call to remove. + */ + private Node lastRet; + + abstract Node startNode(); + abstract Node nextNode(Node p); + + AbstractItr() { + advance(); + } + + /** + * Sets nextNode and nextItem to next valid node, or to null + * if no such. + */ + private void advance() { + lastRet = nextNode; + + Node p = (nextNode == null) ? startNode() : nextNode(nextNode); + for (;; p = nextNode(p)) { + if (p == null) { + // p might be active end or TERMINATOR node; both are OK + nextNode = null; + nextItem = null; + break; + } + E item = p.item; + if (item != null) { + nextNode = p; + nextItem = item; + break; + } + } + } + + public boolean hasNext() { + return nextItem != null; + } + + public E next() { + E item = nextItem; + if (item == null) throw new NoSuchElementException(); + advance(); + return item; + } + + public void remove() { + Node l = lastRet; + if (l == null) throw new IllegalStateException(); + l.item = null; + unlink(l); + lastRet = null; + } + } + + /** Forward iterator */ + private class Itr extends AbstractItr { + Node startNode() { return first(); } + Node nextNode(Node p) { return succ(p); } + } + + /** Descending iterator */ + private class DescendingItr extends AbstractItr { + Node startNode() { return last(); } + Node nextNode(Node p) { return pred(p); } + } + + /** + * Saves this deque to a stream (that is, serializes it). + * + * @serialData All of the elements (each an {@code E}) in + * the proper order, followed by a null + */ + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + + // Write out any hidden stuff + s.defaultWriteObject(); + + // Write out all elements in the proper order. + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null) + s.writeObject(item); + } + + // Use trailing null as sentinel + s.writeObject(null); + } + + /** + * Reconstitutes this deque from a stream (that is, deserializes it). + */ + private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + s.defaultReadObject(); + + // Read in elements until trailing null sentinel found + Node h = null, t = null; + Object item; + while ((item = s.readObject()) != null) { + @SuppressWarnings("unchecked") + Node newNode = new Node<>((E) item); + if (h == null) + h = t = newNode; + else { + t.lazySetNext(newNode); + newNode.lazySetPrev(t); + t = newNode; + } + } + initHeadTail(h, t); + } + + private boolean casHead(Node cmp, Node val) { + return UNSAFE.compareAndSwapObject(this, headOffset, cmp, val); + } + + private boolean casTail(Node cmp, Node val) { + return UNSAFE.compareAndSwapObject(this, tailOffset, cmp, val); + } + + // Unsafe mechanics + + private static final sun.misc.Unsafe UNSAFE; + private static final long headOffset; + private static final long tailOffset; + static { + PREV_TERMINATOR = new Node<>(); + PREV_TERMINATOR.next = PREV_TERMINATOR; + NEXT_TERMINATOR = new Node<>(); + NEXT_TERMINATOR.prev = NEXT_TERMINATOR; + try { + UNSAFE = getUnsafe(); + Class k = FastConcurrentDirectDeque.class; + headOffset = UNSAFE.objectFieldOffset + (k.getDeclaredField("head")); + tailOffset = UNSAFE.objectFieldOffset + (k.getDeclaredField("tail")); + } catch (Exception e) { + throw new Error(e); + } + } + + private static Unsafe getUnsafe() { + if (System.getSecurityManager() != null) { + return new PrivilegedAction() { + public Unsafe run() { + return getUnsafe0(); + } + }.run(); + } + return getUnsafe0(); + } + + private static Unsafe getUnsafe0() { + try { + Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); + theUnsafe.setAccessible(true); + return (Unsafe) theUnsafe.get(null); + } catch (Throwable t) { + throw new RuntimeException("JDK did not allow accessing unsafe", t); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/FileUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/FileUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/FileUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,155 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.BufferedInputStream; +import java.io.BufferedOutputStream; +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URL; + +/** + * @author Stuart Douglas + */ +public class FileUtils { + + private FileUtils() { + + } + + public static String readFile(Class testClass, String fileName) { + final URL res = testClass.getResource(fileName); + return readFile(res); + } + + public static String readFile(URL url) { + try { + return readFile(url.openStream()); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String readFile(final File file) { + try { + return readFile(new FileInputStream(file)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public static String readFile(InputStream file) { + BufferedInputStream stream = null; + try { + stream = new BufferedInputStream(file); + byte[] buff = new byte[1024]; + StringBuilder builder = new StringBuilder(); + int read = -1; + while ((read = stream.read(buff)) != -1) { + builder.append(new String(buff, 0, read)); + } + return builder.toString(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + if (stream != null) { + try { + stream.close(); + } catch (IOException e) { + //ignore + } + } + } + } + + + public static File getFileOrCheckParentsIfNotFound(String baseStr, String path) throws FileNotFoundException { + //File f = new File( System.getProperty("jbossas.project.dir", "../../..") ); + File base = new File(baseStr); + if (!base.exists()) { + throw new FileNotFoundException("Base path not found: " + base.getPath()); + } + base = base.getAbsoluteFile(); + + File f = new File(base, path); + if (f.exists()) + return f; + + File fLast = f; + while (!f.exists()) { + int slash = path.lastIndexOf(File.separatorChar); + if (slash <= 0) // no slash or "/xxx" + throw new FileNotFoundException("Path not found: " + f.getPath()); + path = path.substring(0, slash); + fLast = f; + f = new File(base, path); + } + // When first existing is found, report the last non-existent. + throw new FileNotFoundException("Path not found: " + fLast.getPath()); + } + + + public static void copyFile(final File src, final File dest) throws IOException { + final InputStream in = new BufferedInputStream(new FileInputStream(src)); + try { + copyFile(in, dest); + } finally { + close(in); + } + } + + public static void copyFile(final InputStream in, final File dest) throws IOException { + dest.getParentFile().mkdirs(); + final OutputStream out = new BufferedOutputStream(new FileOutputStream(dest)); + try { + int i = in.read(); + while (i != -1) { + out.write(i); + i = in.read(); + } + } finally { + close(out); + } + } + + + public static void close(Closeable closeable) { + try { + closeable.close(); + } catch (IOException ignore) { + } + } + + public static void deleteRecursive(final File file) { + File[] files = file.listFiles(); + if (files != null) { + for (File f : files) { + deleteRecursive(f); + } + } + file.delete(); + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/FlexBase64.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/FlexBase64.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/FlexBase64.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,1703 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.lang.reflect.Constructor; +import java.nio.ByteBuffer; +import java.security.AccessController; +import java.security.PrivilegedExceptionAction; + +/** + * An efficient and flexible MIME Base64 implementation. + * + * @author Jason T. Greene + */ +public class FlexBase64 { + /* + * Note that this code heavily favors performance over reuse and clean style. + */ + + private static final byte[] ENCODING_TABLE; + private static final byte[] DECODING_TABLE = new byte[80]; + private static final Constructor STRING_CONSTRUCTOR; + + static { + try { + ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes("ASCII"); + } catch (UnsupportedEncodingException e) { + throw new IllegalStateException(); + } + + for (int i = 0; i < ENCODING_TABLE.length; i++) { + int v = (ENCODING_TABLE[i] & 0xFF) - 43; + DECODING_TABLE[v] = (byte)(i + 1); // zero = illegal + } + + Constructor c = null; + try { + PrivilegedExceptionAction> runnable = new PrivilegedExceptionAction>() { + @Override + public Constructor run() throws Exception { + Constructor c; + c = String.class.getDeclaredConstructor(char[].class, boolean.class); + c.setAccessible(true); + return c; + } + }; + if (System.getSecurityManager() != null) { + c = AccessController.doPrivileged(runnable); + } else { + c = runnable.run(); + } + } catch (Throwable t) { + } + + STRING_CONSTRUCTOR = c; + } + + /** + * Creates a state driven base64 encoder. + * + *

The Encoder instance is not thread-safe, and must not be shared between threads without establishing a + * happens-before relationship.

+ * + * @param wrap whether or not to wrap at 76 characters with CRLF + * @return an createEncoder instance + */ + public static Encoder createEncoder(boolean wrap) { + return new Encoder(wrap); + } + + /** + * Creates a state driven base64 decoder. + * + *

The Decoder instance is not thread-safe, and must not be shared between threads without establishing a + * happens-before relationship.

+ * + * @return a new createDecoder instance + */ + public static Decoder createDecoder() { + return new Decoder(); + } + + /** + * Encodes a fixed and complete byte array into a Base64 String. + * + *

This method is only useful for applications which require a String and have all data to be encoded up-front. + * Note that byte arrays or buffers are almost always a better storage choice. They consume half the memory and + * can be reused (modified). In other words, it is almost always better to use {@link #encodeBytes}, + * {@link #createEncoder}, or {@link #createEncoderOutputStream} instead. + * instead. + * + * @param source the byte array to encode from + * @param wrap whether or not to wrap the output at 76 chars with CRLFs + * @return a new String representing the Base64 output + */ + public static String encodeString(byte[] source, boolean wrap) { + return Encoder.encodeString(source, 0, source.length, wrap); + } + + /** + * Encodes a fixed and complete byte array into a Base64 String. + * + *

This method is only useful for applications which require a String and have all data to be encoded up-front. + * Note that byte arrays or buffers are almost always a better storage choice. They consume half the memory and + * can be reused (modified). In other words, it is almost always better to use {@link #encodeBytes}, + * {@link #createEncoder}, or {@link #createEncoderOutputStream} instead.

+ * + *

+     *    // Encodes "ell"
+     *    FlexBase64.encodeString("hello".getBytes("US-ASCII"), 1, 4);
+     * 
+ * + * @param source the byte array to encode from + * @param pos the position to start encoding from + * @param limit the position to halt encoding at (exclusive) + * @param wrap whether or not to wrap the output at 76 chars with CRLFs + * @return a new String representing the Base64 output + */ + public static String encodeString(byte[] source, int pos, int limit, boolean wrap) { + return Encoder.encodeString(source, pos, limit, wrap); + } + + /** + * Encodes a fixed and complete byte buffer into a Base64 String. + * + *

This method is only useful for applications which require a String and have all data to be encoded up-front. + * Note that byte arrays or buffers are almost always a better storage choice. They consume half the memory and + * can be reused (modified). In other words, it is almost always better to use {@link #encodeBytes}, + * {@link #createEncoder}, or {@link #createEncoderOutputStream} instead.

+ * + *

+     *    // Encodes "ell"
+     *    FlexBase64.ecncodeString("hello".getBytes("US-ASCII"), 1, 4);
+     * 
+ * + * @param source the byte buffer to encode from + * @param wrap whether or not to wrap the output at 76 chars with CRLFs + * @return a new String representing the Base64 output + */ + public static String encodeString(ByteBuffer source, boolean wrap) { + return Encoder.encodeString(source, wrap); + } + + /** + * Encodes a fixed and complete byte buffer into a Base64 byte array. + * + *

+     *    // Encodes "ell"
+     *    FlexBase64.ecncodeString("hello".getBytes("US-ASCII"), 1, 4);
+     * 
+ * + * @param source the byte array to encode from + * @param pos the position to start encoding at + * @param limit the position to halt encoding at (exclusive) + * @param wrap whether or not to wrap at 76 characters with CRLFs + * @return a new byte array containing the encoded ASCII values + */ + public static byte[] encodeBytes(byte[] source, int pos, int limit, boolean wrap) { + return Encoder.encodeBytes(source, pos, limit, wrap); + } + + /** + * Decodes a Base64 encoded string into a new byte buffer. The returned byte buffer is a heap buffer, + * and it is therefor possible to retrieve the backing array using {@link java.nio.ByteBuffer#array()}, + * {@link java.nio.ByteBuffer#arrayOffset()} and {@link java.nio.ByteBuffer#limit()}. The latter is very + * important since the decoded array may be larger than the decoded data. This is due to length estimation which + * avoids an unnecessary array copy. + * + * @param source the Base64 string to decode + * @return a byte buffer containing the decoded output + * @throws IOException if the encoding is invalid or corrupted + */ + public static ByteBuffer decode(String source) throws IOException { + return Decoder.decode(source); + } + + /** + * Decodes a Base64 encoded byte buffer into a new byte buffer. The returned byte buffer is a heap buffer, + * and it is therefor possible to retrieve the backing array using {@link java.nio.ByteBuffer#array()}, + * {@link java.nio.ByteBuffer#arrayOffset()} and {@link java.nio.ByteBuffer#limit()}. The latter is very + * important since the decoded array may be larger than the decoded data. This is due to length estimation which + * avoids an unnecessary array copy. + * + * @param source the Base64 content to decode + * @return a byte buffer containing the decoded output + * @throws IOException if the encoding is invalid or corrupted + */ + public static ByteBuffer decode(ByteBuffer source) throws IOException { + return Decoder.decode(source); + } + + + /** + * Decodes a Base64 encoded byte array into a new byte buffer. The returned byte buffer is a heap buffer, + * and it is therefor possible to retrieve the backing array using {@link java.nio.ByteBuffer#array()}, + * {@link java.nio.ByteBuffer#arrayOffset()} and {@link java.nio.ByteBuffer#limit()}. The latter is very + * important since the decoded array may be larger than the decoded data. This is due to length estimation which + * avoids an unnecessary array copy. + * + * @param source the Base64 content to decode + * @param off position to start decoding from in source + * @param limit position to stop decoding in source (exclusive) + * @return a byte buffer containing the decoded output + * @throws IOException if the encoding is invalid or corrupted + */ + public static ByteBuffer decode(byte[] source, int off, int limit) throws IOException { + return Decoder.decode(source, off, limit); + } + + + /** + * Creates an InputStream wrapper which encodes a source into base64 as it is read, until the source hits EOF. + * Upon hitting EOF, a standard base64 termination sequence will be readable. Clients can simply treat this input + * stream as if they were reading from a base64 encoded file. This stream attempts to read and encode in buffer + * size chunks from the source, in order to improve overall performance. Thus, BufferInputStream is not necessary + * and will lead to double buffering. + * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param source an input source to read from + * @param bufferSize the chunk size to buffer from the source + * @param wrap whether or not the stream should wrap base64 output at 76 characters + * @return an encoded input stream instance. + */ + public static EncoderInputStream createEncoderInputStream(InputStream source, int bufferSize, boolean wrap) { + return new EncoderInputStream(source, bufferSize, wrap); + } + + + /** + * Creates an InputStream wrapper which encodes a source into base64 as it is read, until the source hits EOF. + * Upon hitting EOF, a standard base64 termination sequence will be readable. Clients can simply treat this input + * stream as if they were reading from a base64 encoded file. This stream attempts to read and encode in 8192 byte + * chunks. Thus, BufferedInputStream is not necessary as a source and will lead to double buffering. + * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param source an input source to read from + * @return an encoded input stream instance. + */ + public static EncoderInputStream createEncoderInputStream(InputStream source) { + return new EncoderInputStream(source); + } + + /** + * Creates an InputStream wrapper which decodes a base64 input source into the decoded content as it is read, + * until the source hits EOF. Upon hitting EOF, a standard base64 termination sequence will be readable. + * Clients can simply treat this input stream as if they were reading from a base64 encoded file. This stream + * attempts to read and encode in buffer size byte chunks. Thus, BufferedInputStream is not necessary + * as a source and will lead to double buffering. + * + *

Note that the end of a base64 stream can not reliably be detected, so if multiple base64 streams exist on the + * wire, the source stream will need to simulate an EOF when the boundary mechanism is detected.

+ * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param source an input source to read from + * @param bufferSize the chunk size to buffer before when reading from the target + * @return a decoded input stream instance. + */ + public static DecoderInputStream createDecoderInputStream(InputStream source, int bufferSize) { + return new DecoderInputStream(source, bufferSize); + } + + + /** + * Creates an InputStream wrapper which decodes a base64 input source into the decoded content as it is read, + * until the source hits EOF. Upon hitting EOF, a standard base64 termination sequence will be readable. + * Clients can simply treat this input stream as if they were reading from a base64 encoded file. This stream + * attempts to read and encode in 8192 byte chunks. Thus, BufferedInputStream is not necessary + * as a source and will lead to double buffering. + * + *

Note that the end of a base64 stream can not reliably be detected, so if multiple base64 streams exist on the + * wire, the source stream will need to simulate an EOF when the boundary mechanism is detected.

+ * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param source an input source to read from + * @return a decoded input stream instance. + */ + public static DecoderInputStream createDecoderInputStream(InputStream source) { + return new DecoderInputStream(source); + } + + /** + * Creates an OutputStream wrapper which base64 encodes and writes to the passed OutputStream target. When this + * stream is closed base64 padding will be added if needed. Alternatively if this represents an "inner stream", + * the {@link FlexBase64.EncoderOutputStream#complete()} method can be called to close out + * the inner stream without closing the wrapped target. + * + *

All bytes written will be queued to a buffer in the specified size. This stream, therefore, does not require + * BufferedOutputStream, which would lead to double buffering. + * + * @param target an output target to write to + * @param bufferSize the chunk size to buffer before writing to the target + * @param wrap whether or not the stream should wrap base64 output at 76 characters + * @return an encoded output stream instance. + */ + public static EncoderOutputStream createEncoderOutputStream(OutputStream target, int bufferSize, boolean wrap) { + return new EncoderOutputStream(target, bufferSize, wrap); + } + + + /** + * Creates an OutputStream wrapper which base64 encodes and writes to the passed OutputStream target. When this + * stream is closed base64 padding will be added if needed. Alternatively if this represents an "inner stream", + * the {@link FlexBase64.EncoderOutputStream#complete()} method can be called to close out + * the inner stream without closing the wrapped target. + * + *

All bytes written will be queued to an 8192 byte buffer. This stream, therefore, does not require + * BufferedOutputStream, which would lead to double buffering.

+ * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param output the output stream to write encoded output to + * @return an encoded output stream instance. + */ + public static EncoderOutputStream createEncoderOutputStream(OutputStream output) { + return new EncoderOutputStream(output); + } + + + /** + * Creates an OutputStream wrapper which decodes base64 content before writing to the passed OutputStream target. + * + *

All bytes written will be queued to a buffer using the specified buffer size. This stream, therefore, does + * not require BufferedOutputStream, which would lead to double buffering.

+ * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param output the output stream to write decoded output to + * @param bufferSize the buffer size to buffer writes to + * @return a decoded output stream instance. + */ + public static DecoderOutputStream createDecoderOutputStream(OutputStream output, int bufferSize) { + return new DecoderOutputStream(output, bufferSize); + } + + /** + * Creates an OutputStream wrapper which decodes base64 content before writing to the passed OutputStream target. + * + *

All bytes written will be queued to an 8192 byte buffer. This stream, therefore, does + * not require BufferedOutputStream, which would lead to double buffering.

+ * + *

This stream is not thread-safe, and should not be shared between threads, without establishing a + * happens-before relationship.

+ * + * @param output the output stream to write decoded output to + * @return a decoded output stream instance. + */ + public static DecoderOutputStream createDecoderOutputStream(OutputStream output) { + return new DecoderOutputStream(output); + } + + /** + * Controls the encoding process. + */ + public static final class Encoder { + private int state; + private int last; + private int count; + private final boolean wrap; + private int lastPos; + + private Encoder(boolean wrap) { + this.wrap = wrap; + } + + /** + * Encodes bytes read from source and writes them in base64 format to target. If the source limit is hit, this + * method will return and save the current state, such that future calls can resume the encoding process. + * In addition, if the target does not have the capacity to fit an entire quad of bytes, this method will also + * return and save state for subsequent calls to this method. Once all bytes have been encoded to the target, + * {@link #complete(java.nio.ByteBuffer)} should be called to add the necessary padding characters. + * + * @param source the byte buffer to read from + * @param target the byte buffer to write to + */ + public void encode(ByteBuffer source, ByteBuffer target) { + if (target == null) + throw new IllegalStateException(); + + int last = this.last; + int state = this.state; + boolean wrap = this.wrap; + int count = this.count; + final byte[] ENCODING_TABLE = FlexBase64.ENCODING_TABLE; + + int remaining = source.remaining(); + while (remaining > 0) { + // Unrolled state machine for performance (resumes and executes all states in one iteration) + int require = 4 - state; + require = wrap && (count >= 72) ? require + 2 : require; + if (target.remaining() < require) { + break; + } + // ( 6 | 2) (4 | 4) (2 | 6) + int b = source.get() & 0xFF; + if (state == 0) { + target.put(ENCODING_TABLE[b >>> 2]); + last = (b & 0x3) << 4; + state++; + if (--remaining <= 0) { + break; + } + b = source.get() & 0xFF; + } + if (state == 1) { + target.put(ENCODING_TABLE[last | (b >>> 4)]); + last = (b & 0x0F) << 2; + state++; + if (--remaining <= 0) { + break; + } + b = source.get() & 0xFF; + } + if (state == 2) { + target.put(ENCODING_TABLE[last | (b >>> 6)]); + target.put(ENCODING_TABLE[b & 0x3F]); + last = state = 0; + remaining--; + } + + if (wrap) { + count += 4; + if (count >= 76) { + count = 0; + target.putShort((short)0x0D0A); + } + } + } + this.count = count; + this.last = last; + this.state = state; + this.lastPos = source.position(); + } + + /** + * Encodes bytes read from source and writes them in base64 format to target. If the source limit is hit, this + * method will return and save the current state, such that future calls can resume the encoding process. + * In addition, if the target does not have the capacity to fit an entire quad of bytes, this method will also + * return and save state for subsequent calls to this method. Once all bytes have been encoded to the target, + * {@link #complete(byte[], int)} should be called to add the necessary padding characters. In order to + * determine the last read position, the {@link #getLastInputPosition()} can be used. + * + *

Note that the limit values are not lengths, they are positions similar to + * {@link java.nio.ByteBuffer#limit()}. To calculate a length simply subtract position from limit.

+ * + *

+         *  Encoder encoder = FlexBase64.createEncoder(false);
+         *  byte[] outBuffer = new byte[10];
+         *  // Encode "ell"
+         *  int outPosition = encoder.encode("hello".getBytes("US-ASCII"), 1, 4, outBuffer, 5, 10);
+         *  // Prints "9 : ZWxs"
+         *  System.out.println(outPosition + " : " + new String(outBuffer, 0, 5, outPosition - 5));
+         * 
+ * + * @param source the byte array to read from + * @param pos ths position in the byte array to start reading from + * @param limit the position in the byte array that is after the end of the source data + * @param target the byte array to write base64 bytes to + * @param opos the position to start writing to the target array at + * @param olimit the position in the target byte array that makes the end of the writable area (exclusive) + * @return the position in the target array immediately following the last byte written + */ + public int encode(byte[] source, int pos, int limit, byte[] target, int opos, int olimit) { + if (target == null) + throw new IllegalStateException(); + + int last = this.last; + int state = this.state; + int count = this.count; + boolean wrap = this.wrap; + final byte[] ENCODING_TABLE = FlexBase64.ENCODING_TABLE; + + + while (limit > pos) { + // Unrolled state machine for performance (resumes and executes all states in one iteration) + int require = 4 - state; + require = wrap && count >= 72 ? require + 2 : require; + if ((require + opos) > olimit) { + break; + } + // ( 6 | 2) (4 | 4) (2 | 6) + int b = source[pos++] & 0xFF; + if (state == 0) { + target[opos++] = ENCODING_TABLE[b >>> 2]; + last = (b & 0x3) << 4; + state++; + if (pos >= limit) { + break; + } + b = source[pos++] & 0xFF; + } + if (state == 1) { + target[opos++] = ENCODING_TABLE[last | (b >>> 4)]; + last = (b & 0x0F) << 2; + state++; + if (pos >= limit) { + break; + } + b = source[pos++] & 0xFF; + } + if (state == 2) { + target[opos++] = ENCODING_TABLE[last | (b >>> 6)]; + target[opos++] = ENCODING_TABLE[b & 0x3F]; + + last = state = 0; + } + + if (wrap) { + count += 4; + if (count >= 76) { + count = 0; + target[opos++] = 0x0D; + target[opos++] = 0x0A; + } + } + } + this.count = count; + this.last = last; + this.state = state; + this.lastPos = pos; + + return opos; + } + + + private static String encodeString(byte[] source, int pos, int limit, boolean wrap) { + int olimit = (limit - pos); + int remainder = olimit % 3; + olimit = (olimit + (remainder == 0 ? 0 : 3 - remainder)) / 3 * 4; + olimit += (wrap ? (olimit / 76) * 2 + 2 : 0); + char[] target = new char[olimit]; + int opos = 0; + int last = 0; + int count = 0; + int state = 0; + final byte[] ENCODING_TABLE = FlexBase64.ENCODING_TABLE; + + while (limit > pos) { + // ( 6 | 2) (4 | 4) (2 | 6) + int b = source[pos++] & 0xFF; + target[opos++] = (char) ENCODING_TABLE[b >>> 2]; + last = (b & 0x3) << 4; + if (pos >= limit) { + state = 1; + break; + } + b = source[pos++] & 0xFF; + target[opos++] = (char) ENCODING_TABLE[last | (b >>> 4)]; + last = (b & 0x0F) << 2; + if (pos >= limit) { + state = 2; + break; + } + b = source[pos++] & 0xFF; + target[opos++] = (char) ENCODING_TABLE[last | (b >>> 6)]; + target[opos++] = (char) ENCODING_TABLE[b & 0x3F]; + + if (wrap) { + count += 4; + if (count >= 76) { + count = 0; + target[opos++] = 0x0D; + target[opos++] = 0x0A; + } + } + } + + complete(target, opos, state, last, wrap); + + try { + // Eliminate copying on Open/Oracle JDK + if (STRING_CONSTRUCTOR != null) { + return STRING_CONSTRUCTOR.newInstance(target, Boolean.TRUE); + } + } catch (Exception e) { + } + + return new String(target); + } + + private static byte[] encodeBytes(byte[] source, int pos, int limit, boolean wrap) { + int olimit = (limit - pos); + int remainder = olimit % 3; + olimit = (olimit + (remainder == 0 ? 0 : 3 - remainder)) / 3 * 4; + olimit += (wrap ? (olimit / 76) * 2 + 2 : 0); + byte[] target = new byte[olimit]; + int opos = 0; + int count = 0; + int last = 0; + int state = 0; + final byte[] ENCODING_TABLE = FlexBase64.ENCODING_TABLE; + + while (limit > pos) { + // ( 6 | 2) (4 | 4) (2 | 6) + int b = source[pos++] & 0xFF; + target[opos++] = ENCODING_TABLE[b >>> 2]; + last = (b & 0x3) << 4; + if (pos >= limit) { + state = 1; + break; + } + b = source[pos++] & 0xFF; + target[opos++] = ENCODING_TABLE[last | (b >>> 4)]; + last = (b & 0x0F) << 2; + if (pos >= limit) { + state = 2; + break; + } + b = source[pos++] & 0xFF; + target[opos++] = ENCODING_TABLE[last | (b >>> 6)]; + target[opos++] = ENCODING_TABLE[b & 0x3F]; + + if (wrap) { + count += 4; + if (count >= 76) { + count = 0; + target[opos++] = 0x0D; + target[opos++] = 0x0A; + } + } + } + + complete(target, opos, state, last, wrap); + + return target; + } + + private static String encodeString(ByteBuffer source, boolean wrap) { + int remaining = source.remaining(); + int remainder = remaining % 3; + int olimit = (remaining + (remainder == 0 ? 0 : 3 - remainder)) / 3 * 4; + olimit += (wrap ? olimit / 76 * 2 + 2 : 0); + char[] target = new char[olimit]; + int opos = 0; + int last = 0; + int state = 0; + int count = 0; + final byte[] ENCODING_TABLE = FlexBase64.ENCODING_TABLE; + + + while (remaining > 0) { + // ( 6 | 2) (4 | 4) (2 | 6) + int b = source.get() & 0xFF; + target[opos++] = (char) ENCODING_TABLE[b >>> 2]; + last = (b & 0x3) << 4; + if (--remaining <= 0) { + state = 1; + break; + } + b = source.get() & 0xFF; + target[opos++] = (char) ENCODING_TABLE[last | (b >>> 4)]; + last = (b & 0x0F) << 2; + if (--remaining <= 0) { + state = 2; + break; + } + b = source.get() & 0xFF; + target[opos++] = (char) ENCODING_TABLE[last | (b >>> 6)]; + target[opos++] = (char) ENCODING_TABLE[b & 0x3F]; + remaining--; + + if (wrap) { + count += 4; + if (count >= 76) { + count = 0; + target[opos++] = 0x0D; + target[opos++] = 0x0A; + } + } + } + + complete(target, opos, state, last, wrap); + + try { + // Eliminate copying on Open/Oracle JDK + if (STRING_CONSTRUCTOR != null) { + return STRING_CONSTRUCTOR.newInstance(target, Boolean.TRUE); + } + } catch (Exception e) { + } + + return new String(target); + } + + /** + * Gets the last position where encoding left off in the last byte array that was used. + * If the target for encoded content does not have the necessary capacity, this method should be used to + * determine where to start from on subsequent reads. + * + * @return the last known read position + */ + public int getLastInputPosition() { + return lastPos; + } + + /** + * Completes an encoding session by writing out the necessary padding. This is essential to complying + * with the Base64 format. This method will write at most 4 or 2 bytes starting at pos,depending on + * whether or not wrapping is enabled. + * + *

+         *  Encoder encoder = FlexBase64.createEncoder(false);
+         *  byte[] outBuffer = new byte[13];
+         *
+         *  // Encodes "ello"
+         *  int outPosition = encoder.encode("hello".getBytes("US-ASCII"), 0, 4, outBuffer, 5, 13);
+         *  outPosition = encoder.complete(outBuffer, outPosition);
+         *
+         *  // Prints "13 : aGVsbA=="
+         *  System.out.println(outPosition + " : " + new String(outBuffer, 0, 5, outPosition - 5));
+         * 
+ * + * @param target the byte array to write to + * @param pos the position to start writing at + * @return the position after the last byte written + */ + public int complete(byte[] target, int pos) { + if (state > 0) { + target[pos++] = ENCODING_TABLE[last]; + for (int i = state; i < 3; i++) { + target[pos++] = (byte)'='; + } + + last = state = 0; + } + if (wrap) { + target[pos++] = 0x0D; + target[pos++] = 0x0A; + } + + return pos; + } + + private static int complete(char[] target, int pos, int state, int last, boolean wrap) { + if (state > 0) { + target[pos++] = (char) ENCODING_TABLE[last]; + for (int i = state; i < 3; i++) { + target[pos++] = '='; + } + } + if (wrap) { + target[pos++] = 0x0D; + target[pos++] = 0x0A; + } + + return pos; + } + + private static int complete(byte[] target, int pos, int state, int last, boolean wrap) { + if (state > 0) { + target[pos++] = ENCODING_TABLE[last]; + for (int i = state; i < 3; i++) { + target[pos++] = '='; + } + } + if (wrap) { + target[pos++] = 0x0D; + target[pos++] = 0x0A; + } + + return pos; + } + + /** + * Completes an encoding session by writing out the necessary padding. This is essential to complying + * with the Base64 format. This method will write at most 4 or 2 bytes, depending on whether or not wrapping + * is enabled. + * + * @param target the byte buffer to write to + */ + public void complete(ByteBuffer target) { + if (state > 0) { + target.put(ENCODING_TABLE[last]); + for (int i = state; i < 3; i++) { + target.put((byte)'='); + } + + last = state = 0; + } + if (wrap) { + target.putShort((short)0x0D0A); + } + + count = 0; + } + } + + /** + * Controls the decoding process. + */ + public static final class Decoder { + private int state; + private int last; + private int lastPos; + private static final int SKIP = 0x0FD00; + private static final int MARK = 0x0FE00; + private static final int DONE = 0x0FF00; + private static final int ERROR = 0xF0000; + + + + private Decoder() { + } + + + private static int nextByte(ByteBuffer buffer, int state, int last, boolean ignoreErrors) throws IOException { + return nextByte(buffer.get() & 0xFF, state, last, ignoreErrors); + } + + private static int nextByte(Object source, int pos, int state, int last, boolean ignoreErrors) throws IOException { + int c; + if (source instanceof byte[]) { + c = ((byte[])source)[pos] & 0xFF; + } else if (source instanceof String) { + c = ((String)source).charAt(pos) & 0xFF; + } else { + throw new IllegalArgumentException(); + } + + return nextByte(c, state, last, ignoreErrors); + } + + private static int nextByte(int c, int state, int last, boolean ignoreErrors) throws IOException { + if (last == MARK) { + if (c != '=') { + throw new IOException("Expected padding character"); + } + return DONE; + } + if (c == '=') { + if (state == 2) { + return MARK; + } else if (state == 3) { + return DONE; + } else { + throw new IOException("Unexpected padding character"); + } + } + if (c == ' ' || c == '\t' || c == '\r' || c == '\n') { + return SKIP; + } + if (c < 43 || c > 122) { + if (ignoreErrors) { + return ERROR; + } + throw new IOException("Invalid base64 character encountered: " + c); + } + int b = (DECODING_TABLE[c - 43] & 0xFF) - 1; + if (b < 0) { + if (ignoreErrors) { + return ERROR; + } + throw new IOException("Invalid base64 character encountered: " + c); + } + return b; + } + + /** + * Decodes one Base64 byte buffer into another. This method will return and save state + * if the target does not have the required capacity. Subsequent calls with a new target will + * resume reading where it last left off (the source buffer's position). Similarly not all of the + * source data need be available, this method can be repetitively called as data is made available. + * + *

The decoder will skip white space, but will error if it detects corruption.

+ * + * @param source the byte buffer to read encoded data from + * @param target the byte buffer to write decoded data to + * @throws IOException if the encoded data is corrupted + */ + public void decode(ByteBuffer source, ByteBuffer target) throws IOException { + if (target == null) + throw new IllegalStateException(); + + int last = this.last; + int state = this.state; + + int remaining = source.remaining(); + int targetRemaining = target.remaining(); + int b = 0; + while (remaining-- > 0 && targetRemaining > 0) { + b = nextByte(source, state, last, false); + if (b == MARK) { + last = MARK; + if (--remaining <= 0) { + break; + } + b = nextByte(source, state, last, false); + } + if (b == DONE) { + last = state = 0; + break; + } + if (b == SKIP) { + continue; + } + // ( 6 | 2) (4 | 4) (2 | 6) + if (state == 0) { + last = b << 2; + state++; + if (remaining-- <= 0) { + break; + } + b = nextByte(source, state, last, false); + if ((b & 0xF000) != 0) { + source.position(source.position() - 1); + continue; + } + } + if (state == 1) { + target.put((byte)(last | (b >>> 4))); + last = (b & 0x0F) << 4; + state++; + if (remaining-- <= 0 || --targetRemaining <= 0) { + break; + } + b = nextByte(source, state, last, false); + if ((b & 0xF000) != 0) { + source.position(source.position() - 1); + continue; + } + } + if (state == 2) { + target.put((byte) (last | (b >>> 2))); + last = (b & 0x3) << 6; + state++; + if (remaining-- <= 0 || --targetRemaining <= 0) { + break; + } + b = nextByte(source, state, last, false); + if ((b & 0xF000) != 0) { + source.position(source.position() - 1); + continue; + } + } + if (state == 3) { + target.put((byte)(last | b)); + last = state = 0; + targetRemaining--; + } + } + + if (remaining > 0) { + drain(source, b, state, last); + } + + this.last = last; + this.state = state; + this.lastPos = source.position(); + } + + private static void drain(ByteBuffer source, int b, int state, int last) { + while (b != DONE && source.remaining() > 0) { + try { + b = nextByte(source, state, last, true); + } catch (IOException e) { + b = 0; + } + + if (b == MARK) { + last = MARK; + continue; + } + + // Not WS/pad + if ((b & 0xF000) == 0) { + source.position(source.position() - 1); + break; + } + } + + if (b == DONE) { + // SKIP one line of trailing whitespace + while (source.remaining() > 0) { + b = source.get(); + if (b == '\n') { + break; + } else if (b != ' ' && b != '\t' && b != '\r') { + source.position(source.position() - 1); + break; + } + + } + } + } + + private static int drain(Object source, int pos, int limit, int b, int state, int last) { + while (b != DONE && limit > pos) { + try { + b = nextByte(source, pos++, state, last, true); + } catch (IOException e) { + b = 0; + } + + if (b == MARK) { + last = MARK; + continue; + } + + // Not WS/pad + if ((b & 0xF000) == 0) { + pos--; + break; + } + } + + if (b == DONE) { + // SKIP one line of trailing whitespace + while (limit > pos) { + if (source instanceof byte[]) { + b = ((byte[])source)[pos++] & 0xFF; + } else if (source instanceof String) { + b = ((String)source).charAt(pos++) & 0xFF; + } else { + throw new IllegalArgumentException(); + } + + if (b == '\n') { + break; + } else if (b != ' ' && b != '\t' && b != '\r') { + pos--; + } + + + } + + } + + + return pos; + } + + private int decode(Object source, int sourcePos, int sourceLimit, byte[] target, int targetPos, int targetLimit) throws IOException { + if (target == null) + throw new IllegalStateException(); + + int last = this.last; + int state = this.state; + + int pos = sourcePos; + int opos = targetPos; + int limit = sourceLimit; + int olimit = targetLimit; + + int b = 0; + while (limit > pos && olimit > opos) { + b = nextByte(source, pos++, state, last, false); + if (b == MARK) { + last = MARK; + if (pos >= limit) { + break; + } + b = nextByte(source, pos++, state, last, false); + } + if (b == DONE) { + last = state = 0; + break; + } + if (b == SKIP) { + continue; + } + // ( 6 | 2) (4 | 4) (2 | 6) + if (state == 0) { + last = b << 2; + state++; + if (pos >= limit) { + break; + } + b = nextByte(source, pos++, state, last, false); + if ((b & 0xF000) != 0) { + pos--; + continue; + } + } + if (state == 1) { + target[opos++] = ((byte)(last | (b >>> 4))); + last = (b & 0x0F) << 4; + state++; + if (pos >= limit || opos >= olimit) { + break; + } + b = nextByte(source, pos++, state, last, false); + if ((b & 0xF000) != 0) { + pos--; + continue; + } + } + if (state == 2) { + target[opos++] = ((byte) (last | (b >>> 2))); + last = (b & 0x3) << 6; + state++; + if (pos >= limit || opos >= olimit) { + break; + } + b = nextByte(source, pos++, state, last, false); + if ((b & 0xF000) != 0) { + pos--; + continue; + } + } + if (state == 3) { + target[opos++] = ((byte)(last | b)); + last = state = 0; + } + } + + if (limit > pos) { + pos = drain(source, pos, limit, b, state, last); + } + + this.last = last; + this.state = state; + this.lastPos = pos; + return opos; + } + + /** + * Gets the last position where decoding left off in the last byte array that was used for reading. + * If the target for decoded content does not have the necessary capacity, this method should be used to + * determine where to start from on subsequent decode calls. + * + * @return the last known read position + */ + public int getLastInputPosition() { + return lastPos; + } + + + /** + * Decodes one Base64 byte array into another byte array. If the source limit is hit, this method will + * return and save the current state, such that future calls can resume the decoding process. Likewise, + * if the target does not have the capacity, this method will also return and save state for subsequent + * calls to this method. + * + *

When multiple calls are made, {@link #getLastInputPosition()} should be used to determine what value + * should be set for sourcePos. Likewise, the returned target position should be used as the targetPos + * in a subsequent call.

+ * + *

The decoder will skip white space, but will error if it detects corruption.

+ * + * @param source a Base64 encoded string to decode data from + * @param sourcePos the position in the source array to start decoding from + * @param sourceLimit the position in the source array to halt decoding when hit (exclusive) + * @param target the byte buffer to write decoded data to + * @param targetPos the position in the target byte array to begin writing at + * @param targetLimit the position in the target byte array to halt writing (exclusive) + * @throws IOException if the encoded data is corrupted + * @return the position in the target array immediately following the last byte written + * + */ + public int decode(String source, int sourcePos, int sourceLimit, byte[] target, int targetPos, int targetLimit) throws IOException { + return decode((Object)source, sourcePos, sourceLimit, target, targetPos, targetLimit); + } + + /** + * Decodes a Base64 encoded string into the passed byte array. This method will return and save state + * if the target does not have the required capacity. Subsequent calls with a new target will + * resume reading where it last left off (the source buffer's position). Similarly not all of the + * source data need be available, this method can be repetitively called as data is made available. + * + *

Since this method variant assumes a position of 0 and a limit of the item length, + * repeated calls will need fresh source and target values. {@link #decode(String, int, int, byte[], int, int)} + * would be a better fit if you need reuse

+ * + *

The decoder will skip white space, but will error if it detects corruption.

+ * + * @param source a base64 encoded string to decode from + * @param target a byte array to write to + * @throws java.io.IOException if the base64 content is malformed + * @return output position following the last written byte + */ + public int decode(String source, byte[] target) throws IOException { + return decode(source, 0, source.length(), target, 0, target.length); + } + + /** + * Decodes one Base64 byte array into another byte array. If the source limit is hit, this method will + * return and save the current state, such that future calls can resume the decoding process. Likewise, + * if the target does not have the capacity, this method will also return and save state for subsequent + * calls to this method. + * + *

When multiple calls are made, {@link #getLastInputPosition()} should be used to determine what value + * should be set for sourcePos. Likewise, the returned target position should be used as the targetPos + * in a subsequent call.

+ * + *

The decoder will skip white space, but will error if it detects corruption.

+ * + *

+         *  Decoder decoder = FlexBase64.createDecoder();
+         *  byte[] outBuffer = new byte[10];
+         *  byte[] bytes = "aGVsbG8=".getBytes("US-ASCII");
+         *  // Decode only 2 bytes
+         *  int outPosition = decoder.decode(bytes, 0, 8, outBuffer, 5, 7);
+         *  // Resume where we left off and get the rest
+         *  outPosition = decoder.decode(bytes, decoder.getLastInputPosition(), 8, outBuffer, outPosition, 10);
+         *  // Prints "10 : Hello"
+         *  System.out.println(outPosition + " : " + new String(outBuffer, 0, 5, outPosition - 5));
+         * 
+ * + * + * @param source the byte array to read encoded data from + * @param sourcePos the position in the source array to start decoding from + * @param sourceLimit the position in the source array to halt decoding when hit (exclusive) + * @param target the byte buffer to write decoded data to + * @param targetPos the position in the target byte array to begin writing at + * @param targetLimit the position in the target byte array to halt writing (exclusive) + * @throws IOException if the encoded data is corrupted + * @return the position in the target array immediately following the last byte written + */ + public int decode(byte[] source, int sourcePos, int sourceLimit, byte[] target, int targetPos, int targetLimit) throws IOException { + return decode((Object)source, sourcePos, sourceLimit, target, targetPos, targetLimit); + } + + private static ByteBuffer decode(String source) throws IOException { + int remainder = source.length() % 4; + int size = ((source.length() / 4) + (remainder == 0 ? 0 : 4 - remainder)) * 3; + byte[] buffer = new byte[size]; + int actual = createDecoder().decode(source, 0, source.length(), buffer, 0, size); + return ByteBuffer.wrap(buffer, 0, actual); + } + + private static ByteBuffer decode(byte[] source, int off, int limit) throws IOException { + int len = limit - off; + int remainder = len % 4; + int size = ((len / 4) + (remainder == 0 ? 0 : 4 - remainder)) * 3; + byte[] buffer = new byte[size]; + int actual = createDecoder().decode(source, off, limit, buffer, 0, size); + return ByteBuffer.wrap(buffer, 0, actual); + } + + private static ByteBuffer decode(ByteBuffer source) throws IOException { + int len = source.remaining(); + int remainder = len % 4; + int size = ((len / 4) + (remainder == 0 ? 0 : 4 - remainder)) * 3; + ByteBuffer buffer = ByteBuffer.allocate(size); + createDecoder().decode(source, buffer); + buffer.flip(); + return buffer; + } + + } + + /** + * An input stream which decodes bytes as they are read from a stream with Base64 encoded data. + */ + public static class DecoderInputStream extends InputStream { + private final InputStream input; + private final byte[] buffer; + private final Decoder decoder = createDecoder(); + private int pos = 0; + private int limit = 0; + private byte[] one; + + private DecoderInputStream(InputStream input) { + this(input, 8192); + } + + private DecoderInputStream(InputStream input, int bufferSize) { + this.input = input; + buffer = new byte[bufferSize]; + } + + private int fill() throws IOException { + byte[] buffer = this.buffer; + int read = input.read(buffer, 0, buffer.length); + pos = 0; + limit = read; + return read; + } + + /** + * {@inheritDoc} + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + for (;;) { + byte[] source = buffer; + int pos = this.pos; + int limit = this.limit; + boolean setPos = true; + + if (pos >= limit) { + if (len > source.length) { + source = new byte[len]; + limit = input.read(source, 0, len); + pos = 0; + setPos = false; + } else { + limit = fill(); + pos = 0; + } + + if (limit == -1) { + return -1; + } + } + + int requested = len + pos; + limit = limit > requested ? requested : limit; + + int read = decoder.decode(source, pos, limit, b, off, off+len) - off; + if (setPos) { + this.pos = decoder.getLastInputPosition(); + } + + if (read > 0) { + return read; + } + } + } + + + /** + * {@inheritDoc} + */ + @Override + public int read() throws IOException { + byte[] one = this.one; + if (one == null) { + one = this.one = new byte[1]; + } + int read = this.read(one, 0, 1); + return read > 0 ? one[0] & 0xFF : -1; + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + input.close(); + } + } + + /** + * An input stream which encodes bytes as they are read from a stream. + */ + public static class EncoderInputStream extends InputStream { + private final InputStream input; + private final byte[] buffer; + private final byte[] overflow = new byte[6]; + private int overflowPos; + private int overflowLimit; + private final Encoder encoder; + private int pos = 0; + private int limit = 0; + private byte[] one; + private boolean complete; + + private EncoderInputStream(InputStream input) { + this(input, 8192, true); + } + + private EncoderInputStream(InputStream input, int bufferSize, boolean wrap) { + this.input = input; + buffer = new byte[bufferSize]; + this.encoder = new Encoder(wrap); + } + + private int fill() throws IOException { + byte[] buffer = this.buffer; + int read = input.read(buffer, 0, buffer.length); + pos = 0; + limit = read; + return read; + } + + /** + * {@inheritDoc} + */ + @Override + public int read() throws IOException { + byte[] one = this.one; + if (one == null) { + one = this.one = new byte[1]; + } + int read = this.read(one, 0, 1); + return read > 0 ? one[0] & 0xFF : -1; + } + + /** + * {@inheritDoc} + */ + @Override + public int read(byte[] b, int off, int len) throws IOException { + byte[] buffer = this.buffer; + byte[] overflow = this.overflow; + int overflowPos = this.overflowPos; + int overflowLimit = this.overflowLimit; + boolean complete = this.complete; + boolean wrap = encoder.wrap; + + int copy = 0; + if (overflowPos < overflowLimit) { + copy = copyOverflow(b, off, len, overflow, overflowPos, overflowLimit); + if (len <= copy || complete) { + return copy; + } + + len -= copy; + off += copy; + } else if (complete) { + return -1; + } + + for (;;) { + byte[] source = buffer; + int pos = this.pos; + int limit = this.limit; + boolean setPos = true; + + if (pos >= limit) { + if (len > source.length) { + // If requested length exceeds buffer, allocate a new temporary buffer that will be + // one block less than an exact encoded output. This is to handle partial quad carryover + // from an earlier read. + int adjust = (len / 4 * 3) - 3; + if (wrap) { + adjust -= adjust / 76 * 2 + 2; + } + source = new byte[adjust]; + limit = input.read(source, 0, adjust); + pos = 0; + setPos = false; + } else { + limit = fill(); + pos = 0; + } + + if (limit <= 0) { + this.complete = true; + + if (len < (wrap ? 4 : 2)) { + overflowLimit = encoder.complete(overflow, 0); + this.overflowLimit = overflowLimit; + int ret = copyOverflow(b, off, len, overflow, 0, overflowLimit) + copy; + return ret == 0 ? -1 : ret; + } + + int ret = encoder.complete(b, off) - off + copy; + return ret == 0 ? -1 : ret; + } + } + + if (len < (wrap ? 6 : 4)) { + overflowLimit = encoder.encode(source, pos, limit, overflow, 0, overflow.length); + this.overflowLimit = overflowLimit; + this.pos = encoder.getLastInputPosition(); + + return copyOverflow(b, off, len, overflow, 0, overflowLimit) + copy; + } + + int read = encoder.encode(source, pos, limit, b, off, off+len) - off; + if (setPos) { + this.pos = encoder.getLastInputPosition(); + } + + if (read > 0) { + return read + copy; + } + } + } + + private int copyOverflow(byte[] b, int off, int len, byte[] overflow, int pos, int limit) { + limit -= pos; + len = limit <= len ? limit : len; + System.arraycopy(overflow, pos, b, off, len); + this.overflowPos = pos + len; + return len; + } + } + + /** + * An output stream which base64 encodes all passed data and writes it to the wrapped target output stream. + * + *

Closing this stream will result in the correct padding sequence being written. However, as + * required by the OutputStream contract, the wrapped stream will also be closed. If this is not desired, + * the {@link #complete()} method should be used.

+ */ + public static class EncoderOutputStream extends OutputStream { + + private final OutputStream output; + private final byte[] buffer; + private final Encoder encoder; + private int pos = 0; + private byte[] one; + + private EncoderOutputStream(OutputStream output) { + this(output, 8192, true); + } + + private EncoderOutputStream(OutputStream output, int bufferSize, boolean wrap) { + this.output = output; + this.buffer = new byte[bufferSize]; + this.encoder = createEncoder(wrap); + } + + /** + * {@inheritDoc} + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + byte[] buffer = this.buffer; + Encoder encoder = this.encoder; + int pos = this.pos; + int limit = off + len; + int ipos = off; + + while (ipos < limit) { + pos = encoder.encode(b, ipos, limit, buffer, pos, buffer.length); + int last = encoder.getLastInputPosition(); + if (last == ipos || pos >= buffer.length) { + output.write(buffer, 0, pos); + pos = 0; + } + ipos = last; + } + this.pos = pos; + } + + /** + * {@inheritDoc} + */ + @Override + public void write(int b) throws IOException { + byte[] one = this.one; + if (one == null) { + this.one = one = new byte[1]; + } + + one[0] = (byte)b; + write(one, 0, 1); + } + + /** + * {@inheritDoc} + */ + @Override + public void flush() throws IOException { + OutputStream output = this.output; + output.write(buffer, 0, pos); + output.flush(); + } + + /** + * Completes the stream, writing out base64 padding characters if needed. + * + * @throws IOException if the underlying stream throws one + */ + public void complete() throws IOException { + OutputStream output = this.output; + byte[] buffer = this.buffer; + int pos = this.pos; + + boolean completed = false; + if (buffer.length - pos >= (encoder.wrap ? 2 : 4)) { + this.pos = encoder.complete(buffer, pos); + completed = true; + } + + flush(); + + if (!completed) { + int len = encoder.complete(buffer, 0); + output.write(buffer, 0, len); + output.flush(); + } + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + try { + complete(); + } catch (IOException e) { + // eat + } + try { + output.flush(); + } catch (IOException e) { + // eat + } + output.close(); + } + } + + /** + * An output stream which decodes base64 data written to it, and writes the decoded output to the + * wrapped inner stream. + */ + public static class DecoderOutputStream extends OutputStream { + + private final OutputStream output; + private final byte[] buffer; + private final Decoder decoder; + private int pos = 0; + private byte[] one; + + private DecoderOutputStream(OutputStream output) { + this(output, 8192); + } + + private DecoderOutputStream(OutputStream output, int bufferSize) { + this.output = output; + this.buffer = new byte[bufferSize]; + this.decoder = createDecoder(); + } + + /** + * {@inheritDoc} + */ + @Override + public void write(byte[] b, int off, int len) throws IOException { + byte[] buffer = this.buffer; + Decoder decoder = this.decoder; + int pos = this.pos; + int limit = off + len; + int ipos = off; + + while (ipos < limit) { + pos = decoder.decode(b, ipos, limit, buffer, pos, buffer.length); + int last = decoder.getLastInputPosition(); + if (last == ipos || pos >= buffer.length) { + output.write(buffer, 0, pos); + pos = 0; + } + ipos = last; + } + this.pos = pos; + } + + /** + * {@inheritDoc} + */ + @Override + public void write(int b) throws IOException { + byte[] one = this.one; + if (one == null) { + this.one = one = new byte[1]; + } + + one[0] = (byte)b; + write(one, 0, 1); + } + + /** + * {@inheritDoc} + */ + @Override + public void flush() throws IOException { + OutputStream output = this.output; + output.write(buffer, 0, pos); + output.flush(); + } + + /** + * {@inheritDoc} + */ + @Override + public void close() throws IOException { + try { + flush(); + } catch (IOException e) { + // eat + } + try { + output.flush(); + } catch (IOException e) { + // eat + } + output.close(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/HeaderMap.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/HeaderMap.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/HeaderMap.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,865 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.AbstractCollection; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.NoSuchElementException; + +/** + * An optimized array-backed header map. + * + * @author David M. Lloyd + */ +public final class HeaderMap implements Iterable { + + private Object[] table; + private int size; + private Collection headerNames; + + public HeaderMap() { + table = new Object[16]; + } + + private HeaderValues getEntry(final HttpString headerName) { + if (headerName == null) { + return null; + } + final int hc = headerName.hashCode(); + final int idx = hc & (table.length - 1); + final Object o = table[idx]; + if (o == null) { + return null; + } + HeaderValues headerValues; + if (o instanceof HeaderValues) { + headerValues = (HeaderValues) o; + if (! headerName.equals(headerValues.key)) { + return null; + } + return headerValues; + } else { + final HeaderValues[] row = (HeaderValues[]) o; + for (int i = 0; i < row.length; i++) { + headerValues = row[i]; + if (headerValues != null && headerName.equals(headerValues.key)) { return headerValues; } + } + return null; + } + } + + + private HeaderValues getEntry(final String headerName) { + if (headerName == null) { + return null; + } + final int hc = HttpString.hashCodeOf(headerName); + final int idx = hc & (table.length - 1); + final Object o = table[idx]; + if (o == null) { + return null; + } + HeaderValues headerValues; + if (o instanceof HeaderValues) { + headerValues = (HeaderValues) o; + if (! headerValues.key.equalToString(headerName)) { + return null; + } + return headerValues; + } else { + final HeaderValues[] row = (HeaderValues[]) o; + for (int i = 0; i < row.length; i++) { + headerValues = row[i]; + if (headerValues != null && headerValues.key.equalToString(headerName)) { return headerValues; } + } + return null; + } + } + + private HeaderValues removeEntry(final HttpString headerName) { + if (headerName == null) { + return null; + } + final int hc = headerName.hashCode(); + final Object[] table = this.table; + final int idx = hc & (table.length - 1); + final Object o = table[idx]; + if (o == null) { + return null; + } + HeaderValues headerValues; + if (o instanceof HeaderValues) { + headerValues = (HeaderValues) o; + if (! headerName.equals(headerValues.key)) { + return null; + } + table[idx] = null; + size --; + return headerValues; + } else { + final HeaderValues[] row = (HeaderValues[]) o; + for (int i = 0; i < row.length; i++) { + headerValues = row[i]; + if (headerValues != null && headerName.equals(headerValues.key)) { + row[i] = null; + size --; + return headerValues; + } + } + return null; + } + } + + + private HeaderValues removeEntry(final String headerName) { + if (headerName == null) { + return null; + } + final int hc = HttpString.hashCodeOf(headerName); + final Object[] table = this.table; + final int idx = hc & (table.length - 1); + final Object o = table[idx]; + if (o == null) { + return null; + } + HeaderValues headerValues; + if (o instanceof HeaderValues) { + headerValues = (HeaderValues) o; + if (! headerValues.key.equalToString(headerName)) { + return null; + } + table[idx] = null; + size --; + return headerValues; + } else { + final HeaderValues[] row = (HeaderValues[]) o; + for (int i = 0; i < row.length; i++) { + headerValues = row[i]; + if (headerValues != null && headerValues.key.equalToString(headerName)) { + row[i] = null; + size --; + return headerValues; + } + } + return null; + } + } + + private void resize() { + final int oldLen = table.length; + if (oldLen == 0x40000000) { + return; + } + assert Integer.bitCount(oldLen) == 1; + Object[] newTable = Arrays.copyOf(table, oldLen << 1); + table = newTable; + for (int i = 0; i < oldLen; i ++) { + if (newTable[i] == null) { + continue; + } + if (newTable[i] instanceof HeaderValues) { + HeaderValues e = (HeaderValues) newTable[i]; + if ((e.key.hashCode() & oldLen) != 0) { + newTable[i] = null; + newTable[i + oldLen] = e; + } + continue; + } + HeaderValues[] oldRow = (HeaderValues[]) newTable[i]; + HeaderValues[] newRow = oldRow.clone(); + int rowLen = oldRow.length; + newTable[i + oldLen] = newRow; + HeaderValues item; + for (int j = 0; j < rowLen; j ++) { + item = oldRow[j]; + if (item != null) { + if ((item.key.hashCode() & oldLen) != 0) { + oldRow[j] = null; + } else { + newRow[j] = null; + } + } + } + } + } + + private HeaderValues getOrCreateEntry(final HttpString headerName) { + if (headerName == null) { + return null; + } + final int hc = headerName.hashCode(); + final Object[] table = this.table; + final int length = table.length; + final int idx = hc & (length - 1); + final Object o = table[idx]; + HeaderValues headerValues; + if (o == null) { + if (size >= length >> 1) { + resize(); + return getOrCreateEntry(headerName); + } + headerValues = new HeaderValues(headerName); + table[idx] = headerValues; + size++; + return headerValues; + } + return getOrCreateNonEmpty(headerName, table, length, idx, o); + } + + private HeaderValues getOrCreateNonEmpty(HttpString headerName, Object[] table, int length, int idx, Object o) { + HeaderValues headerValues; + if (o instanceof HeaderValues) { + headerValues = (HeaderValues) o; + if (! headerName.equals(headerValues.key)) { + if (size >= length >> 1) { + resize(); + return getOrCreateEntry(headerName); + } + size++; + final HeaderValues[] row = { headerValues, new HeaderValues(headerName), null, null }; + table[idx] = row; + return row[1]; + } + return headerValues; + } else { + final HeaderValues[] row = (HeaderValues[]) o; + int empty = -1; + for (int i = 0; i < row.length; i++) { + headerValues = row[i]; + if (headerValues != null) { + if (headerName.equals(headerValues.key)) { return headerValues; } + } else if (empty == -1) { + empty = i; + } + } + if (size >= length >> 1) { + resize(); + return getOrCreateEntry(headerName); + } + size++; + headerValues = new HeaderValues(headerName); + if (empty != -1) { + row[empty] = headerValues; + } else { + if (row.length >= 16) { + throw new SecurityException("Excessive collisions"); + } + final HeaderValues[] newRow = Arrays.copyOf(row, row.length + 3); + newRow[row.length] = headerValues; + table[idx] = newRow; + } + return headerValues; + } + } + + // get + + public HeaderValues get(final HttpString headerName) { + return getEntry(headerName); + } + + public HeaderValues get(final String headerName) { + return getEntry(headerName); + } + + public String getFirst(HttpString headerName) { + HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) return null; + return headerValues.getFirst(); + } + + public String getFirst(String headerName) { + HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) return null; + return headerValues.getFirst(); + } + + public String get(HttpString headerName, int index) throws IndexOutOfBoundsException { + if (headerName == null) { + return null; + } + final HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) { + return null; + } + return headerValues.get(index); + } + + public String get(String headerName, int index) throws IndexOutOfBoundsException { + if (headerName == null) { + return null; + } + final HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) { + return null; + } + return headerValues.get(index); + } + + public String getLast(HttpString headerName) { + if (headerName == null) { + return null; + } + HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) return null; + return headerValues.getLast(); + } + + public String getLast(String headerName) { + if (headerName == null) { + return null; + } + HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) return null; + return headerValues.getLast(); + } + + // count + + public int count(HttpString headerName) { + if (headerName == null) { + return 0; + } + final HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) { + return 0; + } + return headerValues.size(); + } + + public int count(String headerName) { + if (headerName == null) { + return 0; + } + final HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) { + return 0; + } + return headerValues.size(); + } + + public int size() { + return size; + } + + // iterate + + /** + * Do a fast iteration of this header map without creating any objects. + * + * @return an opaque iterating cookie, or -1 if no iteration is possible + * + * @see #fiNext(long) + * @see #fiCurrent(long) + */ + public long fastIterate() { + final Object[] table = this.table; + final int len = table.length; + int ri = 0; + int ci; + while (ri < len) { + final Object item = table[ri]; + if (item != null) { + if (item instanceof HeaderValues) { + return (long)ri << 32L; + } else { + final HeaderValues[] row = (HeaderValues[]) item; + ci = 0; + final int rowLen = row.length; + while (ci < rowLen) { + if (row[ci] != null) { + return (long)ri << 32L | (ci & 0xffffffffL); + } + ci ++; + } + } + } + ri++; + } + return -1L; + } + + /** + * Do a fast iteration of this header map without creating any objects, only considering non-empty header values. + * + * @return an opaque iterating cookie, or -1 if no iteration is possible + */ + public long fastIterateNonEmpty() { + final Object[] table = this.table; + final int len = table.length; + int ri = 0; + int ci; + while (ri < len) { + final Object item = table[ri]; + if (item != null) { + if (item instanceof HeaderValues && !((HeaderValues) item).isEmpty()) { + return (long)ri << 32L; + } else { + final HeaderValues[] row = (HeaderValues[]) item; + ci = 0; + final int rowLen = row.length; + while (ci < rowLen) { + if (row[ci] != null && !row[ci].isEmpty()) { + return (long)ri << 32L | (ci & 0xffffffffL); + } + ci ++; + } + } + } + ri++; + } + return -1L; + } + + /** + * Find the next index in a fast iteration. + * + * @param cookie the previous cookie value + * @return the next cookie value, or -1L if iteration is done + */ + public long fiNext(long cookie) { + if (cookie == -1L) return -1L; + final Object[] table = this.table; + final int len = table.length; + int ri = (int) (cookie >> 32); + int ci = (int) cookie; + Object item = table[ri]; + if (item instanceof HeaderValues[]) { + final HeaderValues[] row = (HeaderValues[]) item; + final int rowLen = row.length; + if (++ci >= rowLen) { + ri ++; ci = 0; + } else if (row[ci] != null) { + return (long)ri << 32L | (ci & 0xffffffffL); + } + } else { + ri ++; ci = 0; + } + while (ri < len) { + item = table[ri]; + if (item instanceof HeaderValues) { + return (long)ri << 32L; + } else if (item instanceof HeaderValues[]) { + final HeaderValues[] row = (HeaderValues[]) item; + final int rowLen = row.length; + while (ci < rowLen) { + if (row[ci] != null) { + return (long)ri << 32L | (ci & 0xffffffffL); + } + ci ++; + } + } + ci = 0; + ri ++; + } + return -1L; + } + + /** + * Find the next non-empty index in a fast iteration. + * + * @param cookie the previous cookie value + * @return the next cookie value, or -1L if iteration is done + */ + public long fiNextNonEmpty(long cookie) { + if (cookie == -1L) return -1L; + final Object[] table = this.table; + final int len = table.length; + int ri = (int) (cookie >> 32); + int ci = (int) cookie; + Object item = table[ri]; + if (item instanceof HeaderValues[]) { + final HeaderValues[] row = (HeaderValues[]) item; + final int rowLen = row.length; + if (++ci >= rowLen) { + ri ++; ci = 0; + } else if (row[ci] != null && !row[ci].isEmpty()) { + return (long)ri << 32L | (ci & 0xffffffffL); + } + } else { + ri ++; ci = 0; + } + while (ri < len) { + item = table[ri]; + if (item instanceof HeaderValues && !((HeaderValues) item).isEmpty()) { + return (long)ri << 32L; + } else if (item instanceof HeaderValues[]) { + final HeaderValues[] row = (HeaderValues[]) item; + final int rowLen = row.length; + while (ci < rowLen) { + if (row[ci] != null && !row[ci].isEmpty()) { + return (long)ri << 32L | (ci & 0xffffffffL); + } + ci ++; + } + } + ci = 0; + ri ++; + } + return -1L; + } + + /** + * Return the value at the current index in a fast iteration. + * + * @param cookie the iteration cookie value + * @return the values object at this position + * @throws NoSuchElementException if the cookie value is invalid + */ + public HeaderValues fiCurrent(long cookie) { + try { + final Object[] table = this.table; + int ri = (int) (cookie >> 32); + int ci = (int) cookie; + final Object item = table[ri]; + if (item instanceof HeaderValues[]) { + return ((HeaderValues[])item)[ci]; + } else if (ci == 0) { + return (HeaderValues) item; + } else { + throw new NoSuchElementException(); + } + } catch (RuntimeException e) { + throw new NoSuchElementException(); + } + } + + public Iterable eachValue(final HttpString headerName) { + if (headerName == null) { + return Collections.emptyList(); + } + final HeaderValues entry = getEntry(headerName); + if (entry == null) { + return Collections.emptyList(); + } + return entry; + } + + public Iterator iterator() { + return new Iterator() { + final Object[] table = HeaderMap.this.table; + boolean consumed; + int ri, ci; + + private HeaderValues _next() { + for (;;) { + if (ri >= table.length) { + return null; + } + final Object o = table[ri]; + if (o == null) { + // zero-entry row + ri++; + ci = 0; + consumed = false; + continue; + } + if (o instanceof HeaderValues) { + // one-entry row + if (ci > 0 || consumed) { + ri++; + ci = 0; + consumed = false; + continue; + } + return (HeaderValues) o; + } + final HeaderValues[] row = (HeaderValues[]) o; + final int len = row.length; + if (ci >= len) { + ri ++; + ci = 0; + consumed = false; + continue; + } + if (consumed) { + ci++; + consumed = false; + continue; + } + final HeaderValues headerValues = row[ci]; + if (headerValues == null) { + ci ++; + continue; + } + return headerValues; + } + } + + public boolean hasNext() { + return _next() != null; + } + + public HeaderValues next() { + final HeaderValues next = _next(); + if (next == null) { + throw new NoSuchElementException(); + } + consumed = true; + return next; + } + + public void remove() { + } + }; + } + + public Collection getHeaderNames() { + if (headerNames != null) { + return headerNames; + } + return headerNames = new AbstractCollection() { + public boolean contains(final Object o) { + return o instanceof HttpString && getEntry((HttpString) o) != null; + } + + public boolean add(final HttpString httpString) { + getOrCreateEntry(httpString); + return true; + } + + public boolean remove(final Object o) { + if (! (o instanceof HttpString)) return false; + HttpString s = (HttpString) o; + HeaderValues entry = getEntry(s); + if (entry == null) { + return false; + } + entry.clear(); + return true; + } + + public void clear() { + HeaderMap.this.clear(); + } + + public Iterator iterator() { + final Iterator iterator = HeaderMap.this.iterator(); + return new Iterator() { + public boolean hasNext() { + return iterator.hasNext(); + } + + public HttpString next() { + return iterator.next().getHeaderName(); + } + + public void remove() { + iterator.remove(); + } + }; + } + + public int size() { + return HeaderMap.this.size(); + } + }; + } + + // add + + public HeaderMap add(HttpString headerName, String headerValue) { + addLast(headerName, headerValue); + return this; + } + + public HeaderMap addFirst(final HttpString headerName, final String headerValue) { + if (headerName == null) { + throw new IllegalArgumentException("headerName is null"); + } + if (headerValue == null) { + return this; + } + getOrCreateEntry(headerName).addFirst(headerValue); + return this; + } + + public HeaderMap addLast(final HttpString headerName, final String headerValue) { + if (headerName == null) { + throw new IllegalArgumentException("headerName is null"); + } + if (headerValue == null) { + return this; + } + getOrCreateEntry(headerName).addLast(headerValue); + return this; + } + + public HeaderMap add(HttpString headerName, long headerValue) { + add(headerName, Long.toString(headerValue)); + return this; + } + + + public HeaderMap addAll(HttpString headerName, Collection headerValues) { + if (headerName == null) { + throw new IllegalArgumentException("headerName is null"); + } + if (headerValues == null || headerValues.isEmpty()) { + return this; + } + getOrCreateEntry(headerName).addAll(headerValues); + return this; + } + + // put + + public HeaderMap put(HttpString headerName, String headerValue) { + if (headerName == null) { + throw new IllegalArgumentException("headerName is null"); + } + if (headerValue == null) { + remove(headerName); + return this; + } + final HeaderValues headerValues = getOrCreateEntry(headerName); + headerValues.clear(); + headerValues.add(headerValue); + return this; + } + + public HeaderMap put(HttpString headerName, long headerValue) { + if (headerName == null) { + throw new IllegalArgumentException("headerName is null"); + } + final HeaderValues entry = getOrCreateEntry(headerName); + entry.clear(); + entry.add(Long.toString(headerValue)); + return this; + } + + public HeaderMap putAll(HttpString headerName, Collection headerValues) { + if (headerName == null) { + throw new IllegalArgumentException("headerName is null"); + } + if (headerValues == null || headerValues.isEmpty()) { + remove(headerName); + } + final HeaderValues entry = getOrCreateEntry(headerName); + entry.clear(); + entry.addAll(headerValues); + return this; + } + + // clear + + public HeaderMap clear() { + Arrays.fill(table, null); + size = 0; + return this; + } + + // remove + + public Collection remove(HttpString headerName) { + if (headerName == null) { + return Collections.emptyList(); + } + final Collection values = removeEntry(headerName); + return values != null ? values : Collections.emptyList(); + } + + public Collection remove(String headerName) { + if (headerName == null) { + return Collections.emptyList(); + } + final Collection values = removeEntry(headerName); + return values != null ? values : Collections.emptyList(); + } + + // contains + + public boolean contains(HttpString headerName) { + final HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) { + return false; + } + final Object v = headerValues.value; + if (v instanceof String) { + return true; + } + final String[] list = (String[]) v; + for (int i = 0; i < list.length; i++) { + if (list[i] != null) { + return true; + } + } + return false; + } + + public boolean contains(String headerName) { + final HeaderValues headerValues = getEntry(headerName); + if (headerValues == null) { + return false; + } + final Object v = headerValues.value; + if (v instanceof String) { + return true; + } + final String[] list = (String[]) v; + for (int i = 0; i < list.length; i++) { + if (list[i] != null) { + return true; + } + } + return false; + } + + // compare + + @Override + public boolean equals(final Object o) { + return o == this; + } + + @Override + public int hashCode() { + return super.hashCode(); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder("{"); + boolean first = true; + for(HttpString name : getHeaderNames()) { + if(first) { + first = false; + } else { + sb.append(", "); + } + sb.append(name); + sb.append("=["); + boolean f = true; + for(String val : get(name)) { + if(f) { + f = false; + } else { + sb.append(", "); + } + sb.append(val); + } + sb.append("]"); + } + sb.append("}"); + return sb.toString(); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/HeaderToken.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/HeaderToken.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/HeaderToken.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * Representation of a token allowed within a header. + * + * @author Darran Lofthouse + */ +public interface HeaderToken { + + /** + * @return The name of the token as seen within the HTTP header. + */ + String getName(); + + /** + * @return true if this header could be a quoted header. + */ + boolean isAllowQuoted(); + + /* + * Additional items could be added and incorporated into the parsing checks: - + * boolean isMandatory(); + * boolean + * isEnumeration(); + * String[] getAllowedValues(); + */ + +} Index: 3rdParty_sources/undertow/io/undertow/util/HeaderTokenParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/HeaderTokenParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/HeaderTokenParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,118 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import static io.undertow.UndertowMessages.MESSAGES; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Utility to parse the tokens contained within a HTTP header. + * + * @author Darran Lofthouse + */ +public class HeaderTokenParser { + + private static final char EQUALS = '='; + private static final char COMMA = ','; + private static final char QUOTE = '"'; + + private final Map expectedTokens; + + public HeaderTokenParser(final Map expectedTokens) { + this.expectedTokens = expectedTokens; + } + + public Map parseHeader(final String header) { + char[] headerChars = header.toCharArray(); + + // The LinkedHashMap is used so that the parameter order can also be retained. + Map response = new LinkedHashMap<>(); + + SearchingFor searchingFor = SearchingFor.START_OF_NAME; + int nameStart = 0; + E currentToken = null; + int valueStart = 0; + + for (int i = 0; i < headerChars.length; i++) { + switch (searchingFor) { + case START_OF_NAME: + // Eliminate any white space before the name of the parameter. + if (headerChars[i] != COMMA && !Character.isWhitespace(headerChars[i])) { + nameStart = i; + searchingFor = SearchingFor.EQUALS_SIGN; + } + break; + case EQUALS_SIGN: + if (headerChars[i] == EQUALS) { + String paramName = String.valueOf(headerChars, nameStart, i - nameStart); + currentToken = expectedTokens.get(paramName); + if (currentToken == null) { + throw MESSAGES.unexpectedTokenInHeader(paramName); + } + searchingFor = SearchingFor.START_OF_VALUE; + } + break; + case START_OF_VALUE: + if (!Character.isWhitespace(headerChars[i])) { + if (headerChars[i] == QUOTE && currentToken.isAllowQuoted()) { + valueStart = i + 1; + searchingFor = SearchingFor.LAST_QUOTE; + } else { + valueStart = i; + searchingFor = SearchingFor.END_OF_VALUE; + } + } + break; + case LAST_QUOTE: + if (headerChars[i] == QUOTE) { + String value = String.valueOf(headerChars, valueStart, i - valueStart); + response.put(currentToken, value); + + searchingFor = SearchingFor.START_OF_NAME; + } + break; + case END_OF_VALUE: + if (headerChars[i] == COMMA || Character.isWhitespace(headerChars[i])) { + String value = String.valueOf(headerChars, valueStart, i - valueStart); + response.put(currentToken, value); + + searchingFor = SearchingFor.START_OF_NAME; + } + break; + } + } + + if (searchingFor == SearchingFor.END_OF_VALUE) { + // Special case where we reached the end of the array containing the header values. + String value = String.valueOf(headerChars, valueStart, headerChars.length - valueStart); + response.put(currentToken, value); + } else if (searchingFor != SearchingFor.START_OF_NAME) { + // Somehow we are still in the middle of searching for a current value. + throw MESSAGES.invalidHeader(); + } + + return response; + } + + enum SearchingFor { + START_OF_NAME, EQUALS_SIGN, START_OF_VALUE, LAST_QUOTE, END_OF_VALUE; + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/HeaderValues.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/HeaderValues.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/HeaderValues.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,579 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.AbstractCollection; +import java.util.Arrays; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; +import java.util.List; +import java.util.ListIterator; +import java.util.NoSuchElementException; +import java.util.RandomAccess; + +/** + * An array-backed list/deque for header string values. + * + * @author David M. Lloyd + */ +public final class HeaderValues extends AbstractCollection implements Deque, List, RandomAccess { + + private static final String[] NO_STRINGS = new String[0]; + final HttpString key; + byte size; + Object value; + + HeaderValues(final HttpString key) { + this.key = key; + } + + public HttpString getHeaderName() { + return key; + } + + public int size() { + return size; + } + + public boolean isEmpty() { + return size == 0; + } + + public void clear() { + final byte size = this.size; + if (size == 0) return; + clearInternal(size); + } + + private void clearInternal(byte size) { + final Object value = this.value; + if (value instanceof String[]) { + final String[] strings = (String[]) value; + final int len = strings.length; + Arrays.fill(strings, 0, len, null); + } else { + this.value = null; + } + this.size = 0; + } + + private int index(int idx) { + assert idx >= 0; + assert idx < size; + final int len = ((String[]) value).length; + if (idx > len) { + idx -= len; + } + return idx; + } + + public ListIterator listIterator() { + return iterator(0, true); + } + + public ListIterator listIterator(final int index) { + return iterator(index, true); + } + + public Iterator iterator() { + return iterator(0, true); + } + + public Iterator descendingIterator() { + return iterator(0, false); + } + + private ListIterator iterator(final int start, final boolean forwards) { + return new ListIterator() { + int idx = start; + int returned = -1; + + public boolean hasNext() { + return idx < size; + } + + public boolean hasPrevious() { + return idx > 0; + } + + public String next() { + try { + final String next; + if (forwards) { + int idx = this.idx; + next = get(idx); + returned = idx; + this.idx = idx + 1; + return next; + } else { + int idx = this.idx - 1; + next = get(idx); + this.idx = returned = idx; + } + return next; + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + + public int nextIndex() { + return idx; + } + + public String previous() { + try { + final String prev; + if (forwards) { + int idx = this.idx - 1; + prev = get(idx); + this.idx = returned = idx; + } else { + int idx = this.idx; + prev = get(idx); + returned = idx; + this.idx = idx + 1; + return prev; + } + return prev; + } catch (IndexOutOfBoundsException e) { + throw new NoSuchElementException(); + } + } + + public int previousIndex() { + return idx - 1; + } + + public void remove() { + if (returned == -1) { + throw new IllegalStateException(); + } + HeaderValues.this.remove(returned); + returned = -1; + } + + public void set(final String headerValue) { + if (returned == -1) { + throw new IllegalStateException(); + } + HeaderValues.this.set(returned, headerValue); + } + + public void add(final String headerValue) { + if (returned == -1) { + throw new IllegalStateException(); + } + final int idx = this.idx; + HeaderValues.this.add(idx, headerValue); + this.idx = idx + 1; + returned = -1; + } + }; + } + + public boolean offerFirst(final String headerValue) { + int size = this.size; + if (headerValue == null || size == Byte.MAX_VALUE) return false; + final Object value = this.value; + if (value instanceof String[]) { + final String[] strings = (String[]) value; + final int len = strings.length; + if (size == len) { + final String[] newStrings = new String[len + 2]; + System.arraycopy(strings, 0, newStrings, 1, len); + newStrings[0] = headerValue; + this.value = newStrings; + } else { + System.arraycopy(strings, 0, strings, 1, strings.length - 1); + strings[0] = headerValue; + } + this.size = (byte) (size + 1); + } else { + if (size == 0) { + this.value = headerValue; + this.size = (byte) 1; + } else { + this.value = new String[] { headerValue, (String) value, null, null }; + this.size = (byte) 2; + } + } + return true; + } + + public boolean offerLast(final String headerValue) { + int size = this.size; + if (headerValue == null || size == Byte.MAX_VALUE) return false; + final Object value = this.value; + if (value instanceof String[]) { + offerLastMultiValue(headerValue, size, (String[]) value); + } else { + if (size == 0) { + this.value = headerValue; + this.size = (byte) 1; + } else { + this.value = new String[] { (String) value, headerValue, null, null }; + this.size = (byte) 2; + } + } + return true; + } + + private void offerLastMultiValue(String headerValue, int size, String[] value) { + final String[] strings = value; + final int len = strings.length; + if (size == len) { + final String[] newStrings = new String[len + 2]; + System.arraycopy(strings, 0, newStrings, 0, len); + newStrings[len] = headerValue; + this.value = newStrings; + } else { + strings[size] = headerValue; + } + this.size = (byte) (size + 1); + } + + private boolean offer(int idx, final String headerValue) { + int size = this.size; + if (idx < 0 || idx > size || size == Byte.MAX_VALUE || headerValue == null) return false; + if (idx == 0) return offerFirst(headerValue); + if (idx == size) return offerLast(headerValue); + assert size >= 2; // must be >= 2 to pass the last two checks + final Object value = this.value; + assert value instanceof String[]; + final String[] strings = (String[]) value; + final int len = strings.length; + // This stuff is all algebraically derived. + if (size == len) { + // Grow the list, copy each segment into new spots so that head = 0 + final int newLen = len + 2; + final String[] newStrings = new String[newLen]; + System.arraycopy(value, 0, newStrings, 0, idx); + System.arraycopy(value, idx, newStrings, idx + 1, len - idx); + + // finally fill in the new value + newStrings[idx] = headerValue; + this.value = newStrings; + } else{ + System.arraycopy(value, idx, value, idx + 1, len - idx); + + // finally fill in the new value + strings[idx] = headerValue; + } + this.size = (byte) (size + 1); + return true; + } + + public String pollFirst() { + final byte size = this.size; + if (size == 0) return null; + + final Object value = this.value; + if (value instanceof String) { + this.size = 0; + this.value = null; + return (String) value; + } else { + final String[] strings = (String[]) value; + String ret = strings[0]; + System.arraycopy(strings, 1, strings, 0, strings.length - 1); + this.size = (byte) (size - 1); + return ret; + } + } + + public String pollLast() { + final byte size = this.size; + if (size == 0) return null; + + final Object value = this.value; + if (value instanceof String) { + this.size = 0; + this.value = null; + return (String) value; + } else { + final String[] strings = (String[]) value; + int idx = (this.size = (byte) (size - 1)); + final int len = strings.length; + if (idx > len) idx -= len; + try { + return strings[idx]; + } finally { + strings[idx] = null; + } + } + } + + public String remove(int idx) { + final int size = this.size; + if (idx < 0 || idx >= size) throw new IndexOutOfBoundsException(); + if (idx == 0) return removeFirst(); + if (idx == size - 1) return removeLast(); + assert size > 2; // must be > 2 to pass the last two checks + // value must be an array since size > 2 + final String[] value = (String[]) this.value; + final int len = value.length; + String ret = value[idx]; + System.arraycopy(value, idx + 1, value, idx, len - idx - 1); + value[len - 1] = null; + this.size = (byte) (size - 1); + return ret; + } + + public String get(int idx) { + if (idx > size) { + throw new IndexOutOfBoundsException(); + } + Object value = this.value; + assert value != null; + if (value instanceof String) { + assert size == 1; + return (String) value; + } + final String[] a = (String[]) value; + return a[index(idx)]; + } + + public int indexOf(final Object o) { + if (o == null || size == 0) return -1; + if (value instanceof String[]) { + final String[] list = (String[]) value; + final int len = list.length; + for (int i = 0; i < size; i ++) { + if ((i > len ? list[i - len] : list[i]).equals(o)) { + return i; + } + } + } else if (o.equals(value)) { + return 0; + } + return -1; + } + + public int lastIndexOf(final Object o) { + if (o == null || size == 0) return -1; + if (value instanceof String[]) { + final String[] list = (String[]) value; + final int len = list.length; + int idx; + for (int i = size - 1; i >= 0; i --) { + idx = i; + if ((idx > len ? list[idx - len] : list[idx]).equals(o)) { + return i; + } + } + } else if (o.equals(value)) { + return 0; + } + return -1; + } + + public String set(final int index, final String element) { + if (element == null) throw new IllegalArgumentException(); + + final byte size = this.size; + if (index < 0 || index >= size) throw new IndexOutOfBoundsException(); + + final Object value = this.value; + if (size == 1 && value instanceof String) try { + return (String) value; + } finally { + this.value = element; + } else { + final String[] list = (String[]) value; + final int i = index(index); + try { + return list[i]; + } finally { + list[i] = element; + } + } + } + + public boolean addAll(int index, final Collection c) { + final int size = this.size; + if (index < 0 || index > size) throw new IndexOutOfBoundsException(); + final Iterator iterator = c.iterator(); + boolean result = false; + while (iterator.hasNext()) { result |= offer(index, iterator.next()); } + return result; + } + + public List subList(final int fromIndex, final int toIndex) { + // todo - this is about 75% correct, by spec... + if (fromIndex < 0 || toIndex > size || fromIndex > toIndex) throw new IndexOutOfBoundsException(); + final int len = toIndex - fromIndex; + final String[] strings = new String[len]; + for (int i = 0; i < len; i ++) { + strings[i] = get(i + fromIndex); + } + return Arrays.asList(strings); + } + + public String[] toArray() { + int size = this.size; + if (size == 0) { return NO_STRINGS; } + final Object v = this.value; + if (v instanceof String) return new String[] { (String) v }; + final String[] list = (String[]) v; + final int len = list.length; + final int copyEnd = size; + if (copyEnd < len) { + return Arrays.copyOfRange(list, 0, copyEnd); + } else { + String[] ret = Arrays.copyOfRange(list, 0, copyEnd); + System.arraycopy(list, 0, ret, len, copyEnd - len); + return ret; + } + } + + public T[] toArray(final T[] a) { + int size = this.size; + if (size == 0) return a; + final int inLen = a.length; + final Object[] target = inLen < size ? Arrays.copyOfRange(a, inLen, inLen + size) : a; + final Object v = this.value; + if (v instanceof String) { + target[0] = (T)v; + } else { + System.arraycopy(v, 0, target, 0, size); + } + return (T[]) target; + } + + //====================================== + // + // Derived methods + // + //====================================== + + public void addFirst(final String s) { + if (s == null) return; + if (! offerFirst(s)) throw new IllegalStateException(); + } + + public void addLast(final String s) { + if (s == null) return; + if (! offerLast(s)) throw new IllegalStateException(); + } + + public void add(final int index, final String s) { + if (s == null) return; + if (! offer(index, s)) throw new IllegalStateException(); + } + + public boolean contains(final Object o) { + return indexOf(o) != -1; + } + + public String peekFirst() { + return size == 0 ? null : get(0); + } + + public String peekLast() { + return size == 0 ? null : get(size - 1); + } + + public boolean removeFirstOccurrence(final Object o) { + int i = indexOf(o); + return i != -1 && remove(i) != null; + } + + public boolean removeLastOccurrence(final Object o) { + int i = lastIndexOf(o); + return i != -1 && remove(i) != null; + } + + public boolean add(final String s) { + addLast(s); + return true; + } + + public void push(final String s) { + addFirst(s); + } + + public String pop() { + return removeFirst(); + } + + public boolean offer(final String s) { + return offerLast(s); + } + + public String poll() { + return pollFirst(); + } + + public String peek() { + return peekFirst(); + } + + public String remove() { + return removeFirst(); + } + + public String removeFirst() { + final String s = pollFirst(); + if (s == null) { + throw new NoSuchElementException(); + } + return s; + } + + public String removeLast() { + final String s = pollLast(); + if (s == null) { + throw new NoSuchElementException(); + } + return s; + } + + public String getFirst() { + final String s = peekFirst(); + if (s == null) { + throw new NoSuchElementException(); + } + return s; + } + + public String getLast() { + final String s = peekLast(); + if (s == null) { + throw new NoSuchElementException(); + } + return s; + } + + public String element() { + return getFirst(); + } + + public boolean remove(Object obj) { + return removeFirstOccurrence(obj); + } + + public boolean addAll(final Collection c) { + Iterator it = c.iterator(); + while (it.hasNext()) { + add(it.next()); + } + return !c.isEmpty(); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/Headers.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Headers.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Headers.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,301 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * NOTE: if you add a new header here you must also add it to {@link io.undertow.server.protocol.http.HttpRequestParser} + * + * @author David M. Lloyd + */ +public final class Headers { + + private Headers() { + } + + // Headers as strings + + public static final String ACCEPT_STRING = "Accept"; + public static final String ACCEPT_CHARSET_STRING = "Accept-Charset"; + public static final String ACCEPT_ENCODING_STRING = "Accept-Encoding"; + public static final String ACCEPT_LANGUAGE_STRING = "Accept-Language"; + public static final String ACCEPT_RANGES_STRING = "Accept-Ranges"; + public static final String AGE_STRING = "Age"; + public static final String ALLOW_STRING = "Allow"; + public static final String AUTHENTICATION_INFO_STRING = "Authentication-Info"; + public static final String AUTHORIZATION_STRING = "Authorization"; + public static final String CACHE_CONTROL_STRING = "Cache-Control"; + public static final String COOKIE_STRING = "Cookie"; + public static final String COOKIE2_STRING = "Cookie2"; + public static final String CONNECTION_STRING = "Connection"; + public static final String CONTENT_DISPOSITION_STRING = "Content-Disposition"; + public static final String CONTENT_ENCODING_STRING = "Content-Encoding"; + public static final String CONTENT_LANGUAGE_STRING = "Content-Language"; + public static final String CONTENT_LENGTH_STRING = "Content-Length"; + public static final String CONTENT_LOCATION_STRING = "Content-Location"; + public static final String CONTENT_MD5_STRING = "Content-MD5"; + public static final String CONTENT_RANGE_STRING = "Content-Range"; + public static final String CONTENT_TYPE_STRING = "Content-Type"; + public static final String DATE_STRING = "Date"; + public static final String ETAG_STRING = "ETag"; + public static final String EXPECT_STRING = "Expect"; + public static final String EXPIRES_STRING = "Expires"; + public static final String FROM_STRING = "From"; + public static final String HOST_STRING = "Host"; + public static final String IF_MATCH_STRING = "If-Match"; + public static final String IF_MODIFIED_SINCE_STRING = "If-Modified-Since"; + public static final String IF_NONE_MATCH_STRING = "If-None-Match"; + public static final String IF_RANGE_STRING = "If-Range"; + public static final String IF_UNMODIFIED_SINCE_STRING = "If-Unmodified-Since"; + public static final String LAST_MODIFIED_STRING = "Last-Modified"; + public static final String LOCATION_STRING = "Location"; + public static final String MAX_FORWARDS_STRING = "Max-Forwards"; + public static final String ORIGIN_STRING = "Origin"; + public static final String PRAGMA_STRING = "Pragma"; + public static final String PROXY_AUTHENTICATE_STRING = "Proxy-Authenticate"; + public static final String PROXY_AUTHORIZATION_STRING = "Proxy-Authorization"; + public static final String RANGE_STRING = "Range"; + public static final String REFERER_STRING = "Referer"; + public static final String REFRESH_STRING = "Refresh"; + public static final String RETRY_AFTER_STRING = "Retry-After"; + public static final String SEC_WEB_SOCKET_ACCEPT_STRING = "Sec-WebSocket-Accept"; + public static final String SEC_WEB_SOCKET_EXTENSIONS_STRING = "Sec-WebSocket-Extensions"; + public static final String SEC_WEB_SOCKET_KEY_STRING = "Sec-WebSocket-Key"; + public static final String SEC_WEB_SOCKET_KEY1_STRING = "Sec-WebSocket-Key1"; + public static final String SEC_WEB_SOCKET_KEY2_STRING = "Sec-WebSocket-Key2"; + public static final String SEC_WEB_SOCKET_LOCATION_STRING = "Sec-WebSocket-Location"; + public static final String SEC_WEB_SOCKET_ORIGIN_STRING = "Sec-WebSocket-Origin"; + public static final String SEC_WEB_SOCKET_PROTOCOL_STRING = "Sec-WebSocket-Protocol"; + public static final String SEC_WEB_SOCKET_VERSION_STRING = "Sec-WebSocket-Version"; + public static final String SERVER_STRING = "Server"; + public static final String SERVLET_ENGINE_STRING = "Servlet-Engine"; + public static final String SET_COOKIE_STRING = "Set-Cookie"; + public static final String SET_COOKIE2_STRING = "Set-Cookie2"; + public static final String SSL_CLIENT_CERT_STRING = "SSL_CLIENT_CERT"; + public static final String SSL_CIPHER_STRING = "SSL_CIPHER"; + public static final String SSL_SESSION_ID_STRING = "SSL_SESSION_ID"; + public static final String SSL_CIPHER_USEKEYSIZE_STRING = "SSL_CIPHER_USEKEYSIZE"; + public static final String STATUS_STRING = "Status"; + public static final String STRICT_TRANSPORT_SECURITY_STRING = "Strict-Transport-Security"; + public static final String TE_STRING = "TE"; + public static final String TRAILER_STRING = "Trailer"; + public static final String TRANSFER_ENCODING_STRING = "Transfer-Encoding"; + public static final String UPGRADE_STRING = "Upgrade"; + public static final String USER_AGENT_STRING = "User-Agent"; + public static final String VARY_STRING = "Vary"; + public static final String VIA_STRING = "Via"; + public static final String WARNING_STRING = "Warning"; + public static final String WWW_AUTHENTICATE_STRING = "WWW-Authenticate"; + public static final String X_FORWARDED_FOR_STRING = "X-Forwarded-For"; + public static final String X_FORWARDED_PROTO_STRING = "X-Forwarded-Proto"; + public static final String X_FORWARDED_HOST_STRING = "X-Forwarded-Host"; + public static final String X_FORWARDED_PORT_STRING = "X-Forwarded-Port"; + + // Header names + + public static final HttpString ACCEPT = new HttpString(ACCEPT_STRING, 1); + public static final HttpString ACCEPT_CHARSET = new HttpString(ACCEPT_CHARSET_STRING, 2); + public static final HttpString ACCEPT_ENCODING = new HttpString(ACCEPT_ENCODING_STRING, 3); + public static final HttpString ACCEPT_LANGUAGE = new HttpString(ACCEPT_LANGUAGE_STRING, 4); + public static final HttpString ACCEPT_RANGES = new HttpString(ACCEPT_RANGES_STRING, 5); + public static final HttpString AGE = new HttpString(AGE_STRING, 6); + public static final HttpString ALLOW = new HttpString(ALLOW_STRING, 7); + public static final HttpString AUTHENTICATION_INFO = new HttpString(AUTHENTICATION_INFO_STRING, 8); + public static final HttpString AUTHORIZATION = new HttpString(AUTHORIZATION_STRING, 9); + public static final HttpString CACHE_CONTROL = new HttpString(CACHE_CONTROL_STRING, 10); + public static final HttpString CONNECTION = new HttpString(CONNECTION_STRING, 11); + public static final HttpString CONTENT_DISPOSITION = new HttpString(CONTENT_DISPOSITION_STRING, 12); + public static final HttpString CONTENT_ENCODING = new HttpString(CONTENT_ENCODING_STRING, 13); + public static final HttpString CONTENT_LANGUAGE = new HttpString(CONTENT_LANGUAGE_STRING, 14); + public static final HttpString CONTENT_LENGTH = new HttpString(CONTENT_LENGTH_STRING, 15); + public static final HttpString CONTENT_LOCATION = new HttpString(CONTENT_LOCATION_STRING, 16); + public static final HttpString CONTENT_MD5 = new HttpString(CONTENT_MD5_STRING,17); + public static final HttpString CONTENT_RANGE = new HttpString(CONTENT_RANGE_STRING, 18); + public static final HttpString CONTENT_TYPE = new HttpString(CONTENT_TYPE_STRING, 19); + public static final HttpString COOKIE = new HttpString(COOKIE_STRING, 20); + public static final HttpString COOKIE2 = new HttpString(COOKIE2_STRING, 21); + public static final HttpString DATE = new HttpString(DATE_STRING, 22); + public static final HttpString ETAG = new HttpString(ETAG_STRING, 23); + public static final HttpString EXPECT = new HttpString(EXPECT_STRING, 24); + public static final HttpString EXPIRES = new HttpString(EXPIRES_STRING, 25); + public static final HttpString FROM = new HttpString(FROM_STRING, 26); + public static final HttpString HOST = new HttpString(HOST_STRING, 27); + public static final HttpString IF_MATCH = new HttpString(IF_MATCH_STRING, 28); + public static final HttpString IF_MODIFIED_SINCE = new HttpString(IF_MODIFIED_SINCE_STRING, 29); + public static final HttpString IF_NONE_MATCH = new HttpString(IF_NONE_MATCH_STRING, 30); + public static final HttpString IF_RANGE = new HttpString(IF_RANGE_STRING, 31); + public static final HttpString IF_UNMODIFIED_SINCE = new HttpString(IF_UNMODIFIED_SINCE_STRING, 32); + public static final HttpString LAST_MODIFIED = new HttpString(LAST_MODIFIED_STRING, 33); + public static final HttpString LOCATION = new HttpString(LOCATION_STRING, 34); + public static final HttpString MAX_FORWARDS = new HttpString(MAX_FORWARDS_STRING, 35); + public static final HttpString ORIGIN = new HttpString(ORIGIN_STRING, 36); + public static final HttpString PRAGMA = new HttpString(PRAGMA_STRING, 37); + public static final HttpString PROXY_AUTHENTICATE = new HttpString(PROXY_AUTHENTICATE_STRING, 38); + public static final HttpString PROXY_AUTHORIZATION = new HttpString(PROXY_AUTHORIZATION_STRING, 39); + public static final HttpString RANGE = new HttpString(RANGE_STRING, 40); + public static final HttpString REFERER = new HttpString(REFERER_STRING, 41); + public static final HttpString REFRESH = new HttpString(REFRESH_STRING, 42); + public static final HttpString RETRY_AFTER = new HttpString(RETRY_AFTER_STRING, 43); + public static final HttpString SEC_WEB_SOCKET_ACCEPT = new HttpString(SEC_WEB_SOCKET_ACCEPT_STRING, 44); + public static final HttpString SEC_WEB_SOCKET_EXTENSIONS = new HttpString(SEC_WEB_SOCKET_EXTENSIONS_STRING); + public static final HttpString SEC_WEB_SOCKET_KEY = new HttpString(SEC_WEB_SOCKET_KEY_STRING, 45); + public static final HttpString SEC_WEB_SOCKET_KEY1 = new HttpString(SEC_WEB_SOCKET_KEY1_STRING, 46); + public static final HttpString SEC_WEB_SOCKET_KEY2 = new HttpString(SEC_WEB_SOCKET_KEY2_STRING, 47); + public static final HttpString SEC_WEB_SOCKET_LOCATION = new HttpString(SEC_WEB_SOCKET_LOCATION_STRING, 48); + public static final HttpString SEC_WEB_SOCKET_ORIGIN = new HttpString(SEC_WEB_SOCKET_ORIGIN_STRING, 49); + public static final HttpString SEC_WEB_SOCKET_PROTOCOL = new HttpString(SEC_WEB_SOCKET_PROTOCOL_STRING, 50); + public static final HttpString SEC_WEB_SOCKET_VERSION = new HttpString(SEC_WEB_SOCKET_VERSION_STRING, 51); + public static final HttpString SERVER = new HttpString(SERVER_STRING, 52); + public static final HttpString SERVLET_ENGINE = new HttpString(SERVLET_ENGINE_STRING, 53); + public static final HttpString SET_COOKIE = new HttpString(SET_COOKIE_STRING, 54); + public static final HttpString SET_COOKIE2 = new HttpString(SET_COOKIE2_STRING, 55); + public static final HttpString SSL_CLIENT_CERT = new HttpString(SSL_CLIENT_CERT_STRING); + public static final HttpString SSL_CIPHER = new HttpString(SSL_CIPHER_STRING); + public static final HttpString SSL_SESSION_ID = new HttpString(SSL_SESSION_ID_STRING); + public static final HttpString SSL_CIPHER_USEKEYSIZE = new HttpString(SSL_CIPHER_USEKEYSIZE_STRING); + public static final HttpString STATUS = new HttpString(STATUS_STRING, 56); + public static final HttpString STRICT_TRANSPORT_SECURITY = new HttpString(STRICT_TRANSPORT_SECURITY_STRING, 57); + public static final HttpString TE = new HttpString(TE_STRING, 58); + public static final HttpString TRAILER = new HttpString(TRAILER_STRING, 59); + public static final HttpString TRANSFER_ENCODING = new HttpString(TRANSFER_ENCODING_STRING, 60); + public static final HttpString UPGRADE = new HttpString(UPGRADE_STRING, 61); + public static final HttpString USER_AGENT = new HttpString(USER_AGENT_STRING, 62); + public static final HttpString VARY = new HttpString(VARY_STRING, 63); + public static final HttpString VIA = new HttpString(VIA_STRING, 64); + public static final HttpString WARNING = new HttpString(WARNING_STRING, 65); + public static final HttpString WWW_AUTHENTICATE = new HttpString(WWW_AUTHENTICATE_STRING, 66); + public static final HttpString X_FORWARDED_FOR = new HttpString(X_FORWARDED_FOR_STRING, 67); + public static final HttpString X_FORWARDED_HOST = new HttpString(X_FORWARDED_HOST_STRING, 68); + public static final HttpString X_FORWARDED_PORT = new HttpString(X_FORWARDED_PORT_STRING, 69); + public static final HttpString X_FORWARDED_PROTO = new HttpString(X_FORWARDED_PROTO_STRING, 70); + + // Content codings + + public static final HttpString COMPRESS = new HttpString("compress"); + public static final HttpString X_COMPRESS = new HttpString("x-compress"); + public static final HttpString DEFLATE = new HttpString("deflate"); + public static final HttpString IDENTITY = new HttpString("identity"); + public static final HttpString GZIP = new HttpString("gzip"); + public static final HttpString X_GZIP = new HttpString("x-gzip"); + + // Transfer codings + + public static final HttpString CHUNKED = new HttpString("chunked"); + // IDENTITY + // GZIP + // COMPRESS + // DEFLATE + + // Connection values + public static final HttpString KEEP_ALIVE = new HttpString("keep-alive"); + public static final HttpString CLOSE = new HttpString("close"); + + //MIME header used in multipart file uploads + public static final String CONTENT_TRANSFER_ENCODING_STRING = "Content-Transfer-Encoding"; + public static final HttpString CONTENT_TRANSFER_ENCODING = new HttpString(CONTENT_TRANSFER_ENCODING_STRING); + + // Authentication Schemes + public static final HttpString BASIC = new HttpString("Basic"); + public static final HttpString DIGEST = new HttpString("Digest"); + public static final HttpString NEGOTIATE = new HttpString("Negotiate"); + + // Digest authentication Token Names + public static final HttpString ALGORITHM = new HttpString("algorithm"); + public static final HttpString AUTH_PARAM = new HttpString("auth-param"); + public static final HttpString CNONCE = new HttpString("cnonce"); + public static final HttpString DOMAIN = new HttpString("domain"); + public static final HttpString NEXT_NONCE = new HttpString("nextnonce"); + public static final HttpString NONCE = new HttpString("nonce"); + public static final HttpString NONCE_COUNT = new HttpString("nc"); + public static final HttpString OPAQUE = new HttpString("opaque"); + public static final HttpString QOP = new HttpString("qop"); + public static final HttpString REALM = new HttpString("realm"); + public static final HttpString RESPONSE = new HttpString("response"); + public static final HttpString RESPONSE_AUTH = new HttpString("rspauth"); + public static final HttpString STALE = new HttpString("stale"); + public static final HttpString URI = new HttpString("uri"); + public static final HttpString USERNAME = new HttpString("username"); + + + + /** + * Extracts a token from a header that has a given key. For instance if the header is + *

+ * content-type=multipart/form-data boundary=myboundary + * and the key is boundary the myboundary will be returned. + * + * @param header The header + * @param key The key that identifies the token to extract + * @return The token, or null if it was not found + */ + public static String extractTokenFromHeader(final String header, final String key) { + int pos = header.indexOf(key + '='); + if (pos == -1) { + return null; + } + int end; + int start = pos + key.length() + 1; + for (end = start; end < header.length(); ++end) { + char c = header.charAt(end); + if (c == ' ' || c == '\t') { + break; + } + } + return header.substring(start, end); + } + + /** + * Extracts a quoted value from a header that has a given key. For instance if the header is + *

+ * content-disposition=form-data; name="my field" + * and the key is name then "my field" will be returned without the quotes. + * + * @param header The header + * @param key The key that identifies the token to extract + * @return The token, or null if it was not found + */ + public static String extractQuotedValueFromHeader(final String header, final String key) { + int pos = header.indexOf(key + '='); + if (pos == -1) { + return null; + } + + int end; + int start = pos + key.length() + 1; + if (header.charAt(start) == '"') { + start++; + for (end = start; end < header.length(); ++end) { + char c = header.charAt(end); + if (c == '"') { + break; + } + } + return header.substring(start, end); + + } else { + //no quotes + for (end = start; end < header.length(); ++end) { + char c = header.charAt(end); + if (c == ' ' || c == '\t') { + break; + } + } + return header.substring(start, end); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/HexConverter.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/HexConverter.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/HexConverter.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,145 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * A utility class for mapping between byte arrays and their hex representation and back again. + * + * @author Darran Lofthouse + */ +public class HexConverter { + + private static final char[] HEX_CHARS = new char[] + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + private static final byte[] HEX_BYTES = new byte[] + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + /** + * Take the supplied byte array and convert it to a hex encoded String. + * + * @param toBeConverted - the bytes to be converted. + * @return the hex encoded String. + */ + public static String convertToHexString(byte[] toBeConverted) { + if (toBeConverted == null) { + throw new NullPointerException("Parameter to be converted can not be null"); + } + + char[] converted = new char[toBeConverted.length * 2]; + for (int i = 0; i < toBeConverted.length; i++) { + byte b = toBeConverted[i]; + converted[i * 2] = HEX_CHARS[b >> 4 & 0x0F]; + converted[i * 2 + 1] = HEX_CHARS[b & 0x0F]; + } + + return String.valueOf(converted); + } + + /** + * Take the supplied byte array and convert it to to a byte array of the encoded + * hex values. + *

+ * Each byte on the incoming array will be converted to two bytes on the return + * array. + * + * @param toBeConverted - the bytes to be encoded. + * @return the encoded byte array. + */ + public static byte[] convertToHexBytes(byte[] toBeConverted) { + if (toBeConverted == null) { + throw new NullPointerException("Parameter to be converted can not be null"); + } + + byte[] converted = new byte[toBeConverted.length * 2]; + for (int i = 0; i < toBeConverted.length; i++) { + byte b = toBeConverted[i]; + converted[i * 2] = HEX_BYTES[b >> 4 & 0x0F]; + converted[i * 2 + 1] = HEX_BYTES[b & 0x0F]; + } + + return converted; + } + + /** + * Take the incoming character of hex encoded data and convert to the raw byte values. + *

+ * The characters in the incoming array are processed in pairs with two chars of a pair + * being converted to a single byte. + * + * @param toConvert - the hex encoded String to convert. + * @return the raw byte array. + */ + public static byte[] convertFromHex(final char[] toConvert) { + if (toConvert.length % 2 != 0) { + throw new IllegalArgumentException("The supplied character array must contain an even number of hex chars."); + } + + byte[] response = new byte[toConvert.length / 2]; + + for (int i = 0; i < response.length; i++) { + int posOne = i * 2; + response[i] = (byte)(toByte(toConvert, posOne) << 4 | toByte(toConvert, posOne+1)); + } + + return response; + } + + private static byte toByte(final char[] toConvert, final int pos) { + int response = Character.digit(toConvert[pos], 16); + if (response < 0 || response > 15) { + throw new IllegalArgumentException("Non-hex character '" + toConvert[pos] + "' at index=" + pos); + } + + return (byte) response; + } + + /** + * Take the incoming String of hex encoded data and convert to the raw byte values. + *

+ * The characters in the incoming String are processed in pairs with two chars of a pair + * being converted to a single byte. + * + * @param toConvert - the hex encoded String to convert. + * @return the raw byte array. + */ + public static byte[] convertFromHex(final String toConvert) { + return convertFromHex(toConvert.toCharArray()); + } + + public static void main(String[] args) { + byte[] toConvert = new byte[256]; + for (int i = 0; i < toConvert.length; i++) { + toConvert[i] = (byte) i; + } + + String hexValue = convertToHexString(toConvert); + + System.out.println("Converted - " + hexValue); + + byte[] convertedBack = convertFromHex(hexValue); + + StringBuilder sb = new StringBuilder(); + for (byte current : convertedBack) { + sb.append((int)current).append(" "); + } + System.out.println("Converted Back " + sb.toString()); + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/HttpString.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/HttpString.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/HttpString.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,355 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.OutputStream; +import java.io.Serializable; +import java.lang.reflect.Field; +import java.nio.ByteBuffer; +import java.util.Random; + +import static java.lang.Integer.signum; +import static java.lang.System.arraycopy; +import static java.util.Arrays.copyOfRange; + +/** + * An HTTP case-insensitive Latin-1 string. + * + * @author David M. Lloyd + */ +public final class HttpString implements Comparable, Serializable { + private final byte[] bytes; + private final transient int hashCode; + /** + * And integer that is only set for well known header to make + * comparison fast + */ + private final int orderInt; + private transient String string; + + private static final Field hashCodeField; + private static final int hashCodeBase; + + static { + try { + hashCodeField = HttpString.class.getDeclaredField("hashCode"); + } catch (NoSuchFieldException e) { + throw new NoSuchFieldError(e.getMessage()); + } + hashCodeBase = new Random().nextInt(); + } + + /** + * Empty HttpString instance. + */ + public static final HttpString EMPTY = new HttpString(""); + + /** + * Construct a new instance. + * + * @param bytes the byte array to copy + */ + public HttpString(final byte[] bytes) { + this(bytes.clone(), null); + } + + /** + * Construct a new instance. + * + * @param bytes the byte array to copy + * @param offset the offset into the array to start copying + * @param length the number of bytes to copy + */ + public HttpString(final byte[] bytes, int offset, int length) { + this(copyOfRange(bytes, offset, offset + length), null); + } + + /** + * Construct a new instance by reading the remaining bytes from a buffer. + * + * @param buffer the buffer to read + */ + public HttpString(final ByteBuffer buffer) { + this(take(buffer), null); + } + + /** + * Construct a new instance from a {@code String}. The {@code String} will be used + * as the cached {@code toString()} value for this {@code HttpString}. + * + * @param string the source string + */ + public HttpString(final String string) { + this(string, 0); + } + + HttpString(final String string, int orderInt) { + this.orderInt = orderInt; + final int len = string.length(); + final byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) { + char c = string.charAt(i); + if (c > 0xff) { + throw new IllegalArgumentException("Invalid string contents"); + } + bytes[i] = (byte) c; + } + this.bytes = bytes; + this.hashCode = calcHashCode(bytes); + this.string = string; + } + + private HttpString(final byte[] bytes, final String string) { + this.bytes = bytes; + this.hashCode = calcHashCode(bytes); + this.string = string; + this.orderInt = 0; + } + + /** + * Attempt to convert a {@code String} to an {@code HttpString}. If the string cannot be converted, + * {@code null} is returned. + * + * @param string the string to try + * @return the HTTP string, or {@code null} if the string is not in a compatible encoding + */ + public static HttpString tryFromString(String string) { + final int len = string.length(); + final byte[] bytes = new byte[len]; + for (int i = 0; i < len; i++) { + char c = string.charAt(i); + if (c > 0xff) { + return null; + } + bytes[i] = (byte) c; + } + return new HttpString(bytes, string); + } + + /** + * Get the string length. + * + * @return the string length + */ + public int length() { + return bytes.length; + } + + /** + * Get the byte at an index. + * + * @return the byte at an index + */ + public byte byteAt(int idx) { + return bytes[idx]; + } + + /** + * Copy {@code len} bytes from this string at offset {@code srcOffs} to the given array at the given offset. + * + * @param srcOffs the source offset + * @param dst the destination + * @param offs the destination offset + * @param len the number of bytes to copy + */ + public void copyTo(int srcOffs, byte[] dst, int offs, int len) { + arraycopy(bytes, srcOffs, dst, offs, len); + } + + /** + * Copy {@code len} bytes from this string to the given array at the given offset. + * + * @param dst the destination + * @param offs the destination offset + * @param len the number of bytes + */ + public void copyTo(byte[] dst, int offs, int len) { + copyTo(0, dst, offs, len); + } + + /** + * Copy all the bytes from this string to the given array at the given offset. + * + * @param dst the destination + * @param offs the destination offset + */ + public void copyTo(byte[] dst, int offs) { + copyTo(dst, offs, bytes.length); + } + + /** + * Append to a byte buffer. + * + * @param buffer the buffer to append to + */ + public void appendTo(ByteBuffer buffer) { + buffer.put(bytes); + } + + /** + * Append to an output stream. + * + * @param output the stream to write to + * @throws IOException if an error occurs + */ + public void writeTo(OutputStream output) throws IOException { + output.write(bytes); + } + + private static byte[] take(final ByteBuffer buffer) { + if (buffer.hasArray()) { + // avoid useless array clearing + try { + return copyOfRange(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + } finally { + buffer.position(buffer.limit()); + } + } else { + final byte[] bytes = new byte[buffer.remaining()]; + buffer.get(bytes); + return bytes; + } + } + + /** + * Compare this string to another in a case-insensitive manner. + * + * @param other the other string + * @return -1, 0, or 1 + */ + public int compareTo(final HttpString other) { + if(orderInt != 0 && other.orderInt != 0) { + return signum(orderInt - other.orderInt); + } + final int len = Math.min(bytes.length, other.bytes.length); + int res; + for (int i = 0; i < len; i++) { + res = signum(higher(bytes[i]) - higher(other.bytes[i])); + if (res != 0) return res; + } + // shorter strings sort higher + return signum(bytes.length - other.bytes.length); + } + + /** + * Get the hash code. + * + * @return the hash code + */ + @Override + public int hashCode() { + return hashCode; + } + + /** + * Determine if this {@code HttpString} is equal to another. + * + * @param other the other object + * @return {@code true} if they are equal, {@code false} otherwise + */ + @Override + public boolean equals(final Object other) { + return other == this || other instanceof HttpString && equals((HttpString) other); + } + + /** + * Determine if this {@code HttpString} is equal to another. + * + * @param other the other object + * @return {@code true} if they are equal, {@code false} otherwise + */ + public boolean equals(final HttpString other) { + return other == this || other != null && bytesAreEqual(bytes, other.bytes); + } + + private static int calcHashCode(final byte[] bytes) { + int hc = 17; + for (byte b : bytes) { + hc = (hc << 4) + hc + higher(b); + } + return hc; + } + + private static int higher(byte b) { + return b & (b >= 'a' && b <= 'z' ? 0xDF : 0xFF); + } + + private static boolean bytesAreEqual(final byte[] a, final byte[] b) { + return a.length == b.length && bytesAreEquivalent(a, b); + } + + private static boolean bytesAreEquivalent(final byte[] a, final byte[] b) { + assert a.length == b.length; + final int len = a.length; + for (int i = 0; i < len; i++) { + if (higher(a[i]) != higher(b[i])) { + return false; + } + } + return true; + } + + /** + * Get the {@code String} representation of this {@code HttpString}. + * + * @return the string + */ + @Override + @SuppressWarnings("deprecation") + public String toString() { + if (string == null) { + string = new String(bytes, 0); + } + return string; + } + + private void readObject(ObjectInputStream ois) throws ClassNotFoundException, IOException { + ois.defaultReadObject(); + try { + hashCodeField.setInt(this, calcHashCode(bytes)); + } catch (IllegalAccessException e) { + throw new IllegalAccessError(e.getMessage()); + } + } + + static int hashCodeOf(String headerName) { + int hc = 17; + + for (int i = 0; i < headerName.length(); ++i) { + hc = (hc << 4) + hc + higher((byte) headerName.charAt(i)); + } + return hc; + } + + public boolean equalToString(String headerName) { + if(headerName.length() != bytes.length) { + return false; + } + + final int len = bytes.length; + for (int i = 0; i < len; i++) { + if (higher(bytes[i]) != higher((byte)headerName.charAt(i))) { + return false; + } + } + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ImmediateAuthenticationMechanismFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ImmediateAuthenticationMechanismFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ImmediateAuthenticationMechanismFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.security.api.AuthenticationMechanism; +import io.undertow.security.api.AuthenticationMechanismFactory; +import io.undertow.server.handlers.form.FormParserFactory; + +import java.util.Map; + +/** + * {@link AuthenticationMechanismFactory} that simply returns a pre configured {@link AuthenticationMechanism} + * @author Stuart Douglas + */ +public class ImmediateAuthenticationMechanismFactory implements AuthenticationMechanismFactory { + + private final AuthenticationMechanism authenticationMechanism; + + public ImmediateAuthenticationMechanismFactory(AuthenticationMechanism authenticationMechanism) { + this.authenticationMechanism = authenticationMechanism; + } + + @Override + public AuthenticationMechanism create(String mechanismName, FormParserFactory formParserFactory, Map properties) { + return authenticationMechanism; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ImmediateConduitFactory.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ImmediateConduitFactory.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ImmediateConduitFactory.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import org.xnio.conduits.Conduit; + +/** + * @author Stuart Douglas + */ +public class ImmediateConduitFactory implements ConduitFactory { + + private final T value; + + public ImmediateConduitFactory(final T value) { + this.value = value; + } + + @Override + public T create() { + return value; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ImmediatePooled.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ImmediatePooled.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ImmediatePooled.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,52 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import org.xnio.Pooled; + +/** + * Wrapper that allows you to use a non-pooed item as a pooled value + * + * @author Stuart Douglas + */ +public class ImmediatePooled implements Pooled { + + private final T value; + + public ImmediatePooled(T value) { + this.value = value; + } + + @Override + public void discard() { + } + + @Override + public void free() { + } + + @Override + public T getResource() throws IllegalStateException { + return value; + } + + @Override + public void close() { + } +} Index: 3rdParty_sources/undertow/io/undertow/util/LocaleUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/LocaleUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/LocaleUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,86 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +/** + * Utility methods for getting the locale from a request. + * + * @author Stuart Douglas + */ +public class LocaleUtils { + + public static Locale getLocaleFromString(String localeString) { + if (localeString == null) { + return null; + } + final String[] parts = localeString.split("-"); + if (parts.length == 1) { + return new Locale(localeString, ""); + } else if (parts.length == 2) { + return new Locale(parts[0], parts[1]); + } else { + return new Locale(parts[0], parts[1], parts[2]); + } + } + + /** + * Parse a header string and return the list of locales that were found. + * + * If the header is empty or null then an empty list will be returned. + * + * @param acceptLanguage The Accept-Language header + * @return The list of locales, in order of preference + */ + public static List getLocalesFromHeader(final String acceptLanguage) { + if(acceptLanguage == null) { + return Collections.emptyList(); + } + return getLocalesFromHeader(Collections.singletonList(acceptLanguage)); + } + + /** + * Parse a header string and return the list of locales that were found. + * + * If the header is empty or null then an empty list will be returned. + * + * @param acceptLanguage The Accept-Language header + * @return The list of locales, in order of preference + */ + public static List getLocalesFromHeader(final List acceptLanguage) { + if (acceptLanguage == null || acceptLanguage.isEmpty()) { + return Collections.emptyList(); + } + final List ret = new ArrayList<>(); + final List> parsedResults = QValueParser.parse(acceptLanguage); + for (List qvalueResult : parsedResults) { + for (QValueParser.QValueResult res : qvalueResult) { + if (!res.isQValueZero()) { + Locale e = LocaleUtils.getLocaleFromString(res.getValue()); + ret.add(e); + } + } + } + return ret; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/MalformedMessageException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/MalformedMessageException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/MalformedMessageException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,30 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.IOException; + +/** + * Exception that is thrown when multipart parsing cannot parse a request + * +* @author Stuart Douglas +*/ +public class MalformedMessageException extends IOException { + +} Index: 3rdParty_sources/undertow/io/undertow/util/Methods.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Methods.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Methods.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,92 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * + * NOTE: If you add a new method here you must also add it to {@link io.undertow.server.protocol.http.HttpRequestParser} + * + * @author David M. Lloyd + */ +public final class Methods { + + private Methods() { + } + + public static final String OPTIONS_STRING = "OPTIONS"; + public static final String GET_STRING = "GET"; + public static final String HEAD_STRING = "HEAD"; + public static final String POST_STRING = "POST"; + public static final String PUT_STRING = "PUT"; + public static final String DELETE_STRING = "DELETE"; + public static final String TRACE_STRING = "TRACE"; + public static final String CONNECT_STRING = "CONNECT"; + public static final String PROPFIND_STRING = "PROPFIND"; + public static final String PROPPATCH_STRING = "PROPPATCH"; + public static final String MKCOL_STRING = "MKCOL"; + public static final String COPY_STRING = "COPY"; + public static final String MOVE_STRING = "MOVE"; + public static final String LOCK_STRING = "LOCK"; + public static final String UNLOCK_STRING = "UNLOCK"; + public static final String ACL_STRING = "ACL"; + public static final String REPORT_STRING = "REPORT"; + public static final String VERSION_CONTROL_STRING = "VERSION-CONTROL"; + public static final String CHECKIN_STRING = "CHECKIN"; + public static final String CHECKOUT_STRING = "CHECKOUT"; + public static final String UNCHECKOUT_STRING = "UNCHECKOUT"; + public static final String SEARCH_STRING = "SEARCH"; + public static final String MKWORKSPACE_STRING = "MKWORKSPACE"; + public static final String UPDATE_STRING = "UPDATE"; + public static final String LABEL_STRING = "LABEL"; + public static final String MERGE_STRING = "MERGE"; + public static final String BASELINE_CONTROL_STRING = "BASELINE_CONTROL"; + public static final String MKACTIVITY_STRING = "MKACTIVITY"; + + + public static final HttpString OPTIONS = new HttpString(OPTIONS_STRING); + public static final HttpString GET = new HttpString(GET_STRING); + public static final HttpString HEAD = new HttpString(HEAD_STRING); + public static final HttpString POST = new HttpString(POST_STRING); + public static final HttpString PUT = new HttpString(PUT_STRING); + public static final HttpString DELETE = new HttpString(DELETE_STRING); + public static final HttpString TRACE = new HttpString(TRACE_STRING); + public static final HttpString CONNECT = new HttpString(CONNECT_STRING); + public static final HttpString PROPFIND =new HttpString(PROPFIND_STRING); + public static final HttpString PROPPATCH =new HttpString(PROPPATCH_STRING); + public static final HttpString MKCOL =new HttpString(MKCOL_STRING); + public static final HttpString COPY =new HttpString(COPY_STRING); + public static final HttpString MOVE =new HttpString(MOVE_STRING); + public static final HttpString LOCK =new HttpString(LOCK_STRING); + public static final HttpString UNLOCK =new HttpString(UNLOCK_STRING); + public static final HttpString ACL =new HttpString(ACL_STRING); + public static final HttpString REPORT =new HttpString(REPORT_STRING); + public static final HttpString VERSION_CONTROL =new HttpString(VERSION_CONTROL_STRING); + public static final HttpString CHECKIN =new HttpString(CHECKIN_STRING); + public static final HttpString CHECKOUT =new HttpString(CHECKOUT_STRING); + public static final HttpString UNCHECKOUT =new HttpString(UNCHECKOUT_STRING); + public static final HttpString SEARCH =new HttpString(SEARCH_STRING); + public static final HttpString MKWORKSPACE =new HttpString(MKWORKSPACE_STRING); + public static final HttpString UPDATE =new HttpString(UPDATE_STRING); + public static final HttpString LABEL =new HttpString(LABEL_STRING); + public static final HttpString MERGE =new HttpString(MERGE_STRING); + public static final HttpString BASELINE_CONTROL =new HttpString(BASELINE_CONTROL_STRING); + public static final HttpString MKACTIVITY =new HttpString(MKACTIVITY_STRING); + + +} Index: 3rdParty_sources/undertow/io/undertow/util/MimeMappings.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/MimeMappings.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/MimeMappings.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,180 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * + * @author Stuart Douglas + */ +public class MimeMappings { + + + private final Map mappings; + + public static final Map DEFAULT_MIME_MAPPINGS; + + static { + Map defaultMappings = new HashMap<>(101); + defaultMappings.put("txt", "text/plain"); + defaultMappings.put("css", "text/css"); + defaultMappings.put("html", "text/html"); + defaultMappings.put("htm", "text/html"); + defaultMappings.put("gif", "image/gif"); + defaultMappings.put("jpg", "image/jpeg"); + defaultMappings.put("jpe", "image/jpeg"); + defaultMappings.put("jpeg", "image/jpeg"); + defaultMappings.put("js", "application/javascript"); + defaultMappings.put("png", "image/png"); + defaultMappings.put("java", "text/plain"); + defaultMappings.put("body", "text/html"); + defaultMappings.put("rtx", "text/richtext"); + defaultMappings.put("tsv", "text/tab-separated-values"); + defaultMappings.put("etx", "text/x-setext"); + defaultMappings.put("json", "application/json"); + defaultMappings.put("ps", "application/x-postscript"); + defaultMappings.put("class", "application/java"); + defaultMappings.put("csh", "application/x-csh"); + defaultMappings.put("sh", "application/x-sh"); + defaultMappings.put("tcl", "application/x-tcl"); + defaultMappings.put("tex", "application/x-tex"); + defaultMappings.put("texinfo", "application/x-texinfo"); + defaultMappings.put("texi", "application/x-texinfo"); + defaultMappings.put("t", "application/x-troff"); + defaultMappings.put("tr", "application/x-troff"); + defaultMappings.put("roff", "application/x-troff"); + defaultMappings.put("man", "application/x-troff-man"); + defaultMappings.put("me", "application/x-troff-me"); + defaultMappings.put("ms", "application/x-wais-source"); + defaultMappings.put("src", "application/x-wais-source"); + defaultMappings.put("zip", "application/zip"); + defaultMappings.put("bcpio", "application/x-bcpio"); + defaultMappings.put("cpio", "application/x-cpio"); + defaultMappings.put("gtar", "application/x-gtar"); + defaultMappings.put("shar", "application/x-shar"); + defaultMappings.put("sv4cpio", "application/x-sv4cpio"); + defaultMappings.put("sv4crc", "application/x-sv4crc"); + defaultMappings.put("tar", "application/x-tar"); + defaultMappings.put("ustar", "application/x-ustar"); + defaultMappings.put("dvi", "application/x-dvi"); + defaultMappings.put("hdf", "application/x-hdf"); + defaultMappings.put("latex", "application/x-latex"); + defaultMappings.put("bin", "application/octet-stream"); + defaultMappings.put("oda", "application/oda"); + defaultMappings.put("pdf", "application/pdf"); + defaultMappings.put("ps", "application/postscript"); + defaultMappings.put("eps", "application/postscript"); + defaultMappings.put("ai", "application/postscript"); + defaultMappings.put("rtf", "application/rtf"); + defaultMappings.put("nc", "application/x-netcdf"); + defaultMappings.put("cdf", "application/x-netcdf"); + defaultMappings.put("cer", "application/x-x509-ca-cert"); + defaultMappings.put("exe", "application/octet-stream"); + defaultMappings.put("gz", "application/x-gzip"); + defaultMappings.put("Z", "application/x-compress"); + defaultMappings.put("z", "application/x-compress"); + defaultMappings.put("hqx", "application/mac-binhex40"); + defaultMappings.put("mif", "application/x-mif"); + defaultMappings.put("ief", "image/ief"); + defaultMappings.put("tiff", "image/tiff"); + defaultMappings.put("tif", "image/tiff"); + defaultMappings.put("ras", "image/x-cmu-raster"); + defaultMappings.put("pnm", "image/x-portable-anymap"); + defaultMappings.put("pbm", "image/x-portable-bitmap"); + defaultMappings.put("pgm", "image/x-portable-graymap"); + defaultMappings.put("ppm", "image/x-portable-pixmap"); + defaultMappings.put("rgb", "image/x-rgb"); + defaultMappings.put("xbm", "image/x-xbitmap"); + defaultMappings.put("xpm", "image/x-xpixmap"); + defaultMappings.put("xwd", "image/x-xwindowdump"); + defaultMappings.put("au", "audio/basic"); + defaultMappings.put("snd", "audio/basic"); + defaultMappings.put("aif", "audio/x-aiff"); + defaultMappings.put("aiff", "audio/x-aiff"); + defaultMappings.put("aifc", "audio/x-aiff"); + defaultMappings.put("wav", "audio/x-wav"); + defaultMappings.put("mp3", "audio/mpeg"); + defaultMappings.put("mpeg", "video/mpeg"); + defaultMappings.put("mpg", "video/mpeg"); + defaultMappings.put("mpe", "video/mpeg"); + defaultMappings.put("qt", "video/quicktime"); + defaultMappings.put("mov", "video/quicktime"); + defaultMappings.put("avi", "video/x-msvideo"); + defaultMappings.put("movie", "video/x-sgi-movie"); + defaultMappings.put("avx", "video/x-rad-screenplay"); + defaultMappings.put("wrl", "x-world/x-vrml"); + defaultMappings.put("mpv2", "video/mpeg2"); + + /* Add XML related MIMEs */ + + defaultMappings.put("xml", "text/xml"); + defaultMappings.put("xhtml", "application/xhtml+xml"); + defaultMappings.put("xsl", "text/xml"); + defaultMappings.put("svg", "image/svg+xml"); + defaultMappings.put("svgz", "image/svg+xml"); + defaultMappings.put("wbmp", "image/vnd.wap.wbmp"); + defaultMappings.put("wml", "text/vnd.wap.wml"); + defaultMappings.put("wmlc", "application/vnd.wap.wmlc"); + defaultMappings.put("wmls", "text/vnd.wap.wmlscript"); + defaultMappings.put("wmlscriptc", "application/vnd.wap.wmlscriptc"); + + DEFAULT_MIME_MAPPINGS = Collections.unmodifiableMap(defaultMappings); + } + + public static final MimeMappings DEFAULT = MimeMappings.builder().build(); + + MimeMappings(final Map mappings) { + this.mappings = mappings; + } + + public static Builder builder() { + return new Builder(true); + } + + public static Builder builder(boolean includeDefault) { + return new Builder(includeDefault); + } + + public static class Builder { + private final Map mappings = new HashMap<>(); + + + private Builder(boolean includeDefault) { + if (includeDefault) { + mappings.putAll(DEFAULT_MIME_MAPPINGS); + } + } + + public void addMapping(final String extension, final String contentType) { + mappings.put(extension, contentType); + } + + public MimeMappings build() { + return new MimeMappings(mappings); + } + } + + public String getMimeType(final String extension) { + return mappings.get(extension); + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/MultipartParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/MultipartParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/MultipartParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,426 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +import org.xnio.Pool; +import org.xnio.Pooled; + +/** + * @author Stuart Douglas + */ +public class MultipartParser { + + /** + * The Carriage Return ASCII character value. + */ + public static final byte CR = 0x0D; + + + /** + * The Line Feed ASCII character value. + */ + public static final byte LF = 0x0A; + + + /** + * The dash (-) ASCII character value. + */ + public static final byte DASH = 0x2D; + + /** + * A byte sequence that precedes a boundary (CRLF--). + */ + private static final byte[] BOUNDARY_PREFIX = {CR, LF, DASH, DASH}; + + public interface PartHandler { + void beginPart(final HeaderMap headers); + + void data(final ByteBuffer buffer) throws IOException; + + void endPart(); + } + + public static ParseState beginParse(final Pool bufferPool, final PartHandler handler, final byte[] boundary, final String requestCharset) { + + // We prepend CR/LF to the boundary to chop trailing CR/LF from + // body-data tokens. + byte[] boundaryToken = new byte[boundary.length + BOUNDARY_PREFIX.length]; + System.arraycopy(BOUNDARY_PREFIX, 0, boundaryToken, 0, BOUNDARY_PREFIX.length); + System.arraycopy(boundary, 0, boundaryToken, BOUNDARY_PREFIX.length, boundary.length); + return new ParseState(bufferPool, handler, requestCharset, boundaryToken); + } + + public static class ParseState { + private final Pool bufferPool; + private final PartHandler partHandler; + private final String requestCharset; + /** + * The boundary, complete with the initial CRLF-- + */ + private final byte[] boundary; + + //0=preamble + private volatile int state = 0; + private volatile int subState = Integer.MAX_VALUE; // used for preamble parsing + private volatile ByteArrayOutputStream currentString = null; + private volatile String currentHeaderName = null; + private volatile HeaderMap headers; + private volatile Encoding encodingHandler; + + + public ParseState(final Pool bufferPool, final PartHandler partHandler, String requestCharset, final byte[] boundary) { + this.bufferPool = bufferPool; + this.partHandler = partHandler; + this.requestCharset = requestCharset; + this.boundary = boundary; + } + + public void parse(ByteBuffer buffer) throws IOException { + while (buffer.hasRemaining()) { + switch (state) { + case 0: { + preamble(buffer); + break; + } + case 1: { + headerName(buffer); + break; + } + case 2: { + headerValue(buffer); + break; + } + case 3: { + entity(buffer); + break; + } + case -1: { + return; + } + default: { + throw new IllegalStateException("" + state); + } + } + } + } + + private void preamble(final ByteBuffer buffer) { + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (subState >= 0) { + //handle the case of no preamble. In this case there is no CRLF + if (subState == Integer.MAX_VALUE) { + if (boundary[2] == b) { + subState = 2; + } else { + subState = 0; + } + } + if (b == boundary[subState]) { + subState++; + if (subState == boundary.length) { + subState = -1; + } + } else if (b == boundary[0]) { + subState = 1; + } else { + subState = 0; + } + } else if (subState == -1) { + if (b == CR) { + subState = -2; + } + } else if (subState == -2) { + if (b == LF) { + subState = 0; + state = 1;//preamble is done + headers = new HeaderMap(); + return; + } else { + subState = -1; + } + } + } + } + + private void headerName(final ByteBuffer buffer) throws MalformedMessageException, UnsupportedEncodingException { + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (b == ':') { + if (currentString == null || subState != 0) { + throw new MalformedMessageException(); + } else { + currentHeaderName = new String(currentString.toByteArray(), requestCharset); + currentString.reset(); + subState = 0; + state = 2; + return; + } + } else if (b == CR) { + if (currentString != null) { + throw new MalformedMessageException(); + } else { + subState = 1; + } + } else if (b == LF) { + if (currentString != null || subState != 1) { + throw new MalformedMessageException(); + } + state = 3; + subState = 0; + partHandler.beginPart(headers); + //select the appropriate encoding + String encoding = headers.getFirst(Headers.CONTENT_TRANSFER_ENCODING); + if (encoding == null) { + encodingHandler = new IdentityEncoding(); + } else if (encoding.equalsIgnoreCase("base64")) { + encodingHandler = new Base64Encoding(bufferPool); + } else if (encoding.equalsIgnoreCase("quoted-printable")) { + encodingHandler = new QuotedPrintableEncoding(bufferPool); + } else { + encodingHandler = new IdentityEncoding(); + } + headers = null; + return; + + } else { + if (subState != 0) { + throw new MalformedMessageException(); + } else if (currentString == null) { + currentString = new ByteArrayOutputStream(); + } + currentString.write(b); + } + } + } + + private void headerValue(final ByteBuffer buffer) throws MalformedMessageException, UnsupportedEncodingException { + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (b == CR) { + subState = 1; + } else if (b == LF) { + if (subState != 1) { + throw new MalformedMessageException(); + } + headers.put(new HttpString(currentHeaderName.trim()), new String(currentString.toByteArray(), requestCharset).trim()); + state = 1; + subState = 0; + currentString = null; + return; + } else { + if (subState != 0) { + throw new MalformedMessageException(); + } + currentString.write(b); + } + } + } + + private void entity(final ByteBuffer buffer) throws IOException { + int startingSubState = subState; + int pos = buffer.position(); + while (buffer.hasRemaining()) { + final byte b = buffer.get(); + if (subState >= 0) { + if (b == boundary[subState]) { + //if we have a potential boundary match + subState++; + if (subState == boundary.length) { + startingSubState = 0; + //we have our data + ByteBuffer retBuffer = buffer.duplicate(); + retBuffer.position(pos); + + retBuffer.limit(Math.max(buffer.position() - boundary.length, 0)); + encodingHandler.handle(partHandler, retBuffer); + partHandler.endPart(); + subState = -1; + } + } else if (b == boundary[0]) { + //we started half way through a boundary, but it turns out we did not actually meet the boundary condition + //so we call the part handler with our copy of the boundary data + if (startingSubState > 0) { + encodingHandler.handle(partHandler, ByteBuffer.wrap(boundary, 0, startingSubState)); + startingSubState = 0; + } + subState = 1; + } else { + //we started half way through a boundary, but it turns out we did not actually meet the boundary condition + //so we call the part handler with our copy of the boundary data + if (startingSubState > 0) { + encodingHandler.handle(partHandler, ByteBuffer.wrap(boundary, 0, startingSubState)); + startingSubState = 0; + } + subState = 0; + } + } else if (subState == -1) { + if (b == CR) { + subState = -2; + } else if (b == DASH) { + subState = -3; + } + } else if (subState == -2) { + if (b == LF) { + //ok, we have our data + subState = 0; + state = 1; + headers = new HeaderMap(); + return; + } else if (b == DASH) { + subState = -3; + } else { + subState = -1; + } + } else if (subState == -3) { + if (b == DASH) { + state = -1; //we are done + return; + } else { + subState = -1; + } + } + } + //handle the data we read so far + ByteBuffer retBuffer = buffer.duplicate(); + retBuffer.position(pos); + if (subState == 0) { + //if we end partially through a boundary we do not handle the data + encodingHandler.handle(partHandler, retBuffer); + } else if (retBuffer.remaining() > subState && subState > 0) { + //we have some data to handle, and the end of the buffer might be a boundary match + retBuffer.limit(retBuffer.limit() - subState); + encodingHandler.handle(partHandler, retBuffer); + } + } + + public boolean isComplete() { + return state == -1; + } + } + + + private interface Encoding { + void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException; + } + + private static class IdentityEncoding implements Encoding { + + @Override + public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException { + handler.data(rawData); + rawData.clear(); + } + } + + private static class Base64Encoding implements Encoding { + + private final FlexBase64.Decoder decoder = FlexBase64.createDecoder(); + + private final Pool bufferPool; + + private Base64Encoding(final Pool bufferPool) { + this.bufferPool = bufferPool; + } + + @Override + public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException { + Pooled resource = bufferPool.allocate(); + ByteBuffer buf = resource.getResource(); + try { + do { + buf.clear(); + try { + decoder.decode(rawData, buf); + } catch (IOException e) { + throw new RuntimeException(e); + } + buf.flip(); + handler.data(buf); + } while (rawData.hasRemaining()); + } finally { + resource.free(); + } + } + } + + private static class QuotedPrintableEncoding implements Encoding { + + private final Pool bufferPool; + boolean equalsSeen; + byte firstCharacter; + + private QuotedPrintableEncoding(final Pool bufferPool) { + this.bufferPool = bufferPool; + } + + + @Override + public void handle(final PartHandler handler, final ByteBuffer rawData) throws IOException { + boolean equalsSeen = this.equalsSeen; + byte firstCharacter = this.firstCharacter; + Pooled resource = bufferPool.allocate(); + ByteBuffer buf = resource.getResource(); + try { + while (rawData.hasRemaining()) { + byte b = rawData.get(); + if (equalsSeen) { + if (firstCharacter == 0) { + if (b == '\n' || b == '\r') { + //soft line break + //ignore + equalsSeen = false; + } else { + firstCharacter = b; + } + } else { + int result = Character.digit((char) firstCharacter, 16); + result <<= 4; //shift it 4 bytes and then add the next value to the end + result += Character.digit((char) b, 16); + buf.put((byte) result); + equalsSeen = false; + firstCharacter = 0; + } + } else if (b == '=') { + equalsSeen = true; + } else { + buf.put(b); + if (!buf.hasRemaining()) { + buf.flip(); + handler.data(buf); + buf.clear(); + } + } + } + buf.flip(); + handler.data(buf); + } finally { + resource.free(); + this.equalsSeen = equalsSeen; + this.firstCharacter = firstCharacter; + } + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/NetworkUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/NetworkUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/NetworkUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,42 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * @author Stuart Douglas + */ +public class NetworkUtils { + + public static String formatPossibleIpv6Address(String address) { + if (address == null) { + return address; + } + if (!address.contains(":")) { + return address; + } + if (address.startsWith("[") && address.endsWith("]")) { + return address; + } + return "[" + address + "]"; + } + + private NetworkUtils() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/util/PathMatcher.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/PathMatcher.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/PathMatcher.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,266 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowMessages; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.ConcurrentMap; + +/** + * Handler that dispatches to a given handler based of a prefix match of the path. + *

+ * This only matches a single level of a request, e.g if you have a request that takes the form: + *

+ * /foo/bar + *

+ * + * @author Stuart Douglas + */ +public class PathMatcher { + + private static final char PATH_SEPARATOR = '/'; + private static final String STRING_PATH_SEPARATOR = "/"; + + private volatile T defaultHandler; + private final ConcurrentMap paths = new CopyOnWriteMap<>(); + private final ConcurrentMap exactPathMatches = new CopyOnWriteMap<>(); + + /** + * lengths of all registered paths + */ + private volatile int[] lengths = {}; + + public PathMatcher(final T defaultHandler) { + this.defaultHandler = defaultHandler; + } + + public PathMatcher() { + } + + /** + * Matches a path against the registered handlers. + * @param path The relative path to match + * @return The match match. This will never be null, however if none matched its value field will be + */ + public PathMatch match(String path){ + if (!exactPathMatches.isEmpty()) { + T match = getExactPath(path); + if (match != null) { + return new PathMatch<>("", match); + } + } + + int length = path.length(); + final int[] lengths = this.lengths; + for (int i = 0; i < lengths.length; ++i) { + int pathLength = lengths[i]; + if (pathLength == length) { + T next = paths.get(path); + if (next != null) { + return new PathMatch<>(path.substring(pathLength), next); + } + } else if (pathLength < length) { + char c = path.charAt(pathLength); + if (c == '/') { + String part = path.substring(0, pathLength); + T next = paths.get(part); + if (next != null) { + return new PathMatch<>(path.substring(pathLength), next); + } + } + } + } + return new PathMatch<>(path, defaultHandler); + } + + /** + * Adds a path prefix and a handler for that path. If the path does not start + * with a / then one will be prepended. + *

+ * The match is done on a prefix bases, so registering /foo will also match /bar. Exact + * path matches are taken into account first. + *

+ * If / is specified as the path then it will replace the default handler. + * + * @param path The path + * @param handler The handler + */ + public synchronized PathMatcher addPrefixPath(final String path, final T handler) { + if (path.isEmpty()) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + + final String normalizedPath = this.normalizeSlashes(path); + + if (PathMatcher.STRING_PATH_SEPARATOR.equals(normalizedPath)) { + this.defaultHandler = handler; + return this; + } + + paths.put(normalizedPath, handler); + + buildLengths(); + return this; + } + + + public synchronized PathMatcher addExactPath(final String path, final T handler) { + if (path.isEmpty()) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + exactPathMatches.put(this.normalizeSlashes(path), handler); + return this; + } + + public T getExactPath(final String path) { + return exactPathMatches.get(this.normalizeSlashes(path)); + } + + public T getPrefixPath(final String path) { + + final String normalizedPath = this.normalizeSlashes(path); + + // enable the prefix path mechanism to return the default handler + if (PathMatcher.STRING_PATH_SEPARATOR.equals(normalizedPath) && !paths.containsKey(normalizedPath)) { + return this.defaultHandler; + } + + // return the value for the given path + return paths.get(normalizedPath); + } + + private void buildLengths() { + final Set lengths = new TreeSet<>(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return -o1.compareTo(o2); + } + }); + for (String p : paths.keySet()) { + lengths.add(p.length()); + } + + int[] lengthArray = new int[lengths.size()]; + int pos = 0; + for (int i : lengths) { + lengthArray[pos++] = i; + } + this.lengths = lengthArray; + } + + @Deprecated + public synchronized PathMatcher removePath(final String path) { + return removePrefixPath(path); + } + + public synchronized PathMatcher removePrefixPath(final String path) { + if (path == null || path.isEmpty()) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + + final String normalizedPath = this.normalizeSlashes(path); + + if (PathMatcher.STRING_PATH_SEPARATOR.equals(normalizedPath)) { + defaultHandler = null; + return this; + } + + paths.remove(normalizedPath); + + buildLengths(); + return this; + } + + public synchronized PathMatcher removeExactPath(final String path) { + if (path == null || path.isEmpty()) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + + exactPathMatches.remove(this.normalizeSlashes(path)); + + return this; + } + + public synchronized PathMatcher clearPaths() { + paths.clear(); + exactPathMatches.clear(); + this.lengths = new int[0]; + defaultHandler = null; + return this; + } + + public Map getPaths() { + return Collections.unmodifiableMap(paths); + } + + public static final class PathMatch { + private final String remaining; + private final T value; + + public PathMatch(String remaining, T value) { + this.remaining = remaining; + this.value = value; + } + + public String getRemaining() { + return remaining; + } + + public T getValue() { + return value; + } + } + + /** + * Adds a '/' prefix to the beginning of a path if one isn't present + * and removes trailing slashes if any are present. + * + * @param path the path to normalize + * @return a normalized (with respect to slashes) result + */ + private String normalizeSlashes(final String path) { + // prepare + final StringBuilder builder = new StringBuilder(path); + boolean modified = false; + + // remove all trailing '/'s except the first one + while (builder.length() > 0 && builder.length() != 1 && PathMatcher.PATH_SEPARATOR == builder.charAt(builder.length() - 1)) { + builder.deleteCharAt(builder.length() - 1); + modified = true; + } + + // add a slash at the beginning if one isn't present + if (builder.length() == 0 || PathMatcher.PATH_SEPARATOR != builder.charAt(0)) { + builder.insert(0, PathMatcher.PATH_SEPARATOR); + modified = true; + } + + // only create string when it was modified + if (modified) { + return builder.toString(); + } + + return path; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/PathTemplate.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/PathTemplate.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/PathTemplate.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,324 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import io.undertow.UndertowMessages; + + +/** + * Represents a parsed web socket path template. + *

+ * This class can be compared to other path templates, with templates that are considered + * lower have a higher priority, and should be checked first. + *

+ * This comparison can also be used to check for semantically equal paths, if + * a.compareTo(b) == 0 then the two paths are equivalent, which will generally + * result in a deployment exception. + * + * @author Stuart Douglas + */ +public class PathTemplate implements Comparable { + + private final String templateString; + private final boolean template; + private final String base; + private final List parts; + private final Set parameterNames; + + private PathTemplate(String templateString, final boolean template, final String base, final List parts, Set parameterNames) { + this.templateString = templateString; + this.template = template; + this.base = base; + this.parts = parts; + this.parameterNames = Collections.unmodifiableSet(parameterNames); + } + + public static PathTemplate create(final String inputPath) { + // a path is required + if(inputPath == null) { + throw UndertowMessages.MESSAGES.pathMustBeSpecified(); + } + + // prepend a "/" if none is present + if(!inputPath.startsWith("/")) { + return PathTemplate.create("/" + inputPath); + } + + // otherwise normalize template + final StringBuilder builder = new StringBuilder(inputPath); + while(builder != null && builder.length() > 1 && '/' == builder.charAt(builder.length() - 1)) { + builder.deleteCharAt(builder.length() - 1); + } + + // create string from modified string + final String path = builder.toString(); + + int state = 0; + String base = ""; + List parts = new ArrayList<>(); + int stringStart = 0; + //0 parsing base + //1 parsing base, last char was / + //2 in template part + //3 just after template part, expecting / + //4 expecting either template or segment + //5 in segment + + for (int i = 0; i < path.length(); ++i) { + final int c = path.charAt(i); + switch (state) { + case 0: { + if (c == '/') { + state = 1; + } else { + state = 0; + } + break; + } + case 1: { + if (c == '{') { + base = path.substring(0, i); + stringStart = i + 1; + state = 2; + } else if (c != '/') { + state = 0; + } + break; + } + case 2: { + if (c == '}') { + Part part = new Part(true, path.substring(stringStart, i)); + parts.add(part); + stringStart = i; + state = 3; + } + break; + } + case 3: { + if (c == '/') { + state = 4; + } else { + throw UndertowMessages.MESSAGES.couldNotParseUriTemplate(path, i); + } + break; + } + case 4: { + if (c == '{') { + stringStart = i + 1; + state = 2; + } else if (c != '/') { + stringStart = i; + state = 5; + } + break; + } + case 5: { + if (c == '/') { + Part part = new Part(false, path.substring(stringStart, i)); + parts.add(part); + stringStart = i + 1; + state = 4; + } + break; + } + } + } + + switch (state) { + case 0: + case 1: { + base = path; + break; + } + case 2: { + throw UndertowMessages.MESSAGES.couldNotParseUriTemplate(path, path.length()); + } + case 5: { + Part part = new Part(false, path.substring(stringStart)); + parts.add(part); + break; + } + } + final Set templates = new HashSet<>(); + for(Part part : parts) { + if(part.template) { + templates.add(part.part); + } + } + return new PathTemplate(path, state > 1, base, parts, templates); + } + + /** + * Check if the given uri matches the template. If so then it will return true and + * place the value of any path parameters into the given map. + *

+ * Note the map may be modified even if the match in unsuccessful, however in this case + * it will be emptied before the method returns + * + * @param path The request path, relative to the context root + * @param pathParameters The path parameters map to fill out + * @return true if the URI is a match + */ + public boolean matches(final String path, final Map pathParameters) { + if (!path.startsWith(base)) { + return false; + } + int baseLength = base.length(); + if (!template) { + return path.length() == baseLength; + } + + + int cp = 0; + Part current = parts.get(cp); + int stringStart = baseLength; + int i; + for (i = baseLength; i < path.length(); ++i) { + final char c = path.charAt(i); + if (c == '?') { + break; + } else if (c == '/') { + String result = path.substring(stringStart, i); + if (current.template) { + pathParameters.put(current.part, result); + } else if (!result.equals(current.part)) { + pathParameters.clear(); + return false; + } + ++cp; + if (cp == parts.size()) { + //this is a match if this is the last character + return i == (path.length() - 1); + } + current = parts.get(cp); + stringStart = i + 1; + } + } + if (cp + 1 != parts.size()) { + pathParameters.clear(); + return false; + } + String result = path.substring(stringStart, i); + if (current.template) { + pathParameters.put(current.part, result); + } else if (!result.equals(current.part)) { + pathParameters.clear(); + return false; + } + return true; + } + + @Override + public int compareTo(final PathTemplate o) { + //we want templates with the highest priority to sort first + //so we sort in reverse priority order + + //templates have lower priority + if (template && !o.template) { + return 1; + } else if (o.template && !template) { + return -1; + } + + int res = base.compareTo(o.base); + if (res > 0) { + //our base is longer + return -1; + } else if (res < 0) { + return 1; + } else if (!template) { + //they are the same path + return 0; + } + + //the first path with a non-template element + int i = 0; + for (; ; ) { + if (parts.size() == i) { + if (o.parts.size() == i) { + return base.compareTo(o.base); + } + return 1; + } else if (o.parts.size() == i) { + //we have more parts, so should be checked first + return -1; + } + Part thisPath = parts.get(i); + Part otherPart = o.parts.get(i); + if (thisPath.template && !otherPart.template) { + //non template part sorts first + return 1; + } else if (!thisPath.template && otherPart.template) { + return -1; + } else if (!thisPath.template) { + int r = thisPath.part.compareTo(otherPart.part); + if (r != 0) { + return r; + } + } + ++i; + } + } + + public String getBase() { + return base; + } + + public String getTemplateString() { + return templateString; + } + + public Set getParameterNames() { + return parameterNames; + } + + private static class Part { + final boolean template; + final String part; + + private Part(final boolean template, final String part) { + this.template = template; + this.part = part; + } + + @Override + public String toString() { + return "Part{" + + "template=" + template + + ", part='" + part + '\'' + + '}'; + } + } + + @Override + public String toString() { + return "PathTemplate{" + + "template=" + template + + ", base='" + base + '\'' + + ", parts=" + parts + + '}'; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/PathTemplateMatch.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/PathTemplateMatch.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/PathTemplateMatch.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.Map; + +/** + * The result of a path template match. + * + * @author Stuart Douglas + */ +public class PathTemplateMatch { + + public static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(PathTemplateMatch.class); + + private final String matchedTemplate; + private final Map parameters; + + public PathTemplateMatch(String matchedTemplate, Map parameters) { + this.matchedTemplate = matchedTemplate; + this.parameters = parameters; + } + + public String getMatchedTemplate() { + return matchedTemplate; + } + + public Map getParameters() { + return parameters; + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/PathTemplateMatcher.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/PathTemplateMatcher.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/PathTemplateMatcher.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,247 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowMessages; + +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.TreeSet; + +/** + * Utility class that provides fast path matching of path templates. Templates are stored in a map based on the stem of the template, + * and matches longest stem first. + *

+ * TODO: we can probably do this faster using a trie type structure, but I think the current impl should perform ok most of the time + * + * @author Stuart Douglas + */ +public class PathTemplateMatcher { + + /** + * Map of path template stem to the path templates that share the same base. + */ + private Map> pathTemplateMap = new CopyOnWriteMap<>(); + + /** + * lengths of all registered paths + */ + private volatile int[] lengths = {}; + + public PathMatchResult match(final String path) { + final Map params = new HashMap<>(); + int length = path.length(); + final int[] lengths = this.lengths; + for (int i = 0; i < lengths.length; ++i) { + int pathLength = lengths[i]; + if (pathLength == length) { + Set entry = pathTemplateMap.get(path); + if (entry != null) { + PathMatchResult res = handleStemMatch(entry, path, params); + if (res != null) { + return res; + } + } + } else if (pathLength < length) { + char c = path.charAt(pathLength); + if (c == '/') { + String part = path.substring(0, pathLength); + Set entry = pathTemplateMap.get(part); + if (entry != null) { + PathMatchResult res = handleStemMatch(entry, path, params); + if (res != null) { + return res; + } + } + } + } + } + return null; + } + + private PathMatchResult handleStemMatch(Set entry, final String path, final Map params) { + for (PathTemplateHolder val : entry) { + if (val.template.matches(path, params)) { + return new PathMatchResult<>(params, val.template.getTemplateString(), val.value); + } else { + params.clear(); + } + } + return null; + } + + + public synchronized PathTemplateMatcher add(final PathTemplate template, final T value) { + Set values = pathTemplateMap.get(trimBase(template)); + Set newValues; + if (values == null) { + newValues = new TreeSet<>(); + } else { + newValues = new TreeSet<>(values); + } + PathTemplateHolder holder = new PathTemplateHolder(value, template); + if (newValues.contains(holder)) { + PathTemplate equivalent = null; + for (PathTemplateHolder item : newValues) { + if (item.compareTo(holder) == 0) { + equivalent = item.template; + break; + } + } + throw UndertowMessages.MESSAGES.matcherAlreadyContainsTemplate(template.getTemplateString(), equivalent.getTemplateString()); + } + newValues.add(holder); + pathTemplateMap.put(trimBase(template), newValues); + buildLengths(); + return this; + } + + private String trimBase(PathTemplate template) { + if (template.getBase().endsWith("/")) { + return template.getBase().substring(0, template.getBase().length() - 1); + } + return template.getBase(); + } + + private void buildLengths() { + final Set lengths = new TreeSet<>(new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return -o1.compareTo(o2); + } + }); + for (String p : pathTemplateMap.keySet()) { + lengths.add(p.length()); + } + + int[] lengthArray = new int[lengths.size()]; + int pos = 0; + for (int i : lengths) { + lengthArray[pos++] = i; //-1 because the base paths end with a / + } + this.lengths = lengthArray; + } + + public synchronized PathTemplateMatcher add(final String pathTemplate, final T value) { + final PathTemplate template = PathTemplate.create(pathTemplate); + return add(template, value); + } + + public synchronized PathTemplateMatcher addAll(PathTemplateMatcher pathTemplateMatcher) { + for (Entry> entry : pathTemplateMatcher.getPathTemplateMap().entrySet()) { + for (PathTemplateHolder pathTemplateHolder : entry.getValue()) { + add(pathTemplateHolder.template, pathTemplateHolder.value); + } + } + return this; + } + + Map> getPathTemplateMap() { + return pathTemplateMap; + } + + public Set getPathTemplates() { + Set templates = new HashSet<>(); + for (Set holders : pathTemplateMap.values()) { + for (PathTemplateHolder holder: holders) { + templates.add(holder.template); + } + } + return templates; + } + + public synchronized PathTemplateMatcher remove(final String pathTemplate) { + final PathTemplate template = PathTemplate.create(pathTemplate); + return remove(template); + } + + private synchronized PathTemplateMatcher remove(PathTemplate template) { + Set values = pathTemplateMap.get(trimBase(template)); + Set newValues; + if (values == null) { + return this; + } else { + newValues = new TreeSet<>(values); + } + Iterator it = newValues.iterator(); + while (it.hasNext()) { + PathTemplateHolder next = it.next(); + if (next.template.getTemplateString().equals(template.getTemplateString())) { + it.remove(); + break; + } + } + if (newValues.size() == 0) { + pathTemplateMap.remove(trimBase(template)); + } else { + pathTemplateMap.put(trimBase(template), newValues); + } + buildLengths(); + return this; + } + + + public synchronized T get(String template) { + PathTemplate pathTemplate = PathTemplate.create(template); + Set values = pathTemplateMap.get(trimBase(pathTemplate)); + if(values == null) { + return null; + } + for (PathTemplateHolder next : values) { + if (next.template.getTemplateString().equals(template)) { + return next.value; + } + } + return null; + } + + public static class PathMatchResult extends PathTemplateMatch { + private final T value; + + public PathMatchResult(Map parameters, String matchedTemplate, T value) { + super(matchedTemplate, parameters); + this.value = value; + } + + public T getValue() { + return value; + } + } + + private final class PathTemplateHolder implements Comparable { + final T value; + final PathTemplate template; + + private PathTemplateHolder(T value, PathTemplate template) { + this.value = value; + this.template = template; + } + + @Override + public int compareTo(PathTemplateHolder o) { + return template.compareTo(o.template); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/util/PipeliningExecutor.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/PipeliningExecutor.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/PipeliningExecutor.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,74 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowLogger; + +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Executor; + +/** + * Executor that will continue to re-run tasks in a loop that are submitted from its own thread. + * + * @author Stuart Douglas + */ +public class PipeliningExecutor implements Executor { + + private final Executor executor; + + private static final ThreadLocal> THREAD_QUEUE = new ThreadLocal<>(); + + public PipeliningExecutor(Executor executor) { + this.executor = executor; + } + + @Override + public void execute(final Runnable command) { + List queue = THREAD_QUEUE.get(); + if (queue != null) { + queue.add(command); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + LinkedList queue = THREAD_QUEUE.get(); + if (queue == null) { + THREAD_QUEUE.set(queue = new LinkedList<>()); + } + try { + command.run(); + } catch (Throwable t) { + UndertowLogger.REQUEST_LOGGER.debugf(t, "Task %s failed", command); + } + Runnable runnable = queue.poll(); + while (runnable != null) { + try { + runnable.run(); + } catch (Throwable t) { + UndertowLogger.REQUEST_LOGGER.debugf(t, "Task %s failed", command); + } + runnable = queue.poll(); + } + + } + }); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/PortableConcurrentDirectDeque.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/PortableConcurrentDirectDeque.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/PortableConcurrentDirectDeque.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,1447 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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. + */ + +/* + * Written by Doug Lea and Martin Buchholz with assistance from members of + * JCP JSR-166 Expert Group and released to the public domain, as explained + * at http://creativecommons.org/publicdomain/zero/1.0/ + */ + +package io.undertow.util; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Deque; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +/** + * A modified version of ConcurrentLinkedDequeue which includes direct + * removal and is portable accorss all JVMs. This is only a fallback if + * the JVM does not offer access to Unsafe. + * + * More specifically, an unbounded concurrent {@linkplain java.util.Deque deque} based on linked nodes. + * Concurrent insertion, removal, and access operations execute safely + * across multiple threads. + * A {@code ConcurrentLinkedDeque} is an appropriate choice when + * many threads will share access to a common collection. + * Like most other concurrent collection implementations, this class + * does not permit the use of {@code null} elements. + * + *

Iterators are weakly consistent, returning elements + * reflecting the state of the deque at some point at or since the + * creation of the iterator. They do not throw {@link + * java.util.ConcurrentModificationException + * ConcurrentModificationException}, and may proceed concurrently with + * other operations. + * + *

Beware that, unlike in most collections, the {@code size} method + * is NOT a constant-time operation. Because of the + * asynchronous nature of these deques, determining the current number + * of elements requires a traversal of the elements, and so may report + * inaccurate results if this collection is modified during traversal. + * Additionally, the bulk operations {@code addAll}, + * {@code removeAll}, {@code retainAll}, {@code containsAll}, + * {@code equals}, and {@code toArray} are not guaranteed + * to be performed atomically. For example, an iterator operating + * concurrently with an {@code addAll} operation might view only some + * of the added elements. + * + *

This class and its iterator implement all of the optional + * methods of the {@link java.util.Deque} and {@link java.util.Iterator} interfaces. + * + *

Memory consistency effects: As with other concurrent collections, + * actions in a thread prior to placing an object into a + * {@code ConcurrentLinkedDeque} + * happen-before + * actions subsequent to the access or removal of that element from + * the {@code ConcurrentLinkedDeque} in another thread. + * + *

This class is a member of the + * + * Java Collections Framework. + * + * @since 1.7 + * @author Doug Lea + * @author Martin Buchholz + * @author Jason T. Grene + * @param the type of elements held in this collection + */ + +public class PortableConcurrentDirectDeque + extends ConcurrentDirectDeque implements Deque, java.io.Serializable { + + /* + * This is an implementation of a concurrent lock-free deque + * supporting interior removes but not interior insertions, as + * required to support the entire Deque interface. + * + * We extend the techniques developed for ConcurrentLinkedQueue and + * LinkedTransferQueue (see the internal docs for those classes). + * Understanding the ConcurrentLinkedQueue implementation is a + * prerequisite for understanding the implementation of this class. + * + * The data structure is a symmetrical doubly-linked "GC-robust" + * linked list of nodes. We minimize the number of volatile writes + * using two techniques: advancing multiple hops with a single CAS + * and mixing volatile and non-volatile writes of the same memory + * locations. + * + * A node contains the expected E ("item") and links to predecessor + * ("prev") and successor ("next") nodes: + * + * class Node { volatile Node prev, next; volatile E item; } + * + * A node p is considered "live" if it contains a non-null item + * (p.item != null). When an item is CASed to null, the item is + * atomically logically deleted from the collection. + * + * At any time, there is precisely one "first" node with a null + * prev reference that terminates any chain of prev references + * starting at a live node. Similarly there is precisely one + * "last" node terminating any chain of next references starting at + * a live node. The "first" and "last" nodes may or may not be live. + * The "first" and "last" nodes are always mutually reachable. + * + * A new element is added atomically by CASing the null prev or + * next reference in the first or last node to a fresh node + * containing the element. The element's node atomically becomes + * "live" at that point. + * + * A node is considered "active" if it is a live node, or the + * first or last node. Active nodes cannot be unlinked. + * + * A "self-link" is a next or prev reference that is the same node: + * p.prev == p or p.next == p + * Self-links are used in the node unlinking process. Active nodes + * never have self-links. + * + * A node p is active if and only if: + * + * p.item != null || + * (p.prev == null && p.next != p) || + * (p.next == null && p.prev != p) + * + * The deque object has two node references, "head" and "tail". + * The head and tail are only approximations to the first and last + * nodes of the deque. The first node can always be found by + * following prev pointers from head; likewise for tail. However, + * it is permissible for head and tail to be referring to deleted + * nodes that have been unlinked and so may not be reachable from + * any live node. + * + * There are 3 stages of node deletion; + * "logical deletion", "unlinking", and "gc-unlinking". + * + * 1. "logical deletion" by CASing item to null atomically removes + * the element from the collection, and makes the containing node + * eligible for unlinking. + * + * 2. "unlinking" makes a deleted node unreachable from active + * nodes, and thus eventually reclaimable by GC. Unlinked nodes + * may remain reachable indefinitely from an iterator. + * + * Physical node unlinking is merely an optimization (albeit a + * critical one), and so can be performed at our convenience. At + * any time, the set of live nodes maintained by prev and next + * links are identical, that is, the live nodes found via next + * links from the first node is equal to the elements found via + * prev links from the last node. However, this is not true for + * nodes that have already been logically deleted - such nodes may + * be reachable in one direction only. + * + * 3. "gc-unlinking" takes unlinking further by making active + * nodes unreachable from deleted nodes, making it easier for the + * GC to reclaim future deleted nodes. This step makes the data + * structure "gc-robust", as first described in detail by Boehm + * (http://portal.acm.org/citation.cfm?doid=503272.503282). + * + * GC-unlinked nodes may remain reachable indefinitely from an + * iterator, but unlike unlinked nodes, are never reachable from + * head or tail. + * + * Making the data structure GC-robust will eliminate the risk of + * unbounded memory retention with conservative GCs and is likely + * to improve performance with generational GCs. + * + * When a node is dequeued at either end, e.g. via poll(), we would + * like to break any references from the node to active nodes. We + * develop further the use of self-links that was very effective in + * other concurrent collection classes. The idea is to replace + * prev and next pointers with special values that are interpreted + * to mean off-the-list-at-one-end. These are approximations, but + * good enough to preserve the properties we want in our + * traversals, e.g. we guarantee that a traversal will never visit + * the same element twice, but we don't guarantee whether a + * traversal that runs out of elements will be able to see more + * elements later after enqueues at that end. Doing gc-unlinking + * safely is particularly tricky, since any node can be in use + * indefinitely (for example by an iterator). We must ensure that + * the nodes pointed at by head/tail never get gc-unlinked, since + * head/tail are needed to get "back on track" by other nodes that + * are gc-unlinked. gc-unlinking accounts for much of the + * implementation complexity. + * + * Since neither unlinking nor gc-unlinking are necessary for + * correctness, there are many implementation choices regarding + * frequency (eagerness) of these operations. Since volatile + * reads are likely to be much cheaper than CASes, saving CASes by + * unlinking multiple adjacent nodes at a time may be a win. + * gc-unlinking can be performed rarely and still be effective, + * since it is most important that long chains of deleted nodes + * are occasionally broken. + * + * The actual representation we use is that p.next == p means to + * goto the first node (which in turn is reached by following prev + * pointers from head), and p.next == null && p.prev == p means + * that the iteration is at an end and that p is a (static final) + * dummy node, NEXT_TERMINATOR, and not the last active node. + * Finishing the iteration when encountering such a TERMINATOR is + * good enough for read-only traversals, so such traversals can use + * p.next == null as the termination condition. When we need to + * find the last (active) node, for enqueueing a new node, we need + * to check whether we have reached a TERMINATOR node; if so, + * restart traversal from tail. + * + * The implementation is completely directionally symmetrical, + * except that most public methods that iterate through the list + * follow next pointers ("forward" direction). + * + * We believe (without full proof) that all single-element deque + * operations (e.g., addFirst, peekLast, pollLast) are linearizable + * (see Herlihy and Shavit's book). However, some combinations of + * operations are known not to be linearizable. In particular, + * when an addFirst(A) is racing with pollFirst() removing B, it is + * possible for an observer iterating over the elements to observe + * A B C and subsequently observe A C, even though no interior + * removes are ever performed. Nevertheless, iterators behave + * reasonably, providing the "weakly consistent" guarantees. + * + * Empirically, microbenchmarks suggest that this class adds about + * 40% overhead relative to ConcurrentLinkedQueue, which feels as + * good as we can hope for. + */ + + private static final long serialVersionUID = 876323262645176354L; + + /** + * A node from which the first node on list (that is, the unique node p + * with p.prev == null && p.next != p) can be reached in O(1) time. + * Invariants: + * - the first node is always O(1) reachable from head via prev links + * - all live nodes are reachable from the first node via succ() + * - head != null + * - (tmp = head).next != tmp || tmp != head + * - head is never gc-unlinked (but may be unlinked) + * Non-invariants: + * - head.item may or may not be null + * - head may not be reachable from the first or last node, or from tail + */ + private transient volatile Node head; + + /** + * A node from which the last node on list (that is, the unique node p + * with p.next == null && p.prev != p) can be reached in O(1) time. + * Invariants: + * - the last node is always O(1) reachable from tail via next links + * - all live nodes are reachable from the last node via pred() + * - tail != null + * - tail is never gc-unlinked (but may be unlinked) + * Non-invariants: + * - tail.item may or may not be null + * - tail may not be reachable from the first or last node, or from head + */ + private transient volatile Node tail; + + private static final AtomicReferenceFieldUpdater headUpdater = AtomicReferenceFieldUpdater.newUpdater(PortableConcurrentDirectDeque.class, Node.class, "head"); + private static final AtomicReferenceFieldUpdater tailUpdater = AtomicReferenceFieldUpdater.newUpdater(PortableConcurrentDirectDeque.class, Node.class, "tail"); + + private static final Node PREV_TERMINATOR, NEXT_TERMINATOR; + + @SuppressWarnings("unchecked") + Node prevTerminator() { + return (Node) PREV_TERMINATOR; + } + + @SuppressWarnings("unchecked") + Node nextTerminator() { + return (Node) NEXT_TERMINATOR; + } + + static final class Node { + private static final AtomicReferenceFieldUpdater prevUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "prev"); + private static final AtomicReferenceFieldUpdater nextUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Node.class, "next"); + private static final AtomicReferenceFieldUpdater itemUpdater = AtomicReferenceFieldUpdater.newUpdater(Node.class, Object.class, "item"); + + + volatile Node prev; + volatile E item; + volatile Node next; + + Node() { // default constructor for NEXT_TERMINATOR, PREV_TERMINATOR + } + + /** + * Constructs a new node. Uses relaxed write because item can + * only be seen after publication via casNext or casPrev. + */ + Node(E item) { + this.item = item; + } + + boolean casItem(E cmp, E val) { + return itemUpdater.compareAndSet(this, cmp, val); + } + + void lazySetNext(Node val) { + next = val; + } + + boolean casNext(Node cmp, Node val) { + return nextUpdater.compareAndSet(this, cmp, val); + } + + void lazySetPrev(Node val) { + prev = val; + } + + boolean casPrev(Node cmp, Node val) { + return prevUpdater.compareAndSet(this, cmp, val); + } + } + + /** + * Links e as first element. + */ + private Node linkFirst(E e) { + checkNotNull(e); + final Node newNode = new Node<>(e); + + restartFromHead: + for (;;) + for (Node h = head, p = h, q;;) { + if ((q = p.prev) != null && + (q = (p = q).prev) != null) + // Check for head updates every other hop. + // If p == q, we are sure to follow head instead. + p = (h != (h = head)) ? h : q; + else if (p.next == p) // PREV_TERMINATOR + continue restartFromHead; + else { + // p is first node + newNode.lazySetNext(p); // CAS piggyback + if (p.casPrev(null, newNode)) { + // Successful CAS is the linearization point + // for e to become an element of this deque, + // and for newNode to become "live". + if (p != h) // hop two nodes at a time + casHead(h, newNode); // Failure is OK. + return newNode; + } + // Lost CAS race to another thread; re-read prev + } + } + } + + /** + * Links e as last element. + */ + private Node linkLast(E e) { + checkNotNull(e); + final Node newNode = new Node<>(e); + + restartFromTail: + for (;;) + for (Node t = tail, p = t, q;;) { + if ((q = p.next) != null && + (q = (p = q).next) != null) + // Check for tail updates every other hop. + // If p == q, we are sure to follow tail instead. + p = (t != (t = tail)) ? t : q; + else if (p.prev == p) // NEXT_TERMINATOR + continue restartFromTail; + else { + // p is last node + newNode.lazySetPrev(p); // CAS piggyback + if (p.casNext(null, newNode)) { + // Successful CAS is the linearization point + // for e to become an element of this deque, + // and for newNode to become "live". + if (p != t) // hop two nodes at a time + casTail(t, newNode); // Failure is OK. + return newNode; + } + // Lost CAS race to another thread; re-read next + } + } + } + + private static final int HOPS = 2; + + /** + * Unlinks non-null node x. + */ + void unlink(Node x) { + // assert x != null; + // assert x.item == null; + // assert x != PREV_TERMINATOR; + // assert x != NEXT_TERMINATOR; + + final Node prev = x.prev; + final Node next = x.next; + if (prev == null) { + unlinkFirst(x, next); + } else if (next == null) { + unlinkLast(x, prev); + } else { + // Unlink interior node. + // + // This is the common case, since a series of polls at the + // same end will be "interior" removes, except perhaps for + // the first one, since end nodes cannot be unlinked. + // + // At any time, all active nodes are mutually reachable by + // following a sequence of either next or prev pointers. + // + // Our strategy is to find the unique active predecessor + // and successor of x. Try to fix up their links so that + // they point to each other, leaving x unreachable from + // active nodes. If successful, and if x has no live + // predecessor/successor, we additionally try to gc-unlink, + // leaving active nodes unreachable from x, by rechecking + // that the status of predecessor and successor are + // unchanged and ensuring that x is not reachable from + // tail/head, before setting x's prev/next links to their + // logical approximate replacements, self/TERMINATOR. + Node activePred, activeSucc; + boolean isFirst, isLast; + int hops = 1; + + // Find active predecessor + for (Node p = prev; ; ++hops) { + if (p.item != null) { + activePred = p; + isFirst = false; + break; + } + Node q = p.prev; + if (q == null) { + if (p.next == p) + return; + activePred = p; + isFirst = true; + break; + } + else if (p == q) + return; + else + p = q; + } + + // Find active successor + for (Node p = next; ; ++hops) { + if (p.item != null) { + activeSucc = p; + isLast = false; + break; + } + Node q = p.next; + if (q == null) { + if (p.prev == p) + return; + activeSucc = p; + isLast = true; + break; + } + else if (p == q) + return; + else + p = q; + } + + // TODO: better HOP heuristics + if (hops < HOPS + // always squeeze out interior deleted nodes + && (isFirst | isLast)) + return; + + // Squeeze out deleted nodes between activePred and + // activeSucc, including x. + skipDeletedSuccessors(activePred); + skipDeletedPredecessors(activeSucc); + + // Try to gc-unlink, if possible + if ((isFirst | isLast) && + + // Recheck expected state of predecessor and successor + (activePred.next == activeSucc) && + (activeSucc.prev == activePred) && + (isFirst ? activePred.prev == null : activePred.item != null) && + (isLast ? activeSucc.next == null : activeSucc.item != null)) { + + updateHead(); // Ensure x is not reachable from head + updateTail(); // Ensure x is not reachable from tail + + // Finally, actually gc-unlink + x.lazySetPrev(isFirst ? prevTerminator() : x); + x.lazySetNext(isLast ? nextTerminator() : x); + } + } + } + + /** + * Unlinks non-null first node. + */ + private void unlinkFirst(Node first, Node next) { + // assert first != null; + // assert next != null; + // assert first.item == null; + for (Node o = null, p = next, q;;) { + if (p.item != null || (q = p.next) == null) { + if (o != null && p.prev != p && first.casNext(next, p)) { + skipDeletedPredecessors(p); + if (first.prev == null && + (p.next == null || p.item != null) && + p.prev == first) { + + updateHead(); // Ensure o is not reachable from head + updateTail(); // Ensure o is not reachable from tail + + // Finally, actually gc-unlink + o.lazySetNext(o); + o.lazySetPrev(prevTerminator()); + } + } + return; + } + else if (p == q) + return; + else { + o = p; + p = q; + } + } + } + + /** + * Unlinks non-null last node. + */ + private void unlinkLast(Node last, Node prev) { + // assert last != null; + // assert prev != null; + // assert last.item == null; + for (Node o = null, p = prev, q;;) { + if (p.item != null || (q = p.prev) == null) { + if (o != null && p.next != p && last.casPrev(prev, p)) { + skipDeletedSuccessors(p); + if (last.next == null && + (p.prev == null || p.item != null) && + p.next == last) { + + updateHead(); // Ensure o is not reachable from head + updateTail(); // Ensure o is not reachable from tail + + // Finally, actually gc-unlink + o.lazySetPrev(o); + o.lazySetNext(nextTerminator()); + } + } + return; + } + else if (p == q) + return; + else { + o = p; + p = q; + } + } + } + + /** + * Guarantees that any node which was unlinked before a call to + * this method will be unreachable from head after it returns. + * Does not guarantee to eliminate slack, only that head will + * point to a node that was active while this method was running. + */ + private void updateHead() { + // Either head already points to an active node, or we keep + // trying to cas it to the first node until it does. + Node h, p, q; + restartFromHead: + while ((h = head).item == null && (p = h.prev) != null) { + for (;;) { + if ((q = p.prev) == null || + (q = (p = q).prev) == null) { + // It is possible that p is PREV_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + if (casHead(h, p)) + return; + else + continue restartFromHead; + } + else if (h != head) + continue restartFromHead; + else + p = q; + } + } + } + + /** + * Guarantees that any node which was unlinked before a call to + * this method will be unreachable from tail after it returns. + * Does not guarantee to eliminate slack, only that tail will + * point to a node that was active while this method was running. + */ + private void updateTail() { + // Either tail already points to an active node, or we keep + // trying to cas it to the last node until it does. + Node t, p, q; + restartFromTail: + while ((t = tail).item == null && (p = t.next) != null) { + for (;;) { + if ((q = p.next) == null || + (q = (p = q).next) == null) { + // It is possible that p is NEXT_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + if (casTail(t, p)) + return; + else + continue restartFromTail; + } + else if (t != tail) + continue restartFromTail; + else + p = q; + } + } + } + + private void skipDeletedPredecessors(Node x) { + whileActive: + do { + Node prev = x.prev; + // assert prev != null; + // assert x != NEXT_TERMINATOR; + // assert x != PREV_TERMINATOR; + Node p = prev; + findActive: + for (;;) { + if (p.item != null) + break findActive; + Node q = p.prev; + if (q == null) { + if (p.next == p) + continue whileActive; + break findActive; + } + else if (p == q) + continue whileActive; + else + p = q; + } + + // found active CAS target + if (prev == p || x.casPrev(prev, p)) + return; + + } while (x.item != null || x.next == null); + } + + private void skipDeletedSuccessors(Node x) { + whileActive: + do { + Node next = x.next; + // assert next != null; + // assert x != NEXT_TERMINATOR; + // assert x != PREV_TERMINATOR; + Node p = next; + findActive: + for (;;) { + if (p.item != null) + break findActive; + Node q = p.next; + if (q == null) { + if (p.prev == p) + continue whileActive; + break findActive; + } + else if (p == q) + continue whileActive; + else + p = q; + } + + // found active CAS target + if (next == p || x.casNext(next, p)) + return; + + } while (x.item != null || x.prev == null); + } + + /** + * Returns the successor of p, or the first node if p.next has been + * linked to self, which will only be true if traversing with a + * stale pointer that is now off the list. + */ + final Node succ(Node p) { + // TODO: should we skip deleted nodes here? + Node q = p.next; + return (p == q) ? first() : q; + } + + /** + * Returns the predecessor of p, or the last node if p.prev has been + * linked to self, which will only be true if traversing with a + * stale pointer that is now off the list. + */ + final Node pred(Node p) { + Node q = p.prev; + return (p == q) ? last() : q; + } + + /** + * Returns the first node, the unique node p for which: + * p.prev == null && p.next != p + * The returned node may or may not be logically deleted. + * Guarantees that head is set to the returned node. + */ + Node first() { + restartFromHead: + for (;;) + for (Node h = head, p = h, q;;) { + if ((q = p.prev) != null && + (q = (p = q).prev) != null) + // Check for head updates every other hop. + // If p == q, we are sure to follow head instead. + p = (h != (h = head)) ? h : q; + else if (p == h + // It is possible that p is PREV_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + || casHead(h, p)) + return p; + else + continue restartFromHead; + } + } + + /** + * Returns the last node, the unique node p for which: + * p.next == null && p.prev != p + * The returned node may or may not be logically deleted. + * Guarantees that tail is set to the returned node. + */ + Node last() { + restartFromTail: + for (;;) + for (Node t = tail, p = t, q;;) { + if ((q = p.next) != null && + (q = (p = q).next) != null) + // Check for tail updates every other hop. + // If p == q, we are sure to follow tail instead. + p = (t != (t = tail)) ? t : q; + else if (p == t + // It is possible that p is NEXT_TERMINATOR, + // but if so, the CAS is guaranteed to fail. + || casTail(t, p)) + return p; + else + continue restartFromTail; + } + } + + // Minor convenience utilities + + /** + * Throws NullPointerException if argument is null. + * + * @param v the element + */ + private static void checkNotNull(Object v) { + if (v == null) + throw new NullPointerException(); + } + + /** + * Returns element unless it is null, in which case throws + * NoSuchElementException. + * + * @param v the element + * @return the element + */ + private E screenNullResult(E v) { + if (v == null) + throw new NoSuchElementException(); + return v; + } + + /** + * Creates an array list and fills it with elements of this list. + * Used by toArray. + * + * @return the arrayList + */ + private ArrayList toArrayList() { + ArrayList list = new ArrayList<>(); + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null) + list.add(item); + } + return list; + } + + /** + * Constructs an empty deque. + */ + public PortableConcurrentDirectDeque() { + head = tail = new Node<>(null); + } + + /** + * Constructs a deque initially containing the elements of + * the given collection, added in traversal order of the + * collection's iterator. + * + * @param c the collection of elements to initially contain + * @throws NullPointerException if the specified collection or any + * of its elements are null + */ + public PortableConcurrentDirectDeque(Collection c) { + // Copy c into a private chain of Nodes + Node h = null, t = null; + for (E e : c) { + checkNotNull(e); + Node newNode = new Node<>(e); + if (h == null) + h = t = newNode; + else { + t.lazySetNext(newNode); + newNode.lazySetPrev(t); + t = newNode; + } + } + initHeadTail(h, t); + } + + /** + * Initializes head and tail, ensuring invariants hold. + */ + private void initHeadTail(Node h, Node t) { + if (h == t) { + if (h == null) + h = t = new Node<>(null); + else { + // Avoid edge case of a single Node with non-null item. + Node newNode = new Node<>(null); + t.lazySetNext(newNode); + newNode.lazySetPrev(t); + t = newNode; + } + } + head = h; + tail = t; + } + + /** + * Inserts the specified element at the front of this deque. + * As the deque is unbounded, this method will never throw + * {@link IllegalStateException}. + * + * @throws NullPointerException if the specified element is null + */ + public void addFirst(E e) { + linkFirst(e); + } + + /** + * Inserts the specified element at the end of this deque. + * As the deque is unbounded, this method will never throw + * {@link IllegalStateException}. + * + *

This method is equivalent to {@link #add}. + * + * @throws NullPointerException if the specified element is null + */ + public void addLast(E e) { + linkLast(e); + } + + /** + * Inserts the specified element at the front of this deque. + * As the deque is unbounded, this method will never return {@code false}. + * + * @return {@code true} (as specified by {@link java.util.Deque#offerFirst}) + * @throws NullPointerException if the specified element is null + */ + public boolean offerFirst(E e) { + linkFirst(e); + return true; + } + + public Object offerFirstAndReturnToken(E e) { + return linkFirst(e); + } + + public Object offerLastAndReturnToken(E e) { + return linkLast(e); + } + + public void removeToken(Object token) { + if (!(token instanceof Node)) { + throw new IllegalArgumentException(); + } + + Node node = (Node) (token); + while (! node.casItem(node.item, null)) {} + unlink(node); + } + + /** + * Inserts the specified element at the end of this deque. + * As the deque is unbounded, this method will never return {@code false}. + * + *

This method is equivalent to {@link #add}. + * + * @return {@code true} (as specified by {@link java.util.Deque#offerLast}) + * @throws NullPointerException if the specified element is null + */ + public boolean offerLast(E e) { + linkLast(e); + return true; + } + + public E peekFirst() { + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null) + return item; + } + return null; + } + + public E peekLast() { + for (Node p = last(); p != null; p = pred(p)) { + E item = p.item; + if (item != null) + return item; + } + return null; + } + + /** + * @throws java.util.NoSuchElementException {@inheritDoc} + */ + public E getFirst() { + return screenNullResult(peekFirst()); + } + + /** + * @throws java.util.NoSuchElementException {@inheritDoc} + */ + public E getLast() { + return screenNullResult(peekLast()); + } + + public E pollFirst() { + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null && p.casItem(item, null)) { + unlink(p); + return item; + } + } + return null; + } + + public E pollLast() { + for (Node p = last(); p != null; p = pred(p)) { + E item = p.item; + if (item != null && p.casItem(item, null)) { + unlink(p); + return item; + } + } + return null; + } + + /** + * @throws java.util.NoSuchElementException {@inheritDoc} + */ + public E removeFirst() { + return screenNullResult(pollFirst()); + } + + /** + * @throws java.util.NoSuchElementException {@inheritDoc} + */ + public E removeLast() { + return screenNullResult(pollLast()); + } + + // *** Queue and stack methods *** + + /** + * Inserts the specified element at the tail of this deque. + * As the deque is unbounded, this method will never return {@code false}. + * + * @return {@code true} (as specified by {@link java.util.Queue#offer}) + * @throws NullPointerException if the specified element is null + */ + public boolean offer(E e) { + return offerLast(e); + } + + /** + * Inserts the specified element at the tail of this deque. + * As the deque is unbounded, this method will never throw + * {@link IllegalStateException} or return {@code false}. + * + * @return {@code true} (as specified by {@link java.util.Collection#add}) + * @throws NullPointerException if the specified element is null + */ + public boolean add(E e) { + return offerLast(e); + } + + public E poll() { return pollFirst(); } + public E remove() { return removeFirst(); } + public E peek() { return peekFirst(); } + public E element() { return getFirst(); } + public void push(E e) { addFirst(e); } + public E pop() { return removeFirst(); } + + /** + * Removes the first element {@code e} such that + * {@code o.equals(e)}, if such an element exists in this deque. + * If the deque does not contain the element, it is unchanged. + * + * @param o element to be removed from this deque, if present + * @return {@code true} if the deque contained the specified element + * @throws NullPointerException if the specified element is null + */ + public boolean removeFirstOccurrence(Object o) { + checkNotNull(o); + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null && o.equals(item) && p.casItem(item, null)) { + unlink(p); + return true; + } + } + return false; + } + + /** + * Removes the last element {@code e} such that + * {@code o.equals(e)}, if such an element exists in this deque. + * If the deque does not contain the element, it is unchanged. + * + * @param o element to be removed from this deque, if present + * @return {@code true} if the deque contained the specified element + * @throws NullPointerException if the specified element is null + */ + public boolean removeLastOccurrence(Object o) { + checkNotNull(o); + for (Node p = last(); p != null; p = pred(p)) { + E item = p.item; + if (item != null && o.equals(item) && p.casItem(item, null)) { + unlink(p); + return true; + } + } + return false; + } + + /** + * Returns {@code true} if this deque contains at least one + * element {@code e} such that {@code o.equals(e)}. + * + * @param o element whose presence in this deque is to be tested + * @return {@code true} if this deque contains the specified element + */ + public boolean contains(Object o) { + if (o == null) return false; + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null && o.equals(item)) + return true; + } + return false; + } + + /** + * Returns {@code true} if this collection contains no elements. + * + * @return {@code true} if this collection contains no elements + */ + public boolean isEmpty() { + return peekFirst() == null; + } + + /** + * Returns the number of elements in this deque. If this deque + * contains more than {@code Integer.MAX_VALUE} elements, it + * returns {@code Integer.MAX_VALUE}. + * + *

Beware that, unlike in most collections, this method is + * NOT a constant-time operation. Because of the + * asynchronous nature of these deques, determining the current + * number of elements requires traversing them all to count them. + * Additionally, it is possible for the size to change during + * execution of this method, in which case the returned result + * will be inaccurate. Thus, this method is typically not very + * useful in concurrent applications. + * + * @return the number of elements in this deque + */ + public int size() { + int count = 0; + for (Node p = first(); p != null; p = succ(p)) + if (p.item != null) + // Collection.size() spec says to max out + if (++count == Integer.MAX_VALUE) + break; + return count; + } + + /** + * Removes the first element {@code e} such that + * {@code o.equals(e)}, if such an element exists in this deque. + * If the deque does not contain the element, it is unchanged. + * + * @param o element to be removed from this deque, if present + * @return {@code true} if the deque contained the specified element + * @throws NullPointerException if the specified element is null + */ + public boolean remove(Object o) { + return removeFirstOccurrence(o); + } + + /** + * Appends all of the elements in the specified collection to the end of + * this deque, in the order that they are returned by the specified + * collection's iterator. Attempts to {@code addAll} of a deque to + * itself result in {@code IllegalArgumentException}. + * + * @param c the elements to be inserted into this deque + * @return {@code true} if this deque changed as a result of the call + * @throws NullPointerException if the specified collection or any + * of its elements are null + * @throws IllegalArgumentException if the collection is this deque + */ + public boolean addAll(Collection c) { + if (c == this) + // As historically specified in AbstractQueue#addAll + throw new IllegalArgumentException(); + + // Copy c into a private chain of Nodes + Node beginningOfTheEnd = null, last = null; + for (E e : c) { + checkNotNull(e); + Node newNode = new Node<>(e); + if (beginningOfTheEnd == null) + beginningOfTheEnd = last = newNode; + else { + last.lazySetNext(newNode); + newNode.lazySetPrev(last); + last = newNode; + } + } + if (beginningOfTheEnd == null) + return false; + + // Atomically append the chain at the tail of this collection + restartFromTail: + for (;;) + for (Node t = tail, p = t, q;;) { + if ((q = p.next) != null && + (q = (p = q).next) != null) + // Check for tail updates every other hop. + // If p == q, we are sure to follow tail instead. + p = (t != (t = tail)) ? t : q; + else if (p.prev == p) // NEXT_TERMINATOR + continue restartFromTail; + else { + // p is last node + beginningOfTheEnd.lazySetPrev(p); // CAS piggyback + if (p.casNext(null, beginningOfTheEnd)) { + // Successful CAS is the linearization point + // for all elements to be added to this deque. + if (!casTail(t, last)) { + // Try a little harder to update tail, + // since we may be adding many elements. + t = tail; + if (last.next == null) + casTail(t, last); + } + return true; + } + // Lost CAS race to another thread; re-read next + } + } + } + + /** + * Removes all of the elements from this deque. + */ + public void clear() { + while (pollFirst() != null) { } + } + + /** + * Returns an array containing all of the elements in this deque, in + * proper sequence (from first to last element). + * + *

The returned array will be "safe" in that no references to it are + * maintained by this deque. (In other words, this method must allocate + * a new array). The caller is thus free to modify the returned array. + * + *

This method acts as bridge between array-based and collection-based + * APIs. + * + * @return an array containing all of the elements in this deque + */ + public Object[] toArray() { + return toArrayList().toArray(); + } + + /** + * Returns an array containing all of the elements in this deque, + * in proper sequence (from first to last element); the runtime + * type of the returned array is that of the specified array. If + * the deque fits in the specified array, it is returned therein. + * Otherwise, a new array is allocated with the runtime type of + * the specified array and the size of this deque. + * + *

If this deque fits in the specified array with room to spare + * (i.e., the array has more elements than this deque), the element in + * the array immediately following the end of the deque is set to + * {@code null}. + * + *

Like the {@link #toArray()} method, this method acts as + * bridge between array-based and collection-based APIs. Further, + * this method allows precise control over the runtime type of the + * output array, and may, under certain circumstances, be used to + * save allocation costs. + * + *

Suppose {@code x} is a deque known to contain only strings. + * The following code can be used to dump the deque into a newly + * allocated array of {@code String}: + * + *

 {@code String[] y = x.toArray(new String[0]);}
+ * + * Note that {@code toArray(new Object[0])} is identical in function to + * {@code toArray()}. + * + * @param a the array into which the elements of the deque are to + * be stored, if it is big enough; otherwise, a new array of the + * same runtime type is allocated for this purpose + * @return an array containing all of the elements in this deque + * @throws ArrayStoreException if the runtime type of the specified array + * is not a supertype of the runtime type of every element in + * this deque + * @throws NullPointerException if the specified array is null + */ + public T[] toArray(T[] a) { + return toArrayList().toArray(a); + } + + /** + * Returns an iterator over the elements in this deque in proper sequence. + * The elements will be returned in order from first (head) to last (tail). + * + *

The returned iterator is a "weakly consistent" iterator that + * will never throw {@link java.util.ConcurrentModificationException + * ConcurrentModificationException}, and guarantees to traverse + * elements as they existed upon construction of the iterator, and + * may (but is not guaranteed to) reflect any modifications + * subsequent to construction. + * + * @return an iterator over the elements in this deque in proper sequence + */ + public Iterator iterator() { + return new Itr(); + } + + /** + * Returns an iterator over the elements in this deque in reverse + * sequential order. The elements will be returned in order from + * last (tail) to first (head). + * + *

The returned iterator is a "weakly consistent" iterator that + * will never throw {@link java.util.ConcurrentModificationException + * ConcurrentModificationException}, and guarantees to traverse + * elements as they existed upon construction of the iterator, and + * may (but is not guaranteed to) reflect any modifications + * subsequent to construction. + * + * @return an iterator over the elements in this deque in reverse order + */ + public Iterator descendingIterator() { + return new DescendingItr(); + } + + private abstract class AbstractItr implements Iterator { + /** + * Next node to return item for. + */ + private Node nextNode; + + /** + * nextItem holds on to item fields because once we claim + * that an element exists in hasNext(), we must return it in + * the following next() call even if it was in the process of + * being removed when hasNext() was called. + */ + private E nextItem; + + /** + * Node returned by most recent call to next. Needed by remove. + * Reset to null if this element is deleted by a call to remove. + */ + private Node lastRet; + + abstract Node startNode(); + abstract Node nextNode(Node p); + + AbstractItr() { + advance(); + } + + /** + * Sets nextNode and nextItem to next valid node, or to null + * if no such. + */ + private void advance() { + lastRet = nextNode; + + Node p = (nextNode == null) ? startNode() : nextNode(nextNode); + for (;; p = nextNode(p)) { + if (p == null) { + // p might be active end or TERMINATOR node; both are OK + nextNode = null; + nextItem = null; + break; + } + E item = p.item; + if (item != null) { + nextNode = p; + nextItem = item; + break; + } + } + } + + public boolean hasNext() { + return nextItem != null; + } + + public E next() { + E item = nextItem; + if (item == null) throw new NoSuchElementException(); + advance(); + return item; + } + + public void remove() { + Node l = lastRet; + if (l == null) throw new IllegalStateException(); + l.item = null; + unlink(l); + lastRet = null; + } + } + + /** Forward iterator */ + private class Itr extends AbstractItr { + Node startNode() { return first(); } + Node nextNode(Node p) { return succ(p); } + } + + /** Descending iterator */ + private class DescendingItr extends AbstractItr { + Node startNode() { return last(); } + Node nextNode(Node p) { return pred(p); } + } + + /** + * Saves this deque to a stream (that is, serializes it). + * + * @serialData All of the elements (each an {@code E}) in + * the proper order, followed by a null + */ + private void writeObject(java.io.ObjectOutputStream s) + throws java.io.IOException { + + // Write out any hidden stuff + s.defaultWriteObject(); + + // Write out all elements in the proper order. + for (Node p = first(); p != null; p = succ(p)) { + E item = p.item; + if (item != null) + s.writeObject(item); + } + + // Use trailing null as sentinel + s.writeObject(null); + } + + /** + * Reconstitutes this deque from a stream (that is, deserializes it). + */ + private void readObject(java.io.ObjectInputStream s) + throws java.io.IOException, ClassNotFoundException { + s.defaultReadObject(); + + // Read in elements until trailing null sentinel found + Node h = null, t = null; + Object item; + while ((item = s.readObject()) != null) { + @SuppressWarnings("unchecked") + Node newNode = new Node<>((E) item); + if (h == null) + h = t = newNode; + else { + t.lazySetNext(newNode); + newNode.lazySetPrev(t); + t = newNode; + } + } + initHeadTail(h, t); + } + + private boolean casHead(Node cmp, Node val) { + return headUpdater.compareAndSet(this, cmp, val); + } + + private boolean casTail(Node cmp, Node val) { + return tailUpdater.compareAndSet(this, cmp, val); + } + + // Unsafe mechanics + + static { + PREV_TERMINATOR = new Node<>(); + PREV_TERMINATOR.next = PREV_TERMINATOR; + NEXT_TERMINATOR = new Node<>(); + NEXT_TERMINATOR.prev = NEXT_TERMINATOR; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/Protocols.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Protocols.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Protocols.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,54 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * Protocol version strings. + * + * @author David M. Lloyd + */ +public final class Protocols { + + private Protocols() { + } + + /** + * HTTP 0.9. + */ + public static final String HTTP_0_9_STRING = "HTTP/0.9"; + /** + * HTTP 1.0. + */ + public static final String HTTP_1_0_STRING = "HTTP/1.0"; + /** + * HTTP 1.1. + */ + public static final String HTTP_1_1_STRING = "HTTP/1.1"; + + + public static final HttpString HTTP_0_9 = new HttpString(HTTP_0_9_STRING); + /** + * HTTP 1.0. + */ + public static final HttpString HTTP_1_0 = new HttpString(HTTP_1_0_STRING); + /** + * HTTP 1.1. + */ + public static final HttpString HTTP_1_1 = new HttpString(HTTP_1_1_STRING); +} Index: 3rdParty_sources/undertow/io/undertow/util/QValueParser.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/QValueParser.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/QValueParser.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,212 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Utility class for parsing headers that accept q values + * + * @author Stuart Douglas + */ +public class QValueParser { + + private QValueParser() { + + } + + /** + * Parses a set of headers that take q values to determine the most preferred one. + * + * It returns the result in the form of a sorted list of list, with every element in + * the list having the same q value. This means the highest priority items are at the + * front of the list. The container should use its own internal preferred ordering + * to determinately pick the correct item to use + * + * @param headers The headers + * @return The q value results + */ + public static List> parse(List headers) { + final List found = new ArrayList<>(); + QValueResult current = null; + for (final String header : headers) { + final int l = header.length(); + //we do not use a string builder + //we just keep track of where the current string starts and call substring() + int stringStart = 0; + for (int i = 0; i < l; ++i) { + char c = header.charAt(i); + switch (c) { + case ',': { + if (current != null && + (i - stringStart > 2 && header.charAt(stringStart) == 'q' && + header.charAt(stringStart + 1) == '=')) { + //if this is a valid qvalue + current.qvalue = header.substring(stringStart + 2, i); + current = null; + } else if (stringStart != i) { + current = handleNewEncoding(found, header, stringStart, i); + } + stringStart = i + 1; + break; + } + case ';': { + if (stringStart != i) { + current = handleNewEncoding(found, header, stringStart, i); + stringStart = i + 1; + } + break; + } + case ' ': { + if (stringStart != i) { + if (current != null && + (i - stringStart > 2 && header.charAt(stringStart) == 'q' && + header.charAt(stringStart + 1) == '=')) { + //if this is a valid qvalue + current.qvalue = header.substring(stringStart + 2, i); + } else { + current = handleNewEncoding(found, header, stringStart, i); + } + } + stringStart = i + 1; + } + } + } + + if (stringStart != l) { + if (current != null && + (l - stringStart > 2 && header.charAt(stringStart) == 'q' && + header.charAt(stringStart + 1) == '=')) { + //if this is a valid qvalue + current.qvalue = header.substring(stringStart + 2, l); + } else { + current = handleNewEncoding(found, header, stringStart, l); + } + } + } + Collections.sort(found, Collections.reverseOrder()); + String currentQValue = null; + List> values = new ArrayList<>(); + List currentSet = null; + + for(QValueResult val : found) { + if(!val.qvalue.equals(currentQValue)) { + currentQValue = val.qvalue; + currentSet = new ArrayList<>(); + values.add(currentSet); + } + currentSet.add(val); + } + return values; + } + + private static QValueResult handleNewEncoding(final List found, final String header, final int stringStart, final int i) { + final QValueResult current = new QValueResult(); + current.value = header.substring(stringStart, i); + found.add(current); + return current; + } + + public static class QValueResult implements Comparable { + + + /** + * The string value of the result + */ + private String value; + + /** + * we keep the qvalue as a string to avoid parsing the double. + *

+ * This should give both performance and also possible security improvements + */ + private String qvalue = "1"; + + public String getValue() { + return value; + } + + public String getQvalue() { + return qvalue; + } + + @Override + public int compareTo(final QValueResult other) { + //we compare the strings as if they were decimal values. + //we know they can only be + + final String t = qvalue; + final String o = other.qvalue; + if (t == null && o == null) { + //neither of them has a q value + //we compare them via the server specified default precedence + //note that encoding is never null here, a * without a q value is meaningless + //and will be discarded before this + return 0; + } + + if (o == null) { + return 1; + } else if (t == null) { + return -1; + } + + final int tl = t.length(); + final int ol = o.length(); + //we only compare the first 5 characters as per spec + for (int i = 0; i < 5; ++i) { + if (tl == i || ol == i) { + return ol - tl; //longer one is higher + } + if (i == 1) continue; // this is just the decimal point + final int tc = t.charAt(i); + final int oc = o.charAt(i); + + int res = tc - oc; + if (res != 0) { + return res; + } + } + return 0; + } + + + public boolean isQValueZero() { + //we ignore * without a qvalue + if (qvalue != null) { + int length = Math.min(5, qvalue.length()); + //we need to find out if this is prohibiting identity + //encoding (q=0). Otherwise we just treat it as the identity encoding + boolean zero = true; + for (int j = 0; j < length; ++j) { + if (j == 1) continue;//decimal point + if (qvalue.charAt(j) != '0') { + zero = false; + break; + } + } + return zero; + } + return false; + } + + } +} Index: 3rdParty_sources/undertow/io/undertow/util/QueryParameterUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/QueryParameterUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/QueryParameterUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,170 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.LinkedHashMap; +import java.util.Map; +import org.xnio.OptionMap; + +import io.undertow.UndertowOptions; +import io.undertow.server.HttpServerExchange; + +/** + * Methods for dealing with the query string + * + * @author Stuart Douglas + */ +public class QueryParameterUtils { + + private QueryParameterUtils() { + + } + + public static String buildQueryString(final Map> params) { + StringBuilder sb = new StringBuilder(); + boolean first = true; + for (Map.Entry> entry : params.entrySet()) { + if (entry.getValue().isEmpty()) { + if (first) { + first = false; + } else { + sb.append('&'); + } + sb.append(entry.getKey()); + sb.append('='); + } else { + for (String val : entry.getValue()) { + if (first) { + first = false; + } else { + sb.append('&'); + } + sb.append(entry.getKey()); + sb.append('='); + sb.append(val); + } + } + } + return sb.toString(); + } + + /** + * Parses a query string into a map + * @param newQueryString The query string + * @return The map of key value parameters + */ + @Deprecated + public static Map> parseQueryString(final String newQueryString) { + return parseQueryString(newQueryString, null); + } + + /** + * Parses a query string into a map + * @param newQueryString The query string + * @return The map of key value parameters + */ + public static Map> parseQueryString(final String newQueryString, final String encoding) { + Map> newQueryParameters = new LinkedHashMap<>(); + int startPos = 0; + int equalPos = -1; + boolean needsDecode = false; + for(int i = 0; i < newQueryString.length(); ++i) { + char c = newQueryString.charAt(i); + if(c == '=' && equalPos == -1) { + equalPos = i; + } else if(c == '&') { + handleQueryParameter(newQueryString, newQueryParameters, startPos, equalPos, i, encoding, needsDecode); + needsDecode = false; + startPos = i + 1; + equalPos = -1; + } else if((c == '%' || c == '+') && encoding != null) { + needsDecode = true; + } + } + if(startPos != newQueryString.length()) { + handleQueryParameter(newQueryString, newQueryParameters, startPos, equalPos, newQueryString.length(), encoding, needsDecode); + } + return newQueryParameters; + } + + private static void handleQueryParameter(String newQueryString, Map> newQueryParameters, int startPos, int equalPos, int i, final String encoding, boolean needsDecode) { + String key; + String value = ""; + if(equalPos == -1) { + key = decodeParam(newQueryString, startPos, i, encoding, needsDecode); + } else { + key = decodeParam(newQueryString, startPos, equalPos, encoding, needsDecode); + value = decodeParam(newQueryString, equalPos + 1, i, encoding, needsDecode); + } + + Deque queue = newQueryParameters.get(key); + if (queue == null) { + newQueryParameters.put(key, queue = new ArrayDeque<>(1)); + } + if(value != null) { + queue.add(value); + } + } + + private static String decodeParam(String newQueryString, int startPos, int equalPos, String encoding, boolean needsDecode) { + String key; + if (needsDecode) { + try { + key = URLDecoder.decode(newQueryString.substring(startPos, equalPos), encoding); + } catch (UnsupportedEncodingException e) { + key = newQueryString.substring(startPos, equalPos); + } + } else { + key = newQueryString.substring(startPos, equalPos); + } + return key; + } + + @Deprecated + public static Map> mergeQueryParametersWithNewQueryString(final Map> queryParameters, final String newQueryString) { + return mergeQueryParametersWithNewQueryString(queryParameters, newQueryString, "UTF-8"); + } + + public static Map> mergeQueryParametersWithNewQueryString(final Map> queryParameters, final String newQueryString, final String encoding) { + + Map> newQueryParameters = parseQueryString(newQueryString, encoding); + //according to the spec the new query parameters have to 'take precedence' + for (Map.Entry> entry : queryParameters.entrySet()) { + if (!newQueryParameters.containsKey(entry.getKey())) { + newQueryParameters.put(entry.getKey(), new ArrayDeque<>(entry.getValue())); + } else { + newQueryParameters.get(entry.getKey()).addAll(entry.getValue()); + } + } + return newQueryParameters; + } + + public static String getQueryParamEncoding(HttpServerExchange exchange) { + String encoding = null; + OptionMap undertowOptions = exchange.getConnection().getUndertowOptions(); + if(undertowOptions.get(UndertowOptions.DECODE_URL, true)) { + encoding = undertowOptions.get(UndertowOptions.URL_CHARSET, "UTF-8"); + } + return encoding; + } +} Index: 3rdParty_sources/undertow/io/undertow/util/RedirectBuilder.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/RedirectBuilder.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/RedirectBuilder.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,162 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.server.HttpServerExchange; + +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; +import java.util.Deque; +import java.util.Map; + +/** + * Utility class for building redirects. + * + * @author Stuart Douglas + */ +public class RedirectBuilder { + + public static final String UTF_8 = "UTF-8"; + + /** + * Redirects to a new relative path. All other data from the exchange is preserved. + * + * @param exchange The HTTP server exchange + * @param newRelativePath The new relative path + * @return + */ + public static String redirect(final HttpServerExchange exchange, final String newRelativePath) { + return redirect(exchange, newRelativePath, true); + } + + /** + * Redirects to a new relative path. All other data from the exchange is preserved. + * + * @param exchange The HTTP server exchange + * @param newRelativePath The new relative path + * @param includeParameters If query and path parameters from the exchange should be included + * @return + */ + public static String redirect(final HttpServerExchange exchange, final String newRelativePath, final boolean includeParameters) { + try { + StringBuilder uri = new StringBuilder(exchange.getRequestScheme()); + uri.append("://"); + uri.append(exchange.getHostAndPort()); + uri.append(encodeUrlPart(exchange.getResolvedPath())); + if (exchange.getResolvedPath().endsWith("/")) { + if (newRelativePath.startsWith("/")) { + uri.append(encodeUrlPart(newRelativePath.substring(1))); + } else { + uri.append(encodeUrlPart(newRelativePath)); + } + } else { + if (!newRelativePath.startsWith("/")) { + uri.append('/'); + } + uri.append(encodeUrlPart(newRelativePath)); + } + if (includeParameters) { + if (!exchange.getPathParameters().isEmpty()) { + boolean first = true; + uri.append(';'); + for (Map.Entry> param : exchange.getPathParameters().entrySet()) { + for (String value : param.getValue()) { + if (first) { + first = false; + } else { + uri.append('&'); + } + uri.append(URLEncoder.encode(param.getKey(), UTF_8)); + uri.append('='); + uri.append(URLEncoder.encode(value, UTF_8)); + } + } + } + if (!exchange.getQueryString().isEmpty()) { + uri.append('?'); + uri.append(exchange.getQueryString()); + } + } + return uri.toString(); + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + + /** + * perform URL encoding + *

+ * TODO: this whole thing is kinda crappy. + * + * @return + */ + private static String encodeUrlPart(final String part) throws UnsupportedEncodingException { + //we need to go through and check part by part that a section does not need encoding + + int pos = 0; + for (int i = 0; i < part.length(); ++i) { + char c = part.charAt(i); + if(c == '?') { + break; + } else if (c == '/') { + if (pos != i) { + String original = part.substring(pos, i - 1); + String encoded = URLEncoder.encode(original, UTF_8); + if (!encoded.equals(original)) { + return realEncode(part, pos); + } + } + pos = i + 1; + } else if (c == ' ') { + return realEncode(part, pos); + } + } + return part; + } + + private static String realEncode(String part, int startPos) throws UnsupportedEncodingException { + StringBuilder sb = new StringBuilder(); + sb.append(part.substring(0, startPos)); + int pos = startPos; + for (int i = startPos; i < part.length(); ++i) { + char c = part.charAt(i); + if(c == '?') { + break; + } else if (c == '/') { + if (pos != i) { + String original = part.substring(pos, i - 1); + String encoded = URLEncoder.encode(original, UTF_8); + sb.append(encoded); + sb.append('/'); + pos = i + 1; + } + } + } + + String original = part.substring(pos); + String encoded = URLEncoder.encode(original, UTF_8); + sb.append(encoded); + return sb.toString(); + } + + private RedirectBuilder() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/util/ReferenceCountedPooled.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/ReferenceCountedPooled.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/ReferenceCountedPooled.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,130 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowMessages; +import org.xnio.Pooled; + +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +/** + * @author Stuart Douglas + */ +public class ReferenceCountedPooled implements Pooled { + + private final Pooled underlying; + @SuppressWarnings("unused") + private volatile int referenceCount; + private volatile boolean discard = false; + boolean mainFreed = false; + + private static final AtomicIntegerFieldUpdater referenceCountUpdater = AtomicIntegerFieldUpdater.newUpdater(ReferenceCountedPooled.class, "referenceCount"); + + public ReferenceCountedPooled(Pooled underlying, int referenceCount) { + this.underlying = underlying; + this.referenceCount = referenceCount; + } + + @Override + public void discard() { + this.discard = true; + if(referenceCountUpdater.decrementAndGet(this) == 0) { + underlying.discard(); + } + + } + + @Override + public void free() { + if(mainFreed) { + throw UndertowMessages.MESSAGES.bufferAlreadyFreed(); + } + mainFreed = true; + freeInternal(); + } + public void freeInternal() { + if(referenceCountUpdater.decrementAndGet(this) == 0) { + if(discard) { + underlying.discard(); + } else { + underlying.free(); + } + } + } + + @Override + public T getResource() throws IllegalStateException { + return underlying.getResource(); + } + + @Override + public void close() { + free(); + } + + public Pooled createView(final T newValue) { + increaseReferenceCount(); + return new Pooled() { + + boolean free = false; + + @Override + public void discard() { + if(!free) { + free = true; + ReferenceCountedPooled.this.freeInternal(); + } + } + + @Override + public void free() { + //make sure that a given view can only be freed once + if(!free) { + free = true; + ReferenceCountedPooled.this.freeInternal(); + } + } + + @Override + public T getResource() throws IllegalStateException { + if(free) { + throw UndertowMessages.MESSAGES.bufferAlreadyFreed(); + } + return newValue; + } + + @Override + public void close() { + free(); + } + }; + } + + public void increaseReferenceCount() { + int val; + do { + val = referenceCountUpdater.get(this); + if(val == 0) { + //should never happen, as this should only be called from + //code that already has a reference + throw UndertowMessages.MESSAGES.objectWasFreed(); + } + } while (!referenceCountUpdater.compareAndSet(this, val, val + 1)); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/SameThreadExecutor.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/SameThreadExecutor.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/SameThreadExecutor.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,37 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.concurrent.Executor; + +/** + * @author Stuart Douglas + */ +public class SameThreadExecutor implements Executor { + + public static final Executor INSTANCE = new SameThreadExecutor(); + + private SameThreadExecutor() { + } + + @Override + public void execute(final Runnable command) { + command.run(); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/SecureHashMap.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/SecureHashMap.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/SecureHashMap.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,1116 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; +import java.util.concurrent.atomic.AtomicReferenceArray; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +/** + * Lock-free secure concurrent hash map. Attempts to store keys which cause excessive collisions will result in + * a security exception. + * + * @param the key type + * @param the value type + * + * @author David M. Lloyd + */ +public final class SecureHashMap extends AbstractMap implements ConcurrentMap { + private static final int MAX_ROW_LENGTH = 32; + private static final int DEFAULT_INITIAL_CAPACITY = 16; + private static final int MAXIMUM_CAPACITY = 1 << 30; + private static final float DEFAULT_LOAD_FACTOR = 0.60f; + + /** A row which has been resized into the new view. */ + private static final Item[] RESIZED = new Item[0]; + /** A non-existent table entry (as opposed to a {@code null} value). */ + private static final Object NONEXISTENT = new Object(); + + private volatile Table table; + + private final Set keySet = new KeySet(); + private final Set> entrySet = new EntrySet(); + private final Collection values = new Values(); + + private final float loadFactor; + private final int initialCapacity; + + @SuppressWarnings("unchecked") + private static final AtomicIntegerFieldUpdater sizeUpdater = AtomicIntegerFieldUpdater.newUpdater(Table.class, "size"); + + @SuppressWarnings("unchecked") + private static final AtomicReferenceFieldUpdater tableUpdater = AtomicReferenceFieldUpdater.newUpdater(SecureHashMap.class, Table.class, "table"); + @SuppressWarnings("unchecked") + private static final AtomicReferenceFieldUpdater valueUpdater = AtomicReferenceFieldUpdater.newUpdater(Item.class, Object.class, "value"); + + /** + * Construct a new instance. + * + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + */ + public SecureHashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) { + throw new IllegalArgumentException("Initial capacity must be > 0"); + } + if (initialCapacity > MAXIMUM_CAPACITY) { + initialCapacity = MAXIMUM_CAPACITY; + } + if (loadFactor <= 0.0 || Float.isNaN(loadFactor) || loadFactor >= 1.0) { + throw new IllegalArgumentException("Load factor must be between 0.0f and 1.0f"); + } + + int capacity = 1; + + while (capacity < initialCapacity) { + capacity <<= 1; + } + + this.loadFactor = loadFactor; + this.initialCapacity = capacity; + + final Table table = new Table<>(capacity, loadFactor); + tableUpdater.set(this, table); + } + + /** + * Construct a new instance. + * + * @param loadFactor the load factor + */ + public SecureHashMap(final float loadFactor) { + this(DEFAULT_INITIAL_CAPACITY, loadFactor); + } + + /** + * Construct a new instance. + * + * @param initialCapacity the initial capacity + */ + public SecureHashMap(final int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } + + /** + * Construct a new instance. + */ + public SecureHashMap() { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR); + } + + private static int hashOf(final Object key) { + int h = key.hashCode(); + h += (h << 15) ^ 0xffffcd7d; + h ^= (h >>> 10); + h += (h << 3); + h ^= (h >>> 6); + h += (h << 2) + (h << 14); + return h ^ (h >>> 16); + } + + private static boolean equals(final Object o1, final Object o2) { + return o1 == null ? o2 == null : o1.equals(o2); + } + + private Item[] addItem(final Item[] row, final Item newItem) { + if (row == null) { + return createRow(newItem); + } else { + final int length = row.length; + if (length > MAX_ROW_LENGTH) { + throw new SecurityException("Excessive map collisions"); + } + Item[] newRow = Arrays.copyOf(row, length + 1); + newRow[length] = newItem; + return newRow; + } + } + + @SuppressWarnings("unchecked") + private static Item[] createRow(final Item newItem) { + return new Item[] { newItem }; + } + + @SuppressWarnings("unchecked") + private static Item[] createRow(final int length) { + return new Item[length]; + } + + private V doPut(K key, V value, boolean ifAbsent, Table table) { + final int hashCode = hashOf(key); + final AtomicReferenceArray[]> array = table.array; + final int idx = hashCode & array.length() - 1; + + OUTER: for (;;) { + + // Fetch the table row. + Item[] oldRow = array.get(idx); + if (oldRow == RESIZED) { + // row was transported to the new table so recalculate everything + final V result = doPut(key, value, ifAbsent, table.resizeView); + // keep a consistent size view though! + if (result == NONEXISTENT) sizeUpdater.getAndIncrement(table); + return result; + } + if (oldRow != null) { + // Find the matching Item in the row. + Item oldItem = null; + for (Item tryItem : oldRow) { + if (equals(key, tryItem.key)) { + oldItem = tryItem; + break; + } + } + if (oldItem != null) { + // entry exists; try to return the old value and try to replace the value if allowed. + V oldItemValue; + do { + oldItemValue = oldItem.value; + if (oldItemValue == NONEXISTENT) { + // Key was removed; on the next iteration or two the doornail should be gone. + continue OUTER; + } + } while (! ifAbsent && ! valueUpdater.compareAndSet(oldItem, oldItemValue, value)); + return oldItemValue; + } + // Row exists but item doesn't. + } + + // Row doesn't exist, or row exists but item doesn't; try and add a new item to the row. + final Item newItem = new Item<>(key, hashCode, value); + final Item[] newRow = addItem(oldRow, newItem); + if (! array.compareAndSet(idx, oldRow, newRow)) { + // Nope, row changed; retry. + continue; + } + + // Up the table size. + final int threshold = table.threshold; + int newSize = sizeUpdater.incrementAndGet(table); + // >= 0 is really a sign-bit check + while (newSize >= 0 && (newSize & 0x7fffffff) > threshold) { + if (sizeUpdater.compareAndSet(table, newSize, newSize | 0x80000000)) { + resize(table); + return nonexistent(); + } + newSize = sizeUpdater.get(table); + } + // Success. + return nonexistent(); + } + } + + private void resize(Table origTable) { + final AtomicReferenceArray[]> origArray = origTable.array; + final int origCapacity = origArray.length(); + final Table newTable = new Table<>(origCapacity << 1, loadFactor); + // Prevent resize until we're done... + newTable.size = 0x80000000; + origTable.resizeView = newTable; + final AtomicReferenceArray[]> newArray = newTable.array; + + for (int i = 0; i < origCapacity; i ++) { + // for each row, try to resize into two new rows + Item[] origRow, newRow0, newRow1; + do { + origRow = origArray.get(i); + if (origRow != null) { + int count0 = 0, count1 = 0; + for (Item item : origRow) { + if ((item.hashCode & origCapacity) == 0) { + count0++; + } else { + count1++; + } + } + if (count0 != 0) { + newRow0 = createRow(count0); + int j = 0; + for (Item item : origRow) { + if ((item.hashCode & origCapacity) == 0) { + newRow0[j++] = item; + } + } + newArray.lazySet(i, newRow0); + } + if (count1 != 0) { + newRow1 = createRow(count1); + int j = 0; + for (Item item : origRow) { + if ((item.hashCode & origCapacity) != 0) { + newRow1[j++] = item; + } + } + newArray.lazySet(i + origCapacity, newRow1); + } + } + } while (! origArray.compareAndSet(i, origRow, SecureHashMap.resized())); + if (origRow != null) sizeUpdater.getAndAdd(newTable, origRow.length); + } + + int size; + do { + size = newTable.size; + if ((size & 0x7fffffff) >= newTable.threshold) { + // shorter path for reads and writes + table = newTable; + // then time for another resize, right away + resize(newTable); + return; + } + } while (!sizeUpdater.compareAndSet(newTable, size, size & 0x7fffffff)); + + // All done, plug in the new table + table = newTable; + } + + private static Item[] remove(Item[] row, int idx) { + final int len = row.length; + assert idx < len; + if (len == 1) { + return null; + } + @SuppressWarnings("unchecked") + Item[] newRow = new Item[len - 1]; + if (idx > 0) { + System.arraycopy(row, 0, newRow, 0, idx); + } + if (idx < len - 1) { + System.arraycopy(row, idx + 1, newRow, idx, len - 1 - idx); + } + return newRow; + } + + public V putIfAbsent(final K key, final V value) { + final V result = doPut(key, value, true, table); + return result == NONEXISTENT ? null : result; + } + + public boolean remove(final Object objectKey, final Object objectValue) { + // Get type-safe key and value. + @SuppressWarnings("unchecked") + final K key = (K) objectKey; + @SuppressWarnings("unchecked") + final V value = (V) objectValue; + return doRemove(key, value, table); + } + + private boolean doRemove(final Item item, final Table table) { + int hashCode = item.hashCode; + + final AtomicReferenceArray[]> array = table.array; + final int idx = hashCode & array.length() - 1; + + Item[] oldRow; + + for (;;) { + oldRow = array.get(idx); + if (oldRow == null) { + return false; + } + if (oldRow == RESIZED) { + boolean result; + if (result = doRemove(item, table.resizeView)) { + sizeUpdater.getAndDecrement(table); + } + return result; + } + + int rowIdx = -1; + for (int i = 0; i < oldRow.length; i ++) { + if (item == oldRow[i]) { + rowIdx = i; + break; + } + } + if (rowIdx == -1) { + return false; + } + if (array.compareAndSet(idx, oldRow, remove(oldRow, rowIdx))) { + sizeUpdater.getAndDecrement(table); + return true; + } + // row changed, cycle back again + } + } + + private boolean doRemove(final K key, final V value, final Table table) { + + final int hashCode = hashOf(key); + + final AtomicReferenceArray[]> array = table.array; + final int idx = hashCode & array.length() - 1; + + Item[] oldRow; + + // Fetch the table row. + oldRow = array.get(idx); + if (oldRow == null) { + // no match for the key + return false; + } + if (oldRow == RESIZED) { + boolean result; + if (result = doRemove(key, value, table.resizeView)) { + // keep size consistent + sizeUpdater.getAndDecrement(table); + } + return result; + } + + // Find the matching Item in the row. + Item oldItem = null; + V oldValue = null; + int rowIdx = -1; + for (int i = 0; i < oldRow.length; i ++) { + Item tryItem = oldRow[i]; + if (equals(key, tryItem.key)) { + if (equals(value, oldValue = tryItem.value)) { + oldItem = tryItem; + rowIdx = i; + break; + } else { + // value doesn't match; exit without changing map. + return false; + } + } + } + if (oldItem == null) { + // no such entry exists. + return false; + } + + while (! valueUpdater.compareAndSet(oldItem, oldValue, NONEXISTENT)) { + if (equals(value, oldValue = oldItem.value)) { + // Values are equal; try marking it as removed again. + continue; + } + // Value was changed to a non-equal value. + return false; + } + + // Now we are free to remove the item from the row. + if (array.compareAndSet(idx, oldRow, remove(oldRow, rowIdx))) { + // Adjust the table size, since we are definitely the ones to be removing this item from the table. + sizeUpdater.decrementAndGet(table); + return true; + } else { + // The old row changed so retry by the other algorithm + return doRemove(oldItem, table); + } + } + + @SuppressWarnings("unchecked") + public V remove(final Object objectKey) { + final V result = doRemove((K) objectKey, table); + return result == NONEXISTENT ? null : result; + } + + private V doRemove(final K key, final Table table) { + final int hashCode = hashOf(key); + + final AtomicReferenceArray[]> array = table.array; + final int idx = hashCode & array.length() - 1; + + // Fetch the table row. + Item[] oldRow = array.get(idx); + if (oldRow == null) { + // no match for the key + return nonexistent(); + } + if (oldRow == RESIZED) { + V result; + if ((result = doRemove(key, table.resizeView)) != NONEXISTENT) { + // keep size consistent + sizeUpdater.getAndDecrement(table); + } + return result; + } + + // Find the matching Item in the row. + Item oldItem = null; + int rowIdx = -1; + for (int i = 0; i < oldRow.length; i ++) { + Item tryItem = oldRow[i]; + if (equals(key, tryItem.key)) { + oldItem = tryItem; + rowIdx = i; + break; + } + } + if (oldItem == null) { + // no such entry exists. + return nonexistent(); + } + + // Mark the item as "removed". + @SuppressWarnings("unchecked") + V oldValue = (V) valueUpdater.getAndSet(oldItem, NONEXISTENT); + if (oldValue == NONEXISTENT) { + // Someone else beat us to it. + return nonexistent(); + } + + // Now we are free to remove the item from the row. + if (array.compareAndSet(idx, oldRow, remove(oldRow, rowIdx))) { + // Adjust the table size, since we are definitely the ones to be removing this item from the table. + sizeUpdater.decrementAndGet(table); + + // Item is removed from the row; we are done here. + return oldValue; + } else { + boolean result = doRemove(oldItem, table); + assert result; + return oldValue; + } + } + + @SuppressWarnings("unchecked") + private static V nonexistent() { + return (V) NONEXISTENT; + } + + @SuppressWarnings("unchecked") + private static Item[] resized() { + return (Item[]) RESIZED; + } + + public boolean replace(final K key, final V oldValue, final V newValue) { + return doReplace(key, oldValue, newValue, table); + } + + private boolean doReplace(final K key, final V oldValue, final V newValue, final Table table) { + final int hashCode = hashOf(key); + final AtomicReferenceArray[]> array = table.array; + final int idx = hashCode & array.length() - 1; + + // Fetch the table row. + Item[] oldRow = array.get(idx); + if (oldRow == null) { + // no match for the key + return false; + } + if (oldRow == RESIZED) { + return doReplace(key, oldValue, newValue, table.resizeView); + } + + // Find the matching Item in the row. + Item oldItem = null; + V oldRowValue = null; + for (Item tryItem : oldRow) { + if (equals(key, tryItem.key)) { + if (equals(oldValue, oldRowValue = tryItem.value)) { + oldItem = tryItem; + break; + } else { + // value doesn't match; exit without changing map. + return false; + } + } + } + if (oldItem == null) { + // no such entry exists. + return false; + } + + // Now swap the item. + while (! valueUpdater.compareAndSet(oldItem, oldRowValue, newValue)) { + if (equals(oldValue, oldRowValue = oldItem.value)) { + // Values are equal; try swapping it again. + continue; + } + // Value was changed to a non-equal value. + return false; + } + + // Item is swapped; we are done here. + return true; + } + + public V replace(final K key, final V value) { + final V result = doReplace(key, value, table); + return result == NONEXISTENT ? null : result; + } + + private V doReplace(final K key, final V value, final Table table) { + final int hashCode = hashOf(key); + final AtomicReferenceArray[]> array = table.array; + final int idx = hashCode & array.length() - 1; + + // Fetch the table row. + Item[] oldRow = array.get(idx); + if (oldRow == null) { + // no match for the key + return nonexistent(); + } + if (oldRow == RESIZED) { + return doReplace(key, value, table.resizeView); + } + + // Find the matching Item in the row. + Item oldItem = null; + for (Item tryItem : oldRow) { + if (equals(key, tryItem.key)) { + oldItem = tryItem; + break; + } + } + if (oldItem == null) { + // no such entry exists. + return nonexistent(); + } + + // Now swap the item. + @SuppressWarnings("unchecked") + V oldRowValue = (V) valueUpdater.getAndSet(oldItem, value); + if (oldRowValue == NONEXISTENT) { + // Item was removed. + return nonexistent(); + } + + // Item is swapped; we are done here. + return oldRowValue; + } + + public int size() { + return table.size & 0x7fffffff; + } + + private V doGet(final Table table, final K key) { + final AtomicReferenceArray[]> array = table.array; + final Item[] row = array.get(hashOf(key) & (array.length() - 1)); + if(row != null) { + for (Item item : row) { + if (equals(key, item.key)) { + return item.value; + } + } + } + return nonexistent(); + } + + public boolean containsKey(final Object key) { + @SuppressWarnings("unchecked") + final V value = doGet(table, (K) key); + return value != NONEXISTENT; + } + + public V get(final Object key) { + @SuppressWarnings("unchecked") + final V value = doGet(table, (K) key); + return value == NONEXISTENT ? null : value; + } + + public V put(final K key, final V value) { + final V result = doPut(key, value, false, table); + return result == NONEXISTENT ? null : result; + } + + public void clear() { + table = new Table<>(initialCapacity, loadFactor); + } + + public Set> entrySet() { + return entrySet; + } + + public Collection values() { + return values; + } + + public Set keySet() { + return keySet; + } + + final class KeySet extends AbstractSet implements Set { + + public void clear() { + SecureHashMap.this.clear(); + } + + public boolean contains(final Object o) { + return containsKey(o); + } + + @SuppressWarnings("unchecked") + public boolean remove(final Object o) { + return doRemove((K) o, table) != NONEXISTENT; + } + + public Iterator iterator() { + return new KeyIterator(); + } + + public Object[] toArray() { + ArrayList list = new ArrayList<>(size()); + list.addAll(this); + return list.toArray(); + } + + public T[] toArray(final T[] a) { + ArrayList list = new ArrayList<>(); + list.addAll((Collection) this); + return list.toArray(a); + } + + public boolean add(final K k) { + return doPut(k, null, true, table) == NONEXISTENT; + } + + public int size() { + return SecureHashMap.this.size(); + } + } + + final class Values extends AbstractCollection implements Collection { + + public void clear() { + SecureHashMap.this.clear(); + } + + public boolean contains(final Object o) { + return containsValue(o); + } + + public Iterator iterator() { + return new ValueIterator(); + } + + public Object[] toArray() { + ArrayList list = new ArrayList<>(size()); + list.addAll(this); + return list.toArray(); + } + + public T[] toArray(final T[] a) { + ArrayList list = new ArrayList<>(); + list.addAll((Collection) this); + return list.toArray(a); + } + + public int size() { + return SecureHashMap.this.size(); + } + } + + final class EntrySet extends AbstractSet> implements Set> { + + public Iterator> iterator() { + return new EntryIterator(); + } + + public boolean add(final Entry entry) { + return doPut(entry.getKey(), entry.getValue(), true, table) == NONEXISTENT; + } + + @SuppressWarnings("unchecked") + public boolean remove(final Object o) { + return o instanceof Entry && remove((Entry) o); + } + + public boolean remove(final Entry entry) { + return doRemove(entry.getKey(), entry.getValue(), table); + } + + public void clear() { + SecureHashMap.this.clear(); + } + + public Object[] toArray() { + ArrayList list = new ArrayList<>(size()); + list.addAll(this); + return list.toArray(); + } + + public T[] toArray(final T[] a) { + ArrayList list = new ArrayList<>(); + list.addAll((Set) this); + return list.toArray(a); + } + + @SuppressWarnings("unchecked") + public boolean contains(final Object o) { + return o instanceof Entry && contains((Entry) o); + } + + public boolean contains(final Entry entry) { + final V tableValue = doGet(table, entry.getKey()); + final V entryValue = entry.getValue(); + return tableValue == null ? entryValue == null : tableValue.equals(entryValue); + } + + public int size() { + return SecureHashMap.this.size(); + } + } + + abstract class TableIterator implements Iterator> { + public abstract Item next(); + + abstract V nextValue(); + } + + final class RowIterator extends TableIterator { + private final Table table; + final Item[] row; + + private int idx; + private Item next; + private Item remove; + + RowIterator(final Table table, final Item[] row) { + this.table = table; + this.row = row; + } + + public boolean hasNext() { + if (next == null) { + final Item[] row = this.row; + if (row == null || idx == row.length) { + return false; + } + next = row[idx++]; + } + return true; + } + + V nextValue() { + V value; + do { + if (next == null) { + final Item[] row = this.row; + if (row == null || idx == row.length) { + return nonexistent(); + } + next = row[idx++]; + } + value = next.value; + } while (value == NONEXISTENT); + next = null; + return value; + } + + public Item next() { + if (hasNext()) try { + return next; + } finally { + remove = next; + next = null; + } + throw new NoSuchElementException(); + } + + public void remove() { + final Item remove = this.remove; + if (remove == null) { + throw new IllegalStateException("next() not yet called"); + } + if (valueUpdater.getAndSet(remove, NONEXISTENT) == NONEXISTENT) { + // someone else beat us to it; this is idempotent-ish + return; + } + // item guaranteed to be in the map... somewhere + this.remove = null; + doRemove(remove, table); + } + } + + final class BranchIterator extends TableIterator { + private final TableIterator branch0; + private final TableIterator branch1; + + private boolean branch; + + BranchIterator(final TableIterator branch0, final TableIterator branch1) { + this.branch0 = branch0; + this.branch1 = branch1; + } + + public boolean hasNext() { + return branch0.hasNext() || branch1.hasNext(); + } + + public Item next() { + if (branch) { + return branch1.next(); + } + if (branch0.hasNext()) { + return branch0.next(); + } + branch = true; + return branch1.next(); + } + + V nextValue() { + if (branch) { + return branch1.nextValue(); + } + V value = branch0.nextValue(); + if (value != NONEXISTENT) { + return value; + } + branch = true; + return branch1.nextValue(); + } + + public void remove() { + if (branch) { + branch0.remove(); + } else { + branch1.remove(); + } + } + } + + private TableIterator createRowIterator(Table table, int rowIdx) { + final AtomicReferenceArray[]> array = table.array; + final Item[] row = array.get(rowIdx); + if (row == RESIZED) { + final Table resizeView = table.resizeView; + return new BranchIterator(createRowIterator(resizeView, rowIdx), createRowIterator(resizeView, rowIdx + array.length())); + } else { + return new RowIterator(table, row); + } + } + + final class EntryIterator implements Iterator> { + private final Table table = SecureHashMap.this.table; + private TableIterator tableIterator; + private TableIterator removeIterator; + private int tableIdx; + private Item next; + + public boolean hasNext() { + while (next == null) { + if (tableIdx == table.array.length()) { + return false; + } + if (tableIterator == null) { + int rowIdx = tableIdx++; + if (table.array.get(rowIdx) != null) { + tableIterator = createRowIterator(table, rowIdx); + } + } + if (tableIterator != null) { + if (tableIterator.hasNext()) { + next = tableIterator.next(); + return true; + } else { + tableIterator = null; + } + } + } + return true; + } + + public Entry next() { + if (hasNext()) try { + return next; + } finally { + removeIterator = tableIterator; + next = null; + } + throw new NoSuchElementException(); + } + + public void remove() { + final TableIterator removeIterator = this.removeIterator; + if (removeIterator == null) { + throw new IllegalStateException(); + } else try { + removeIterator.remove(); + } finally { + this.removeIterator = null; + } + } + } + + final class KeyIterator implements Iterator { + private final Table table = SecureHashMap.this.table; + private TableIterator tableIterator; + private TableIterator removeIterator; + private int tableIdx; + private Item next; + + public boolean hasNext() { + while (next == null) { + if (tableIdx == table.array.length() && tableIterator == null) { + return false; + } + if (tableIterator == null) { + int rowIdx = tableIdx++; + if (table.array.get(rowIdx) != null) { + tableIterator = createRowIterator(table, rowIdx); + } + } + if (tableIterator != null) { + if (tableIterator.hasNext()) { + next = tableIterator.next(); + return true; + } else { + tableIterator = null; + } + } + } + return true; + } + + public K next() { + if (hasNext()) try { + return next.key; + } finally { + removeIterator = tableIterator; + next = null; + } + throw new NoSuchElementException(); + } + + public void remove() { + final TableIterator removeIterator = this.removeIterator; + if (removeIterator == null) { + throw new IllegalStateException(); + } else try { + removeIterator.remove(); + } finally { + this.removeIterator = null; + } + } + } + + final class ValueIterator implements Iterator { + private final Table table = SecureHashMap.this.table; + private TableIterator tableIterator; + private TableIterator removeIterator; + private int tableIdx; + private V next = nonexistent(); + + public boolean hasNext() { + while (next == NONEXISTENT) { + if (tableIdx == table.array.length() && tableIterator == null) { + return false; + } + if (tableIterator == null) { + int rowIdx = tableIdx++; + if (table.array.get(rowIdx) != null) { + tableIterator = createRowIterator(table, rowIdx); + } + } + if (tableIterator != null) { + next = tableIterator.nextValue(); + if (next == NONEXISTENT) { + tableIterator = null; + } + } + } + return true; + } + + public V next() { + if (hasNext()) try { + return next; + } finally { + removeIterator = tableIterator; + next = nonexistent(); + } + throw new NoSuchElementException(); + } + + public void remove() { + final TableIterator removeIterator = this.removeIterator; + if (removeIterator == null) { + throw new IllegalStateException(); + } else try { + removeIterator.remove(); + } finally { + this.removeIterator = null; + } + } + } + + static final class Table { + final AtomicReferenceArray[]> array; + final int threshold; + /** Bits 0-30 are size; bit 31 is 1 if the table is being resized. */ + volatile int size; + volatile Table resizeView; + + private Table(int capacity, float loadFactor) { + array = new AtomicReferenceArray<>(capacity); + threshold = capacity == MAXIMUM_CAPACITY ? Integer.MAX_VALUE : (int)(capacity * loadFactor); + } + } + + static final class Item implements Entry { + private final K key; + private final int hashCode; + volatile V value; + + Item(final K key, final int hashCode, final V value) { + this.key = key; + this.hashCode = hashCode; + //noinspection ThisEscapedInObjectConstruction + valueUpdater.lazySet(this, value); + } + + public K getKey() { + return key; + } + + public V getValue() { + V value = this.value; + if (value == NONEXISTENT) { + throw new IllegalStateException("Already removed"); + } + return value; + } + + public V setValue(final V value) { + V oldValue; + do { + oldValue = this.value; + if (oldValue == NONEXISTENT) { + throw new IllegalStateException("Already removed"); + } + } while (! valueUpdater.compareAndSet(this, oldValue, value)); + return oldValue; + } + + public int hashCode() { + return hashCode; + } + + public boolean equals(final Object obj) { + return obj instanceof Item && equals((Item) obj); + } + + public boolean equals(final Item obj) { + return obj != null && hashCode == obj.hashCode && key.equals(obj.key); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/Sessions.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/Sessions.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/Sessions.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.session.Session; +import io.undertow.server.session.SessionConfig; +import io.undertow.server.session.SessionManager; + +/** + * Utility class for working with sessions. + * + * @author Stuart Douglas + */ +public class Sessions { + + /** + * Gets the active session, returning null if one is not present. + * @param exchange The exchange + * @return The session + */ + public static Session getSession(final HttpServerExchange exchange) { + SessionManager sessionManager = exchange.getAttachment(SessionManager.ATTACHMENT_KEY); + SessionConfig sessionConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY); + if(sessionManager == null) { + throw UndertowMessages.MESSAGES.sessionManagerNotFound(); + } + return sessionManager.getSession(exchange, sessionConfig); + } + + /** + * Gets the active session, creating a new one if one does not exist + * @param exchange The exchange + * @return The session + */ + public static Session getOrCreateSession(final HttpServerExchange exchange) { + SessionManager sessionManager = exchange.getAttachment(SessionManager.ATTACHMENT_KEY); + SessionConfig sessionConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY); + if(sessionManager == null) { + throw UndertowMessages.MESSAGES.sessionManagerNotFound(); + } + Session session = sessionManager.getSession(exchange, sessionConfig); + if(session == null) { + session = sessionManager.createSession(exchange, sessionConfig); + } + return session; + } + + private Sessions () {} + +} Index: 3rdParty_sources/undertow/io/undertow/util/SimpleAttachmentKey.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/SimpleAttachmentKey.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/SimpleAttachmentKey.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * @author David M. Lloyd + */ +class SimpleAttachmentKey extends AttachmentKey { + private final Class valueClass; + + SimpleAttachmentKey(final Class valueClass) { + this.valueClass = valueClass; + } + + public T cast(final Object value) { + return valueClass.cast(value); + } + + @Override + public String toString() { + if (valueClass != null) { + StringBuilder sb = new StringBuilder(getClass().getName()); + sb.append("<"); + sb.append(valueClass.getName()); + sb.append(">"); + return sb.toString(); + } + return super.toString(); + } +} Index: 3rdParty_sources/undertow/io/undertow/util/StatusCodes.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/StatusCodes.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/StatusCodes.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,235 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +/** + * @author Stuart Douglas + */ +public class StatusCodes { + + //chosen simply because it gives no collisions + //if more codes are added this will need to be re-evaluated + private static final int SIZE = 0x2df; + private static final Entry[] TABLE = new Entry[SIZE]; + + public static final int CONTINUE = 100; + public static final int SWITCHING_PROTOCOLS = 101; + public static final int PROCESSING = 102; + public static final int OK = 200; + public static final int CREATED = 201; + public static final int ACCEPTED = 202; + public static final int NON_AUTHORITATIVE_INFORMATION = 203; + public static final int NO_CONTENT = 204; + public static final int RESET_CONTENT = 205; + public static final int PARTIAL_CONTENT = 206; + public static final int MULTI_STATUS = 207; + public static final int ALREADY_REPORTED = 208; + public static final int IM_USED = 226; + public static final int MULTIPLE_CHOICES = 300; + public static final int MOVED_PERMENANTLY = 301; + public static final int FOUND = 302; + public static final int SEE_OTHER = 303; + public static final int NOT_MODIFIED = 304; + public static final int USE_PROXY = 305; + public static final int TEMPORARY_REDIRECT = 307; + public static final int PERMANENT_REDIRECT = 308; + public static final int BAD_REQUEST = 400; + public static final int UNAUTHORIZED = 401; + public static final int PAYMENT_REQUIRED = 402; + public static final int FORBIDDEN = 403; + public static final int NOT_FOUND = 404; + public static final int METHOD_NOT_ALLOWED = 405; + public static final int NOT_ACCEPTABLE = 406; + public static final int PROXY_AUTHENTICATION_REQUIRED = 407; + public static final int REQUEST_TIME_OUT = 408; + public static final int CONFLICT = 409; + public static final int GONE = 410; + public static final int LENGTH_REQUIRED = 411; + public static final int PRECONDITION_FAILED = 412; + public static final int REQUEST_ENTITY_TOO_LARGE = 413; + public static final int REQUEST_URI_TOO_LARGE = 414; + public static final int UNSUPPORTED_MEDIA_TYPE = 415; + public static final int REQUEST_RANGE_NOT_SATISFIABLE = 416; + public static final int EXPECTATION_FAILED = 417; + public static final int UNPROCESSABLE_ENTITY = 422; + public static final int LOCKED = 423; + public static final int FAILED_DEPENDENCY = 424; + public static final int UPGRADE_REQUIRED = 426; + public static final int PRECONDITION_REQUIRED = 428; + public static final int TOO_MANY_REQUESTS = 429; + public static final int REQUEST_HEADER_FIELDS_TOO_LARGE = 431; + public static final int INTERNAL_SERVER_ERROR = 500; + public static final int NOT_IMPLEMENTED = 501; + public static final int BAD_GATEWAY = 502; + public static final int SERVICE_UNAVAILABLE = 503; + public static final int GATEWAY_TIME_OUT = 504; + public static final int HTTP_VERSION_NOT_SUPPORTED = 505; + public static final int INSUFFICIENT_STORAGE = 507; + public static final int LOOP_DETECTED = 508; + public static final int NOT_EXTENDED = 510; + public static final int NETWORK_AUTHENTICATION_REQUIRED = 511; + + public static final String CONTINUE_STRING = "Continue"; + public static final String SWITCHING_PROTOCOLS_STRING = "Switching Protocols"; + public static final String PROCESSING_STRING = "Processing"; + public static final String OK_STRING = "OK"; + public static final String CREATED_STRING = "Created"; + public static final String ACCEPTED_STRING = "Accepted"; + public static final String NON_AUTHORITATIVE_INFORMATION_STRING = "Non-Authoritative Information"; + public static final String NO_CONTENT_STRING = "No Content"; + public static final String RESET_CONTENT_STRING = "Reset Content"; + public static final String PARTIAL_CONTENT_STRING = "Partial Content"; + public static final String MULTI_STATUS_STRING = "Multi-Status"; + public static final String ALREADY_REPORTED_STRING = "Already Reported"; + public static final String IM_USED_STRING = "IM Used"; + public static final String MULTIPLE_CHOICES_STRING = "Multiple Choices"; + public static final String MOVED_PERMANENTLY_STRING = "Moved Permanently"; + public static final String FOUND_STRING = "Found"; + public static final String SEE_OTHER_STRING = "See Other"; + public static final String NOT_MODIFIED_STRING = "Not Modified"; + public static final String USE_PROXY_STRING = "Use Proxy"; + public static final String TEMPORARY_REDIRECT_STRING = "Temporary Redirect"; + public static final String PERMANENT_REDIRECT_STRING = "Permanent Redirect"; + public static final String BAD_REQUEST_STRING = "Bad Request"; + public static final String UNAUTHORIZED_STRING = "Unauthorized"; + public static final String PAYMENT_REQUIRED_STRING = "Payment Required"; + public static final String FORBIDDEN_STRING = "Forbidden"; + public static final String NOT_FOUND_STRING = "Not Found"; + public static final String METHOD_NOT_ALLOWED_STRING = "Method Not Allowed"; + public static final String NOT_ACCEPTABLE_STRING = "Not Acceptable"; + public static final String PROXY_AUTHENTICATION_REQUIRED_STRING = "Proxy Authentication Required"; + public static final String REQUEST_TIME_OUT_STRING = "Request Time-out"; + public static final String CONFLICT_STRING = "Conflict"; + public static final String GONE_STRING = "Gone"; + public static final String LENGTH_REQUIRED_STRING = "Length Required"; + public static final String PRECONDITION_FAILED_STRING = "Precondition Failed"; + public static final String REQUEST_ENTITY_TOO_LARGE_STRING = "Request Entity Too Large"; + public static final String REQUEST_URI_TOO_LARGE_STRING = "Request-URI Too Large"; + public static final String UNSUPPORTED_MEDIA_TYPE_STRING = "Unsupported Media Type"; + public static final String REQUEST_RANGE_NOT_SATISFIABLE_STRING = "Requested range not satisfiable"; + public static final String EXPECTATION_FAILED_STRING = "Expectation Failed"; + public static final String UNPROCESSABLE_ENTITY_STRING = "Unprocessable Entity"; + public static final String LOCKED_STRING = "Locked"; + public static final String FAILED_DEPENDENCY_STRING = "Failed Dependency"; + public static final String UPGRADE_REQUIRED_STRING = "Upgrade Required"; + public static final String PRECONDITION_REQUIRED_STRING = "Precondition Required"; + public static final String TOO_MANY_REQUESTS_STRING = "Too Many Requests"; + public static final String REQUEST_HEADER_FIELDS_TOO_LARGE_STRING = "Request Header Fields Too Large"; + public static final String INTERNAL_SERVER_ERROR_STRING = "Internal Server Error"; + public static final String NOT_IMPLEMENTED_STRING = "Not Implemented"; + public static final String BAD_GATEWAY_STRING = "Bad Gateway"; + public static final String SERVICE_UNAVAILABLE_STRING = "Service Unavailable"; + public static final String GATEWAY_TIME_OUT_STRING = "Gateway Time-out"; + public static final String HTTP_VERSION_NOT_SUPPORTED_STRING = "HTTP Version not supported"; + public static final String INSUFFICIENT_STORAGE_STRING = "Insufficient Storage"; + public static final String LOOP_DETECTED_STRING = "Loop Detected"; + public static final String NOT_EXTENDED_STRING = "Not Extended"; + public static final String NETWORK_AUTHENTICATION_REQUIRED_STRING = "Network Authentication Required"; + + static { + putCode(CONTINUE, CONTINUE_STRING); + putCode(SWITCHING_PROTOCOLS, SWITCHING_PROTOCOLS_STRING); + putCode(PROCESSING, PROCESSING_STRING); + putCode(OK, OK_STRING); + putCode(CREATED, CREATED_STRING); + putCode(ACCEPTED, ACCEPTED_STRING); + putCode(NON_AUTHORITATIVE_INFORMATION, NON_AUTHORITATIVE_INFORMATION_STRING); + putCode(NO_CONTENT, NO_CONTENT_STRING); + putCode(RESET_CONTENT, RESET_CONTENT_STRING); + putCode(PARTIAL_CONTENT, PARTIAL_CONTENT_STRING); + putCode(MULTI_STATUS, MULTI_STATUS_STRING); + putCode(ALREADY_REPORTED, ALREADY_REPORTED_STRING); + putCode(IM_USED, IM_USED_STRING); + putCode(MULTIPLE_CHOICES, MULTIPLE_CHOICES_STRING); + putCode(MOVED_PERMENANTLY, MOVED_PERMANENTLY_STRING); + putCode(FOUND, FOUND_STRING); + putCode(SEE_OTHER, SEE_OTHER_STRING); + putCode(NOT_MODIFIED, NOT_MODIFIED_STRING); + putCode(USE_PROXY, USE_PROXY_STRING); + putCode(TEMPORARY_REDIRECT, TEMPORARY_REDIRECT_STRING); + putCode(PERMANENT_REDIRECT, PERMANENT_REDIRECT_STRING); + putCode(BAD_REQUEST, BAD_REQUEST_STRING); + putCode(UNAUTHORIZED, UNAUTHORIZED_STRING); + putCode(PAYMENT_REQUIRED, PAYMENT_REQUIRED_STRING); + putCode(FORBIDDEN, FORBIDDEN_STRING); + putCode(NOT_FOUND, NOT_FOUND_STRING); + putCode(METHOD_NOT_ALLOWED, METHOD_NOT_ALLOWED_STRING); + putCode(NOT_ACCEPTABLE, NOT_ACCEPTABLE_STRING); + putCode(PROXY_AUTHENTICATION_REQUIRED, PROXY_AUTHENTICATION_REQUIRED_STRING); + putCode(REQUEST_TIME_OUT, REQUEST_TIME_OUT_STRING); + putCode(CONFLICT, CONFLICT_STRING); + putCode(GONE, GONE_STRING); + putCode(LENGTH_REQUIRED, LENGTH_REQUIRED_STRING); + putCode(PRECONDITION_FAILED, PRECONDITION_FAILED_STRING); + putCode(REQUEST_ENTITY_TOO_LARGE, REQUEST_ENTITY_TOO_LARGE_STRING); + putCode(REQUEST_URI_TOO_LARGE, REQUEST_URI_TOO_LARGE_STRING); + putCode(UNSUPPORTED_MEDIA_TYPE, UNSUPPORTED_MEDIA_TYPE_STRING); + putCode(REQUEST_RANGE_NOT_SATISFIABLE, REQUEST_RANGE_NOT_SATISFIABLE_STRING); + putCode(EXPECTATION_FAILED, EXPECTATION_FAILED_STRING); + putCode(UNPROCESSABLE_ENTITY, UNPROCESSABLE_ENTITY_STRING); + putCode(LOCKED, LOCKED_STRING); + putCode(FAILED_DEPENDENCY, FAILED_DEPENDENCY_STRING); + putCode(UPGRADE_REQUIRED, UPGRADE_REQUIRED_STRING); + putCode(PRECONDITION_REQUIRED, PRECONDITION_REQUIRED_STRING); + putCode(TOO_MANY_REQUESTS, TOO_MANY_REQUESTS_STRING); + putCode(REQUEST_HEADER_FIELDS_TOO_LARGE, REQUEST_HEADER_FIELDS_TOO_LARGE_STRING); + putCode(INTERNAL_SERVER_ERROR, INTERNAL_SERVER_ERROR_STRING); + putCode(NOT_IMPLEMENTED, NOT_IMPLEMENTED_STRING); + putCode(BAD_GATEWAY, BAD_GATEWAY_STRING); + putCode(SERVICE_UNAVAILABLE, SERVICE_UNAVAILABLE_STRING); + putCode(GATEWAY_TIME_OUT, GATEWAY_TIME_OUT_STRING); + putCode(HTTP_VERSION_NOT_SUPPORTED, HTTP_VERSION_NOT_SUPPORTED_STRING); + putCode(INSUFFICIENT_STORAGE, INSUFFICIENT_STORAGE_STRING); + putCode(LOOP_DETECTED, LOOP_DETECTED_STRING); + putCode(NOT_EXTENDED, NOT_EXTENDED_STRING); + putCode(NETWORK_AUTHENTICATION_REQUIRED, NETWORK_AUTHENTICATION_REQUIRED_STRING); + + } + + private static void putCode(int code, String reason) { + Entry e = new Entry(reason, code); + int h = code & SIZE; + if(TABLE[h] != null) { + throw new IllegalArgumentException("hash collision"); + } + TABLE[h] = e; + } + + private StatusCodes() { + } + + public static final String getReason(final int code) { + final Entry result = TABLE[code & SIZE]; + if (result == null || result.code != code) { + return "Unknown"; + } else { + return result.reason; + } + } + + private static final class Entry { + final String reason; + final int code; + + private Entry(final String reason, final int code) { + this.reason = reason; + this.code = code; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/StringReadChannelListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/StringReadChannelListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/StringReadChannelListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,100 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import io.undertow.websockets.core.UTF8Output; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; + +/** + * Simple utility class for reading a string + *

+ * todo: handle unicode properly + * + * @author Stuart Douglas + */ +public abstract class StringReadChannelListener implements ChannelListener { + + private final UTF8Output string = new UTF8Output(); + private final Pool bufferPool; + + public StringReadChannelListener(final Pool bufferPool) { + this.bufferPool = bufferPool; + } + + public void setup(final StreamSourceChannel channel) { + Pooled resource = bufferPool.allocate(); + ByteBuffer buffer = resource.getResource(); + try { + int r = 0; + do { + r = channel.read(buffer); + if (r == 0) { + channel.getReadSetter().set(this); + channel.resumeReads(); + } else if (r == -1) { + stringDone(string.extract()); + IoUtils.safeClose(channel); + } else { + buffer.flip(); + string.write(buffer); + } + } while (r > 0); + } catch (IOException e) { + error(e); + } finally { + resource.free(); + } + } + + @Override + public void handleEvent(final StreamSourceChannel channel) { + Pooled resource = bufferPool.allocate(); + ByteBuffer buffer = resource.getResource(); + try { + int r = 0; + do { + r = channel.read(buffer); + if (r == 0) { + return; + } else if (r == -1) { + stringDone(string.extract()); + IoUtils.safeClose(channel); + } else { + buffer.flip(); + string.write(buffer); + } + } while (r > 0); + } catch (IOException e) { + error(e); + } finally { + resource.free(); + } + } + + protected abstract void stringDone(String string); + + protected abstract void error(IOException e); +} Index: 3rdParty_sources/undertow/io/undertow/util/StringWriteChannelListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/StringWriteChannelListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/StringWriteChannelListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,114 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import io.undertow.UndertowLogger; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.channels.StreamSinkChannel; + +/** + * A simple write listener that can be used to write out the contents of a String. When the string is written + * out it closes the channel. + * + * This should not be added directly to the channel, instead {@link #setup(org.xnio.channels.StreamSinkChannel)} + * should be called, which will attempt a write, and only add the listener if required. + * + * @author Stuart Douglas + */ +public class StringWriteChannelListener implements ChannelListener { + + private final ByteBuffer buffer; + + public StringWriteChannelListener( final String string) { + this(string, Charset.defaultCharset()); + } + + public StringWriteChannelListener( final String string, Charset charset) { + buffer = ByteBuffer.wrap(string.getBytes(charset)); + } + + public void setup(final StreamSinkChannel channel) { + try { + int c; + do { + c = channel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + if (buffer.hasRemaining()) { + channel.getWriteSetter().set(this); + channel.resumeWrites(); + } else { + writeDone(channel); + } + } catch (IOException e) { + handleError(channel, e); + } + } + + protected void handleError(StreamSinkChannel channel, IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(channel); + } + + @Override + public void handleEvent(final StreamSinkChannel channel) { + try { + int c; + do { + c = channel.write(buffer); + } while (buffer.hasRemaining() && c > 0); + if (buffer.hasRemaining()) { + channel.resumeWrites(); + return; + } else { + writeDone(channel); + } + } catch (IOException e) { + handleError(channel, e); + } + } + + public boolean hasRemaining() { + return buffer.hasRemaining(); + } + + protected void writeDone(final StreamSinkChannel channel) { + try { + channel.shutdownWrites(); + + if (!channel.flush()) { + channel.getWriteSetter().set(ChannelListeners.flushingChannelListener(new ChannelListener() { + @Override + public void handleEvent(StreamSinkChannel o) { + IoUtils.safeClose(channel); + } + }, ChannelListeners.closingChannelExceptionHandler())); + channel.resumeWrites(); + + } + } catch (IOException e) { + handleError(channel, e); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/util/URLUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/util/URLUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/util/URLUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,220 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.util; + +import io.undertow.UndertowMessages; +import io.undertow.server.HttpServerExchange; + +import java.io.UnsupportedEncodingException; +import java.net.URLDecoder; + +/** + * Utilities for dealing with URLs + * + * @author Stuart Douglas + */ +public class URLUtils { + + private static final QueryStringParser QUERY_STRING_PARSER = new QueryStringParser() { + @Override + void handle(HttpServerExchange exchange, String key, String value) { + exchange.addQueryParam(key, value); + } + }; + private static final QueryStringParser PATH_PARAM_PARSER = new QueryStringParser() { + @Override + void handle(HttpServerExchange exchange, String key, String value) { + exchange.addPathParam(key, value); + } + }; + + private URLUtils() { + + } + + public static void parseQueryString(final String string, final HttpServerExchange exchange, final String charset, final boolean doDecode) { + QUERY_STRING_PARSER.parse(string, exchange, charset, doDecode); + } + + public static void parsePathParms(final String string, final HttpServerExchange exchange, final String charset, final boolean doDecode) { + PATH_PARAM_PARSER.parse(string, exchange, charset, doDecode); + } + + /** + * Decodes a URL. If the decoding fails for any reason then an IllegalArgumentException will be thrown. + * + * @param s The string to decode + * @param enc The encoding + * @param decodeSlash If slash characters should be decoded + * @param buffer The string builder to use as a buffer. + * @return The decoded URL + */ + public static String decode(String s, String enc, boolean decodeSlash, StringBuilder buffer) { + buffer.setLength(0); + boolean needToChange = false; + int numChars = s.length(); + int i = 0; + boolean mightRequireSlashEscape = false; + + char c; + byte[] bytes = null; + while (i < numChars) { + c = s.charAt(i); + switch (c) { + case '+': + buffer.append(' '); + i++; + needToChange = true; + break; + case '%': + /* + * Starting with this instance of %, process all + * consecutive substrings of the form %xy. Each + * substring %xy will yield a byte. Convert all + * consecutive bytes obtained this way to whatever + * character(s) they represent in the provided + * encoding. + */ + try { + // (numChars-i)/3 is an upper bound for the number + // of remaining bytes + if (bytes == null) { + bytes = new byte[(numChars - i) / 3]; + } + int pos = 0; + + while (((i + 2) < numChars) && (c == '%')) { + char p1 = Character.toLowerCase(s.charAt(i + 1)); + char p2 = Character.toLowerCase(s.charAt(i + 2)); + int v = 0; + if (p1 >= '0' && p1 <= '9') { + v = (p1 - '0') << 4; + } else if (p1 >= 'a' && p1 <= 'f') { + v = (p1 - 'a' + 10) << 4; + } else { + throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc); + } + if (p2 >= '0' && p2 <= '9') { + v += (p2 - '0'); + } else if (p2 >= 'a' && p2 <= 'f') { + v += (p2 - 'a' + 10); + } else { + throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc); + } + if (v < 0) { + throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc); + } + if(v == '/' || v== '\\') { + mightRequireSlashEscape = true; + } + + bytes[pos++] = (byte) v; + i += 3; + if (i < numChars) { + c = s.charAt(i); + } + } + + // A trailing, incomplete byte encoding such as + // "%x" will cause an exception to be thrown + + if ((i < numChars) && (c == '%')) { + throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc); + } + + String decoded = new String(bytes, 0, pos, enc); + if (!decodeSlash && mightRequireSlashEscape) { + //we need to re-encode slash characters + //this is yuck, but a corner case + int decPos = 0; + for (int j = 0; j < decoded.length(); ++j) { + char decChar = decoded.charAt(j); + if (decChar == '/') { + buffer.append(decoded.substring(decPos, j)); + buffer.append("%2F"); + decPos = j + 1; + } else if (decChar == '\\') { + buffer.append(decoded.substring(decPos, j)); + buffer.append("%5C"); + decPos = j + 1; + } + } + buffer.append(decoded.substring(decPos)); + } else { + buffer.append(decoded); + } + mightRequireSlashEscape = false; + } catch (NumberFormatException e) { + throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc); + } catch (UnsupportedEncodingException e) { + throw UndertowMessages.MESSAGES.failedToDecodeURL(s, enc); + } + needToChange = true; + break; + default: + buffer.append(c); + i++; + break; + } + } + + return (needToChange ? buffer.toString() : s); + } + + private abstract static class QueryStringParser { + + void parse(final String string, final HttpServerExchange exchange, final String charset, final boolean doDecode) { + try { + int stringStart = 0; + String attrName = null; + for (int i = 0; i < string.length(); ++i) { + char c = string.charAt(i); + if (c == '=' && attrName == null) { + attrName = string.substring(stringStart, i); + stringStart = i + 1; + } else if (c == '&') { + if (attrName != null) { + handle(exchange, decode(charset, attrName, doDecode), decode(charset, string.substring(stringStart, i), doDecode)); + } else { + handle(exchange, decode(charset, string.substring(stringStart, i), doDecode), ""); + } + stringStart = i + 1; + attrName = null; + } + } + if (attrName != null) { + handle(exchange, decode(charset, attrName, doDecode), decode(charset, string.substring(stringStart, string.length()), doDecode)); + } else if (string.length() != stringStart) { + handle(exchange, decode(charset, string.substring(stringStart, string.length()), doDecode), ""); + } + } catch (UnsupportedEncodingException e) { + throw new RuntimeException(e); + } + } + + private String decode(String charset, String attrName, final boolean doDecode) throws UnsupportedEncodingException { + if (doDecode) { + return URLDecoder.decode(attrName, charset); + } + return attrName; + } + + abstract void handle(final HttpServerExchange exchange, final String key, final String value); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/WebSocketConnectionCallback.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/WebSocketConnectionCallback.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/WebSocketConnectionCallback.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,37 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets; + +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.spi.WebSocketHttpExchange; + +/** + * Interface that is used on the client side to accept web socket connections + * + * @author Stuart Douglas + */ +public interface WebSocketConnectionCallback { + + /** + * Is called once the WebSocket connection is established, which means the handshake was successful. + * + */ + void onConnect(WebSocketHttpExchange exchange, WebSocketChannel channel); + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/WebSocketExtension.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/WebSocketExtension.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/WebSocketExtension.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,99 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * @author Stuart Douglas + */ +public class WebSocketExtension { + + private final String name; + private final List parameters; + + public WebSocketExtension(String name, List parameters) { + this.name = name; + this.parameters = Collections.unmodifiableList(new ArrayList<>(parameters)); + } + + public String getName() { + return name; + } + + public List getParameters() { + return parameters; + } + + public static final class Parameter { + private final String name; + private final String value; + + public Parameter(String name, String value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public String getValue() { + return value; + } + + @Override + public String toString() { + return "{'" + name + '\'' + + ": '" + value + '\'' + + '}'; + } + } + + @Override + public String toString() { + return "WebSocketExtension{" + + "name='" + name + '\'' + + ", parameters=" + parameters + + '}'; + } + + public static List parse(final String extensionHeader) { + List extensions = new ArrayList<>(); + //TODO: more efficient parsing algorithm + String[] parts = extensionHeader.split(","); + for (String part : parts) { + String[] items = part.split(";"); + if (items.length > 0) { + final List params = new ArrayList<>(items.length - 1); + String name = items[0]; + for (int i = 1; i < items.length; ++i) { + String[] param = items[i].split("="); + if (param.length == 2) { + params.add(new Parameter(param[0], param[1])); + } + } + extensions.add(new WebSocketExtension(name, params)); + } + } + return extensions; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/WebSocketProtocolHandshakeHandler.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/WebSocketProtocolHandshakeHandler.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/WebSocketProtocolHandshakeHandler.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,208 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets; + + +import io.undertow.server.HttpHandler; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.handlers.ResponseCodeHandler; +import io.undertow.util.Methods; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.protocol.Handshake; +import io.undertow.websockets.core.protocol.version07.Hybi07Handshake; +import io.undertow.websockets.core.protocol.version08.Hybi08Handshake; +import io.undertow.websockets.core.protocol.version13.Hybi13Handshake; +import io.undertow.websockets.spi.AsyncWebSocketHttpServerExchange; +import org.xnio.StreamConnection; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * {@link HttpHandler} which will process the {@link HttpServerExchange} and do the actual handshake/upgrade + * to WebSocket. + * + * @author Norman Maurer + */ +public class WebSocketProtocolHandshakeHandler implements HttpHandler { + private final Set handshakes; + + /** + * The upgrade listener. This will only be used if another web socket implementation is being layered on top. + */ + private final HttpUpgradeListener upgradeListener; + + private final WebSocketConnectionCallback callback; + + private final Set peerConnections = Collections.newSetFromMap(new ConcurrentHashMap()); + + /** + * The handler that is invoked if there are no web socket headers + */ + private final HttpHandler next; + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(final WebSocketConnectionCallback callback) { + this(callback, ResponseCodeHandler.HANDLE_404); + } + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(final WebSocketConnectionCallback callback, final HttpHandler next) { + this.callback = callback; + Set handshakes = new HashSet<>(); + handshakes.add(new Hybi13Handshake()); + handshakes.add(new Hybi08Handshake()); + handshakes.add(new Hybi07Handshake()); + this.handshakes = handshakes; + this.next = next; + this.upgradeListener = null; + } + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param handshakes The supported handshake methods + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(Collection handshakes, final WebSocketConnectionCallback callback) { + this(handshakes, callback, ResponseCodeHandler.HANDLE_404); + } + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param handshakes The supported handshake methods + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(Collection handshakes, final WebSocketConnectionCallback callback, final HttpHandler next) { + this.callback = callback; + this.handshakes = new HashSet<>(handshakes); + this.next = next; + this.upgradeListener = null; + } + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(final HttpUpgradeListener callback) { + this(callback, ResponseCodeHandler.HANDLE_404); + } + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(final HttpUpgradeListener callback, final HttpHandler next) { + this.callback = null; + Set handshakes = new HashSet<>(); + handshakes.add(new Hybi13Handshake()); + handshakes.add(new Hybi08Handshake()); + handshakes.add(new Hybi07Handshake()); + this.handshakes = handshakes; + this.next = next; + this.upgradeListener = callback; + } + + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param handshakes The supported handshake methods + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(Collection handshakes, final HttpUpgradeListener callback) { + this(handshakes, callback, ResponseCodeHandler.HANDLE_404); + } + + /** + * Create a new {@link WebSocketProtocolHandshakeHandler} + * + * @param handshakes The supported handshake methods + * @param callback The {@link WebSocketConnectionCallback} which will be executed once the handshake was + * established + */ + public WebSocketProtocolHandshakeHandler(Collection handshakes, final HttpUpgradeListener callback, final HttpHandler next) { + this.callback = null; + this.handshakes = new HashSet<>(handshakes); + this.next = next; + this.upgradeListener = callback; + } + + @Override + public void handleRequest(final HttpServerExchange exchange) throws Exception { + if (!exchange.getRequestMethod().equals(Methods.GET)) { + // Only GET is supported to start the handshake + next.handleRequest(exchange); + return; + } + final AsyncWebSocketHttpServerExchange facade = new AsyncWebSocketHttpServerExchange(exchange, peerConnections); + Handshake handshaker = null; + for (Handshake method : handshakes) { + if (method.matches(facade)) { + handshaker = method; + break; + } + } + + if (handshaker == null) { + next.handleRequest(exchange); + } else { + final Handshake selected = handshaker; + if (upgradeListener == null) { + exchange.upgradeChannel(new HttpUpgradeListener() { + @Override + public void handleUpgrade(StreamConnection streamConnection, HttpServerExchange exchange) { + WebSocketChannel channel = selected.createChannel(facade, streamConnection, facade.getBufferPool()); + peerConnections.add(channel); + callback.onConnect(facade, channel); + } + }); + } else { + exchange.upgradeChannel(upgradeListener); + } + handshaker.handshake(facade); + } + } + + public Set getPeerConnections() { + return peerConnections; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocket13ClientHandshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocket13ClientHandshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocket13ClientHandshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,182 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.client; + +import io.undertow.util.FlexBase64; +import io.undertow.util.Headers; +import io.undertow.websockets.WebSocketExtension; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketMessages; +import io.undertow.websockets.core.WebSocketUtils; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.protocol.version13.WebSocket13Channel; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.http.HandshakeChecker; + +import java.io.IOException; +import java.net.URI; +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * @author Stuart Douglas + */ +public class WebSocket13ClientHandshake extends WebSocketClientHandshake { + + public static final String MAGIC_NUMBER = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + private final WebSocketClientNegotiation negotiation; + + public WebSocket13ClientHandshake(final URI url, WebSocketClientNegotiation negotiation) { + super(url); + this.negotiation = negotiation; + } + + public WebSocket13ClientHandshake(final URI url) { + this(url, null); + } + + @Override + public WebSocketChannel createChannel(final StreamConnection channel, final String wsUri, final Pool bufferPool) { + return new WebSocket13Channel(channel, bufferPool, wsUri, negotiation != null ? negotiation.getSelectedSubProtocol() : "", true, false, new HashSet()); + } + + + public Map createHeaders() { + Map headers = new HashMap<>(); + headers.put(Headers.UPGRADE_STRING, "websocket"); + headers.put(Headers.CONNECTION_STRING, "upgrade"); + String key = createSecKey(); + headers.put(Headers.SEC_WEB_SOCKET_KEY_STRING, key); + headers.put(Headers.SEC_WEB_SOCKET_VERSION_STRING, getVersion().toHttpHeaderValue()); + if (negotiation != null) { + List subProtocols = negotiation.getSupportedSubProtocols(); + if (subProtocols != null && !subProtocols.isEmpty()) { + StringBuilder sb = new StringBuilder(); + Iterator it = subProtocols.iterator(); + while (it.hasNext()) { + sb.append(it.next()); + if (it.hasNext()) { + sb.append(", "); + } + } + headers.put(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING, sb.toString()); + } + List extensions = negotiation.getSupportedExtensions(); + if (extensions != null && !extensions.isEmpty()) { + StringBuilder sb = new StringBuilder(); + Iterator it = extensions.iterator(); + while (it.hasNext()) { + WebSocketExtension next = it.next(); + sb.append(next); + for (WebSocketExtension.Parameter param : next.getParameters()) { + sb.append("; "); + sb.append(param.getName()); + sb.append("="); + sb.append(param.getValue()); + } + if (it.hasNext()) { + sb.append(", "); + } + } + headers.put(Headers.SEC_WEB_SOCKET_EXTENSIONS_STRING, sb.toString()); + } + } + return headers; + + } + + protected String createSecKey() { + SecureRandom random = new SecureRandom(); + byte[] data = new byte[16]; + for (int i = 0; i < 4; ++i) { + int val = random.nextInt(); + data[i * 4] = (byte) val; + data[i * 4 + 1] = (byte) ((val >> 8) & 0xFF); + data[i * 4 + 2] = (byte) ((val >> 16) & 0xFF); + data[i * 4 + 3] = (byte) ((val >> 24) & 0xFF); + } + return FlexBase64.encodeString(data, false); + } + + @Override + public HandshakeChecker handshakeChecker(final URI uri, final Map requestHeaders) { + final String sentKey = requestHeaders.get(Headers.SEC_WEB_SOCKET_KEY_STRING); + return new HandshakeChecker() { + @Override + public void checkHandshake(Map headers) throws IOException { + if(negotiation != null) { + negotiation.afterRequest(headers); + } + String upgrade = headers.get(Headers.UPGRADE_STRING.toLowerCase(Locale.ENGLISH)); + if (upgrade == null || !upgrade.trim().equalsIgnoreCase("websocket")) { + throw WebSocketMessages.MESSAGES.noWebSocketUpgradeHeader(); + } + String connHeader = headers.get(Headers.CONNECTION_STRING.toLowerCase(Locale.ENGLISH)); + if (connHeader == null || !connHeader.trim().equalsIgnoreCase("upgrade")) { + throw WebSocketMessages.MESSAGES.noWebSocketConnectionHeader(); + } + String acceptKey = headers.get(Headers.SEC_WEB_SOCKET_ACCEPT_STRING.toLowerCase(Locale.ENGLISH)); + final String dKey = solve(sentKey); + if (!dKey.equals(acceptKey)) { + throw WebSocketMessages.MESSAGES.webSocketAcceptKeyMismatch(dKey, acceptKey); + } + if (negotiation != null) { + String subProto = headers.get(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING.toLowerCase(Locale.ENGLISH)); + if (subProto != null && !subProto.isEmpty() && !negotiation.getSupportedSubProtocols().contains(subProto)) { + throw WebSocketMessages.MESSAGES.unsupportedProtocol(subProto, negotiation.getSupportedSubProtocols()); + } + List extensions = Collections.emptyList(); + String extHeader = headers.get(Headers.SEC_WEB_SOCKET_EXTENSIONS_STRING.toLowerCase(Locale.ENGLISH)); + if (extHeader != null) { + extensions = WebSocketExtension.parse(extHeader); + } + negotiation.handshakeComplete(subProto, extensions); + } + } + }; + } + + protected final String solve(final String nonceBase64) { + try { + final String concat = nonceBase64 + MAGIC_NUMBER; + final MessageDigest digest = MessageDigest.getInstance("SHA1"); + + digest.update(concat.getBytes(WebSocketUtils.UTF_8)); + final byte[] bytes = digest.digest(); + return FlexBase64.encodeString(bytes, false); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public WebSocketVersion getVersion() { + return WebSocketVersion.V13; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClient.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClient.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClient.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,114 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.client; + +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; + +import org.xnio.Cancellable; +import org.xnio.ChannelListener; +import org.xnio.FutureResult; +import org.xnio.IoFuture; +import org.xnio.OptionMap; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.XnioWorker; +import org.xnio.http.HttpUpgrade; +import org.xnio.ssl.XnioSsl; + +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * The Web socket client. + * + * @author Stuart Douglas + */ +public class WebSocketClient { + + + public static IoFuture connect(XnioWorker worker, final Pool bufferPool, final OptionMap optionMap, final URI uri, WebSocketVersion version) { + return connect(worker, bufferPool, optionMap, uri, version, null); + } + + public static IoFuture connect(XnioWorker worker, XnioSsl ssl, final Pool bufferPool, final OptionMap optionMap, final URI uri, WebSocketVersion version) { + return connect(worker, ssl, bufferPool, optionMap, uri, version, null); + } + + public static IoFuture connect(XnioWorker worker, final Pool bufferPool, final OptionMap optionMap, final URI uri, WebSocketVersion version, WebSocketClientNegotiation clientNegotiation) { + return connect(worker, null, bufferPool, optionMap, uri, version, clientNegotiation); + } + + public static IoFuture connect(XnioWorker worker, XnioSsl ssl, final Pool bufferPool, final OptionMap optionMap, final URI uri, WebSocketVersion version, WebSocketClientNegotiation clientNegotiation) { + + final FutureResult ioFuture = new FutureResult<>(); + final URI newUri; + try { + newUri = new URI(uri.getScheme().equals("wss") ? "https" : "http", uri.getUserInfo(), uri.getHost(), uri.getPort() == -1 ? (uri.getScheme().equals("wss") ? 443 : 80) : uri.getPort(), uri.getPath().isEmpty() ? "/" : uri.getPath(), uri.getQuery(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + final WebSocketClientHandshake handshake = WebSocketClientHandshake.create(version, newUri, clientNegotiation); + final Map headers = handshake.createHeaders(); + if (clientNegotiation != null) { + clientNegotiation.beforeRequest(headers); + } + final IoFuture result; + if (ssl != null) { + result = HttpUpgrade.performUpgrade(worker, ssl, null, newUri, headers, new ChannelListener() { + @Override + public void handleEvent(StreamConnection channel) { + WebSocketChannel result = handshake.createChannel(channel, newUri.toString(), bufferPool); + ioFuture.setResult(result); + } + }, null, optionMap, handshake.handshakeChecker(newUri, headers)); + } else { + result = HttpUpgrade.performUpgrade(worker, null, newUri, headers, new ChannelListener() { + @Override + public void handleEvent(StreamConnection channel) { + WebSocketChannel result = handshake.createChannel(channel, newUri.toString(), bufferPool); + ioFuture.setResult(result); + } + }, null, optionMap, handshake.handshakeChecker(newUri, headers)); + } + result.addNotifier(new IoFuture.Notifier() { + @Override + public void notify(IoFuture res, Object attachment) { + if (res.getStatus() == IoFuture.Status.FAILED) { + ioFuture.setException(res.getException()); + } + } + }, null); + ioFuture.addCancelHandler(new Cancellable() { + @Override + public Cancellable cancel() { + result.cancel(); + return null; + } + }); + return ioFuture.getIoFuture(); + } + + + private WebSocketClient() { + + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClientHandshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClientHandshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClientHandshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,61 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.client; + +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import org.xnio.Pool; +import org.xnio.StreamConnection; +import org.xnio.http.HandshakeChecker; + +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Map; + +/** + * @author Stuart Douglas + */ +public abstract class WebSocketClientHandshake { + + protected final URI url; + + public static WebSocketClientHandshake create(final WebSocketVersion version, final URI uri) { + return create(version, uri, null); + } + + public static WebSocketClientHandshake create(final WebSocketVersion version, final URI uri, WebSocketClientNegotiation clientNegotiation) { + switch (version) { + case V13: + return new WebSocket13ClientHandshake(uri, clientNegotiation); + } + throw new IllegalArgumentException(); + } + + public WebSocketClientHandshake(final URI url) { + this.url = url; + } + + public abstract WebSocketChannel createChannel(final StreamConnection channel, final String wsUri, final Pool bufferPool); + + public abstract Map createHeaders(); + + public abstract HandshakeChecker handshakeChecker(final URI uri, final Map requestHeaders); + + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClientNegotiation.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClientNegotiation.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/client/WebSocketClientNegotiation.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.client; + +import io.undertow.websockets.WebSocketExtension; + +import java.util.List; +import java.util.Map; + +/** + * @author Stuart Douglas + */ +public class WebSocketClientNegotiation { + + private final List supportedSubProtocols; + private final List supportedExtensions; + private volatile String selectedSubProtocol; + private volatile List selectedExtensions; + + public WebSocketClientNegotiation(List supportedSubProtocols, List supportedExtensions) { + this.supportedSubProtocols = supportedSubProtocols; + this.supportedExtensions = supportedExtensions; + } + + public List getSupportedSubProtocols() { + return supportedSubProtocols; + } + + public List getSupportedExtensions() { + return supportedExtensions; + } + + public String getSelectedSubProtocol() { + return selectedSubProtocol; + } + + public List getSelectedExtensions() { + return selectedExtensions; + } + + public void beforeRequest(final Map headers) { + + } + public void afterRequest(final Map headers) { + + } + + public void handshakeComplete(String selectedProtocol, List selectedExtensions) { + this.selectedExtensions = selectedExtensions; + this.selectedSubProtocol = selectedProtocol; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/AbstractReceiveListener.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/AbstractReceiveListener.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/AbstractReceiveListener.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,219 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import org.xnio.ChannelListener; +import org.xnio.IoUtils; +import org.xnio.Pooled; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A receive listener that performs a callback when it receives a message + * + * @author Stuart Douglas + */ +public abstract class AbstractReceiveListener implements ChannelListener { + + @Override + public void handleEvent(WebSocketChannel channel) { + try { + final StreamSourceFrameChannel result = channel.receive(); + if (result == null) { + return; + } else if (result.getType() == WebSocketFrameType.BINARY) { + onBinary(channel, result); + } else if (result.getType() == WebSocketFrameType.TEXT) { + onText(channel, result); + } else if (result.getType() == WebSocketFrameType.PONG) { + onPong(channel, result); + } else if (result.getType() == WebSocketFrameType.PING) { + onPing(channel, result); + } else if (result.getType() == WebSocketFrameType.CLOSE) { + onClose(channel, result); + } + } catch (IOException e) { + onError(channel, e); + } + } + + protected void onPing(WebSocketChannel webSocketChannel, StreamSourceFrameChannel channel) throws IOException { + bufferFullMessage(channel); + } + + protected void onClose(WebSocketChannel webSocketChannel, StreamSourceFrameChannel channel) throws IOException { + bufferFullMessage(channel); + } + + protected void onPong(WebSocketChannel webSocketChannel, StreamSourceFrameChannel messageChannel) throws IOException { + bufferFullMessage(messageChannel); + } + + protected void onText(WebSocketChannel webSocketChannel, StreamSourceFrameChannel messageChannel) throws IOException { + bufferFullMessage(messageChannel); + } + + protected void onBinary(WebSocketChannel webSocketChannel, StreamSourceFrameChannel messageChannel) throws IOException { + bufferFullMessage(messageChannel); + } + + protected void onError(WebSocketChannel channel, Throwable error) { + IoUtils.safeClose(channel); + } + + /** + * Utility method that reads a full text or binary message, including all fragmented parts. Once the full message is + * read then the {@link #onFullTextMessage(WebSocketChannel, BufferedTextMessage)} or + * {@link #onFullBinaryMessage(WebSocketChannel, BufferedBinaryMessage)} method will be invoked. + * + * @param messageChannel The message channel + */ + protected final void bufferFullMessage(StreamSourceFrameChannel messageChannel) { + if (messageChannel.getType() == WebSocketFrameType.TEXT) { + readBufferedText(messageChannel, new BufferedTextMessage(getMaxTextBufferSize(), true)); + } else if (messageChannel.getType() == WebSocketFrameType.BINARY) { + readBufferedBinary(messageChannel, false, new BufferedBinaryMessage(getMaxBinaryBufferSize(), true)); + } else if (messageChannel.getType() == WebSocketFrameType.PONG) { + readBufferedBinary(messageChannel, true, new BufferedBinaryMessage(getMaxPongBufferSize(), true)); + } else if (messageChannel.getType() == WebSocketFrameType.PING) { + readBufferedBinary(messageChannel, true, new BufferedBinaryMessage(getMaxPingBufferSize(), true)); + } else if (messageChannel.getType() == WebSocketFrameType.CLOSE) { + readBufferedBinary(messageChannel, true, new BufferedBinaryMessage(getMaxCloseBufferSize(), true)); + } + } + + protected long getMaxBinaryBufferSize() { + return -1; + } + + protected long getMaxPongBufferSize() { + return -1; + } + + protected long getMaxCloseBufferSize() { + return -1; + } + + protected long getMaxPingBufferSize() { + return -1; + } + + protected long getMaxTextBufferSize() { + return -1; + } + + private void readBufferedBinary(final StreamSourceFrameChannel messageChannel, final boolean controlFrame, final BufferedBinaryMessage buffer) { + + buffer.read(messageChannel, new WebSocketCallback() { + @Override + public void complete(WebSocketChannel channel, BufferedBinaryMessage context) { + try { + WebSocketFrameType type = messageChannel.getType(); + if (!controlFrame) { + onFullBinaryMessage(channel, buffer); + } else if (type == WebSocketFrameType.PONG) { + onFullPongMessage(channel, buffer); + } else if (type == WebSocketFrameType.PING) { + onFullPingMessage(channel, buffer); + } else if (type == WebSocketFrameType.CLOSE) { + onFullCloseMessage(channel, buffer); + } + } catch (IOException e) { + AbstractReceiveListener.this.onError(channel, e); + } + } + + @Override + public void onError(WebSocketChannel channel, BufferedBinaryMessage context, Throwable throwable) { + context.getData().free(); + AbstractReceiveListener.this.onError(channel, throwable); + } + }); + } + + private void readBufferedText(StreamSourceFrameChannel messageChannel, final BufferedTextMessage textMessage) { + textMessage.read(messageChannel, new WebSocketCallback() { + @Override + public void complete(WebSocketChannel channel, BufferedTextMessage context) { + try { + onFullTextMessage(channel, textMessage); + } catch (IOException e) { + AbstractReceiveListener.this.onError(channel, e); + } + } + + @Override + public void onError(WebSocketChannel channel, BufferedTextMessage context, Throwable throwable) { + AbstractReceiveListener.this.onError(channel, throwable); + } + }); + } + + protected void onFullTextMessage(final WebSocketChannel channel, BufferedTextMessage message) throws IOException { + + } + + protected void onFullBinaryMessage(final WebSocketChannel channel, BufferedBinaryMessage message) throws IOException { + + } + + protected void onFullPingMessage(final WebSocketChannel channel, BufferedBinaryMessage message) throws IOException { + final Pooled data = message.getData(); + WebSockets.sendPong(data.getResource(), channel, new FreeDataCallback(data)); + } + + protected void onFullPongMessage(final WebSocketChannel channel, BufferedBinaryMessage message) throws IOException { + } + + protected void onFullCloseMessage(final WebSocketChannel channel, BufferedBinaryMessage message) throws IOException { + Pooled data = message.getData(); + try { + CloseMessage cm = new CloseMessage(data.getResource()); + onCloseMessage(cm, channel); + if (!channel.isCloseFrameSent()) { + WebSockets.sendClose(cm.toByteBuffer(), channel, null); + } + } finally { + data.free(); + } + } + + protected void onCloseMessage(CloseMessage cm, WebSocketChannel channel) { + + } + + private static class FreeDataCallback implements WebSocketCallback { + private final Pooled data; + + public FreeDataCallback(Pooled data) { + this.data = data; + } + + @Override + public void complete(WebSocketChannel channel, Void context) { + data.free(); + } + + @Override + public void onError(WebSocketChannel channel, Void context, Throwable throwable) { + data.free(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/BinaryOutputStream.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/BinaryOutputStream.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/BinaryOutputStream.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,73 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.UndertowMessages; +import org.xnio.channels.Channels; + +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +/** + * {@link OutputStream} implementation which buffers all the data until {@link #close()} is called and then will + * try to send it in a blocking fashion with the provided {@link StreamSinkFrameChannel}. + * + * @author Norman Maurer + */ +public final class BinaryOutputStream extends OutputStream { + private final StreamSinkFrameChannel sender; + private boolean closed; + + public BinaryOutputStream(StreamSinkFrameChannel sender) { + this.sender = sender; + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + checkClosed(); + Channels.writeBlocking(sender, ByteBuffer.wrap(b, off, len)); + } + + @Override + public void write(int b) throws IOException { + checkClosed(); + Channels.writeBlocking(sender, ByteBuffer.wrap(new byte[]{(byte) b})); + } + + @Override + public void flush() throws IOException { + checkClosed(); + sender.flush(); + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + sender.shutdownWrites(); + Channels.flushBlocking(sender); + } + } + + private void checkClosed() throws IOException { + if (closed) { + throw UndertowMessages.MESSAGES.streamIsClosed(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/BufferedBinaryMessage.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/BufferedBinaryMessage.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/BufferedBinaryMessage.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,233 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.util.ImmediatePooled; +import org.xnio.ChannelListener; +import org.xnio.Pooled; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * A buffered binary message. + * + * @author Stuart Douglas + */ +public class BufferedBinaryMessage { + + private final boolean bufferFullMessage; + private List> data = new ArrayList<>(1); + private Pooled current; + private final long maxMessageSize; + private long currentSize; + private boolean complete; + private int frameCount; + + + public BufferedBinaryMessage(long maxMessageSize, boolean bufferFullMessage) { + this.bufferFullMessage = bufferFullMessage; + this.maxMessageSize = maxMessageSize; + } + + public BufferedBinaryMessage(boolean bufferFullMessage) { + this(-1, bufferFullMessage); + } + + public void readBlocking(StreamSourceFrameChannel channel) throws IOException { + if (current == null) { + current = channel.getWebSocketChannel().getBufferPool().allocate(); + } + for (; ; ) { + int res = channel.read(current.getResource()); + if (res == -1) { + complete = true; + return; + } else if (res == 0) { + channel.awaitReadable(); + } + checkMaxSize(channel, res); + if (bufferFullMessage) { + dealWithFullBuffer(channel); + } else if (!current.getResource().hasRemaining()) { + return; + } + } + } + + private void dealWithFullBuffer(StreamSourceFrameChannel channel) { + if (!current.getResource().hasRemaining()) { + current.getResource().flip(); + data.add(current); + current = channel.getWebSocketChannel().getBufferPool().allocate(); + } + } + + public void read(final StreamSourceFrameChannel channel, final WebSocketCallback callback) { + try { + for (; ; ) { + if (current == null) { + current = channel.getWebSocketChannel().getBufferPool().allocate(); + } + int res = channel.read(current.getResource()); + if (res == -1) { + this.complete = true; + callback.complete(channel.getWebSocketChannel(), this); + return; + } else if (res == 0) { + channel.getReadSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSourceFrameChannel channel) { + if(complete ) { + return; + } + try { + for (; ; ) { + if (current == null) { + current = channel.getWebSocketChannel().getBufferPool().allocate(); + } + int res = channel.read(current.getResource()); + if (res == -1) { + complete = true; + channel.suspendReads(); + callback.complete(channel.getWebSocketChannel(), BufferedBinaryMessage.this); + return; + } else if (res == 0) { + return; + } + + checkMaxSize(channel, res); + if (bufferFullMessage) { + dealWithFullBuffer(channel); + } else if (!current.getResource().hasRemaining()) { + callback.complete(channel.getWebSocketChannel(), BufferedBinaryMessage.this); + } else { + handleNewFrame(channel, callback); + } + } + } catch (IOException e) { + channel.suspendReads(); + callback.onError(channel.getWebSocketChannel(), BufferedBinaryMessage.this, e); + } + } + }); + channel.resumeReads(); + return; + } + + checkMaxSize(channel, res); + if (bufferFullMessage) { + dealWithFullBuffer(channel); + } else if (!current.getResource().hasRemaining()) { + callback.complete(channel.getWebSocketChannel(), BufferedBinaryMessage.this); + } else { + handleNewFrame(channel, callback); + } + } + } catch (IOException e) { + callback.onError(channel.getWebSocketChannel(), this, e); + } + } + + private void handleNewFrame(StreamSourceFrameChannel channel, final WebSocketCallback callback) { + //TODO: remove this crap + //basically some bogus web sockets TCK tests assume that messages will be broken up into frames + //even if we have the full message available. + if(!bufferFullMessage) { + if(channel.getWebSocketFrameCount() != frameCount && current != null && !channel.isFinalFragment()) { + frameCount = channel.getWebSocketFrameCount(); + callback.complete(channel.getWebSocketChannel(), this); + } + } + } + + private void checkMaxSize(StreamSourceFrameChannel channel, int res) throws IOException { + currentSize += res; + if (maxMessageSize > 0 && currentSize > maxMessageSize) { + WebSockets.sendClose(new CloseMessage(CloseMessage.MSG_TOO_BIG, WebSocketMessages.MESSAGES.messageToBig(maxMessageSize)).toByteBuffer(), channel.getWebSocketChannel(), null); + throw new IOException(WebSocketMessages.MESSAGES.messageToBig(maxMessageSize)); + } + } + + public Pooled getData() { + if (current == null) { + return new ImmediatePooled<>(new ByteBuffer[0]); + } + if (data.isEmpty()) { + final Pooled current = this.current; + current.getResource().flip(); + this.current = null; + final ByteBuffer[] data = new ByteBuffer[]{current.getResource()}; + return new PooledByteBufferArray(Collections.>singletonList(current), data); + } + current.getResource().flip(); + data.add(current); + current = null; + ByteBuffer[] ret = new ByteBuffer[data.size()]; + for (int i = 0; i < data.size(); ++i) { + ret[i] = data.get(i).getResource(); + } + List> data = this.data; + this.data = new ArrayList<>(); + + return new PooledByteBufferArray(data, ret); + } + + public boolean isComplete() { + return complete; + } + + private static final class PooledByteBufferArray implements Pooled { + + private final List> pooled; + private final ByteBuffer[] data; + + private PooledByteBufferArray(List> pooled, ByteBuffer[] data) { + this.pooled = pooled; + this.data = data; + } + + @Override + public void discard() { + for (Pooled item : pooled) { + item.discard(); + } + } + + @Override + public void free() { + for (Pooled item : pooled) { + item.free(); + } + } + + @Override + public ByteBuffer[] getResource() throws IllegalStateException { + return data; + } + + @Override + public void close() { + free(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/BufferedTextMessage.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/BufferedTextMessage.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/BufferedTextMessage.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,195 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import org.xnio.ChannelListener; +import org.xnio.Pooled; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * A buffered text message. + * + * @author Stuart Douglas + */ +public class BufferedTextMessage { + + private final UTF8Output data = new UTF8Output(); + + private final boolean bufferFullMessage; + private final long maxMessageSize; + private boolean complete; + long currentSize; + + /** + * @param maxMessageSize The maximum message size + * @param bufferFullMessage If the complete message should be buffered + */ + public BufferedTextMessage(long maxMessageSize, boolean bufferFullMessage) { + this.maxMessageSize = maxMessageSize; + this.bufferFullMessage = bufferFullMessage; + } + + public BufferedTextMessage(boolean bufferFullMessage) { + this(-1, bufferFullMessage); + } + + private void checkMaxSize(StreamSourceFrameChannel channel, int res) throws IOException { + if(res > 0) { + currentSize += res; + } + if (maxMessageSize > 0 && currentSize > maxMessageSize) { + WebSockets.sendClose(new CloseMessage(CloseMessage.MSG_TOO_BIG, WebSocketMessages.MESSAGES.messageToBig(maxMessageSize)).toByteBuffer(), channel.getWebSocketChannel(), null); + throw new IOException(WebSocketMessages.MESSAGES.messageToBig(maxMessageSize)); + } + } + + public void readBlocking(StreamSourceFrameChannel channel) throws IOException { + Pooled pooled = channel.getWebSocketChannel().getBufferPool().allocate(); + final ByteBuffer buffer = pooled.getResource(); + try { + for (; ; ) { + int res = channel.read(buffer); + if (res == -1) { + buffer.flip(); + data.write(buffer); + this.complete = true; + return; + } else if (res == 0) { + channel.awaitReadable(); + } + checkMaxSize(channel, res); + if (!buffer.hasRemaining()) { + buffer.flip(); + data.write(buffer); + buffer.compact(); + if (!bufferFullMessage) { + //if we are not reading the full message we return + return; + } + } + } + } finally { + pooled.free(); + } + } + + public void read(final StreamSourceFrameChannel channel, final WebSocketCallback callback) { + Pooled pooled = channel.getWebSocketChannel().getBufferPool().allocate(); + final ByteBuffer buffer = pooled.getResource(); + try { + try { + for (; ; ) { + int res = channel.read(buffer); + if (res == -1) { + this.complete = true; + buffer.flip(); + data.write(buffer); + callback.complete(channel.getWebSocketChannel(), this); + return; + } else if (res == 0) { + buffer.flip(); + if (buffer.hasRemaining()) { + data.write(buffer); + if (!bufferFullMessage) { + callback.complete(channel.getWebSocketChannel(), this); + } + } + channel.getReadSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSourceFrameChannel channel) { + if(complete ) { + return; + } + Pooled pooled = channel.getWebSocketChannel().getBufferPool().allocate(); + final ByteBuffer buffer = pooled.getResource(); + try { + try { + for (; ; ) { + int res = channel.read(buffer); + if (res == -1) { + checkMaxSize(channel, res); + buffer.flip(); + data.write(buffer); + complete = true; + callback.complete(channel.getWebSocketChannel(), BufferedTextMessage.this); + return; + } else if (res == 0) { + buffer.flip(); + if (buffer.hasRemaining()) { + data.write(buffer); + if (!bufferFullMessage) { + callback.complete(channel.getWebSocketChannel(), BufferedTextMessage.this); + } + } + return; + } + if (!buffer.hasRemaining()) { + buffer.flip(); + data.write(buffer); + buffer.clear(); + if (!bufferFullMessage) { + callback.complete(channel.getWebSocketChannel(), BufferedTextMessage.this); + } + } + } + } catch (IOException e) { + callback.onError(channel.getWebSocketChannel(), BufferedTextMessage.this, e); + } + } finally { + pooled.free(); + } + } + }); + channel.resumeReads(); + return; + } + checkMaxSize(channel, res); + if (!buffer.hasRemaining()) { + buffer.flip(); + data.write(buffer); + buffer.clear(); + if (!bufferFullMessage) { + callback.complete(channel.getWebSocketChannel(), this); + } + } + } + } catch (IOException e) { + callback.onError(channel.getWebSocketChannel(), this, e); + } + } finally { + pooled.free(); + } + } + + /** + * Gets the buffered data and clears the buffered text message. If this is not called on a UTF8 + * character boundary there may be partial code point data that is still buffered. + * + * @return The data + */ + public String getData() { + return data.extract(); + } + + public boolean isComplete() { + return complete; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/CloseMessage.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/CloseMessage.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/CloseMessage.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,95 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +/** + * A close message + * + * @author Stuart Douglas + */ +public class CloseMessage { + + private static final Charset utf8 = Charset.forName("UTF-8"); + + private final int code; + private final String reason; + /* + * For the exact meaning of the codes refer to the WebSocket + * RFC Section 7.4. + */ + public static final int NORMAL_CLOSURE = 1000; + public static final int GOING_AWAY = 1001; + public static final int WRONG_CODE = 1002; + public static final int PROTOCOL_ERROR = 1003; + public static final int MSG_CONTAINS_INVALID_DATA = 1007; + public static final int MSG_VIOLATES_POLICY = 1008; + public static final int MSG_TOO_BIG = 1009; + public static final int MISSING_EXTENSIONS = 1010; + public static final int UNEXPECTED_ERROR = 1011; + + public CloseMessage(final ByteBuffer buffer) { + if(buffer.remaining() >= 2) { + code = (buffer.get() & 0XFF) << 8 | (buffer.get() & 0xFF); + reason = new UTF8Output(buffer).extract(); + } else { + code = GOING_AWAY; + reason = ""; + } + } + + public CloseMessage(int code, String reason) { + this.code = code; + this.reason = reason == null ? "" : reason; + } + + public CloseMessage(final ByteBuffer[] buffers) { + this(WebSockets.mergeBuffers(buffers)); + } + + public String getReason() { + return reason; + } + + public int getCode() { + return code; + } + + public ByteBuffer toByteBuffer() { + byte[] data = reason.getBytes(utf8); + ByteBuffer buffer = ByteBuffer.allocate(data.length + 2); + buffer.putShort((short) code); + buffer.put(data); + buffer.flip(); + return buffer; + } + + /** + * Return {@code true} if the provided code is a valid close status code. + */ + public static boolean isValid(int code) { + if (code >= 0 && code <= 999 || code >= 1004 && code <= 1006 + || code >= 1012 && code <= 2999) { + return false; + } + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/FixedPayloadFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/FixedPayloadFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/FixedPayloadFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,145 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.websockets.core.function.ChannelFunction; +import io.undertow.websockets.core.function.ChannelFunctionFileChannel; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +/** + * A StreamSourceFrameChannel that is used to read a Frame with a fixed sized payload. + * + * @author Norman Maurer + */ +public abstract class FixedPayloadFrameSourceChannel extends StreamSourceFrameChannel { + + private final ChannelFunction[] functions; + + protected FixedPayloadFrameSourceChannel(WebSocketChannel wsChannel, WebSocketFrameType type, long payloadSize, int rsv, boolean finalFragment, Pooled pooled, long frameLength, ChannelFunction... functions) { + super(wsChannel, type, payloadSize, rsv, finalFragment, pooled, frameLength); + this.functions = functions; + } + + @Override + protected void handleHeaderData(FrameHeaderData headerData) { + super.handleHeaderData(headerData); + if(functions != null) { + for(ChannelFunction func : functions) { + func.newFrame(headerData); + } + } + } + + @Override + public final long transferTo(long position, long count, FileChannel target) throws IOException { + long r; + if (functions != null && functions.length > 0) { + r = super.transferTo(position, count, new ChannelFunctionFileChannel(target, functions)); + } else { + r = super.transferTo(position, count, target); + } + return r; + } + + @Override + public final long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + // use this because of XNIO bug + // See https://issues.jboss.org/browse/XNIO-185 + return WebSocketUtils.transfer(this, count, throughBuffer, target); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int position = dst.position(); + int r = super.read(dst); + if (r > 0) { + afterRead(dst, position, r); + } + return r; + } + + @Override + public final long read(ByteBuffer[] dsts) throws IOException { + return read(dsts, 0, dsts.length); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + Bounds[] old = new Bounds[length]; + for (int i = offset; i < length; i++) { + ByteBuffer dst = dsts[i]; + old[i - offset] = new Bounds(dst.position(), dst.limit()); + } + long b = super.read(dsts, offset, length); + if (b > 0) { + for (int i = offset; i < length; i++) { + ByteBuffer dst = dsts[i]; + int oldPos = old[i - offset].position; + afterRead(dst, oldPos, dst.position() - oldPos); + } + } + return b; + } + + /** + * Called after data was read into the {@link ByteBuffer} + * + * @param buffer the {@link ByteBuffer} into which the data was read + * @param position the position it was written to + * @param length the number of bytes there were written + * @throws IOException thrown if an error occurs + */ + protected void afterRead(ByteBuffer buffer, int position, int length) throws IOException { + try { + for (ChannelFunction func : functions) { + func.afterRead(buffer, position, length); + } + if (isComplete()) { + try { + for (ChannelFunction func : functions) { + func.complete(); + } + } catch (UnsupportedEncodingException e) { + getFramedChannel().markReadsBroken(e); + throw e; + } + } + } catch (UnsupportedEncodingException e) { + getFramedChannel().markReadsBroken(e); + throw e; + } + + } + + private static class Bounds { + final int position; + final int limit; + + Bounds(int position, int limit) { + this.position = position; + this.limit = limit; + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/InvalidOpCodeException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/InvalidOpCodeException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/InvalidOpCodeException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * @author Stuart Douglas + */ +public class InvalidOpCodeException extends WebSocketException { + + public InvalidOpCodeException() { + } + + public InvalidOpCodeException(String msg, Throwable cause) { + super(msg, cause); + } + + public InvalidOpCodeException(String msg) { + super(msg); + } + + public InvalidOpCodeException(Throwable cause) { + super(cause); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/SendChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/SendChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/SendChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,26 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * Marker interface for channels that send frames. + * + * @author Norman Maurer + */ +interface SendChannel { +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/StreamSinkFrameChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/StreamSinkFrameChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/StreamSinkFrameChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,90 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSinkChannel; + +/** + * @author Norman Maurer + */ +public abstract class StreamSinkFrameChannel extends AbstractFramedStreamSinkChannel { + + private final WebSocketFrameType type; + + private int rsv; + + protected StreamSinkFrameChannel(WebSocketChannel channel, WebSocketFrameType type) { + super(channel); + this.type = type; + } + + /** + * Return the RSV for the extension. Default is 0. + */ + public int getRsv() { + return rsv; + } + + /** + * Set the RSV which is used for extensions. + *

+ * This can only be set before any write or transfer operations where passed + * to the wrapped {@link org.xnio.channels.StreamSinkChannel}, after that an {@link IllegalStateException} will be thrown. + * + */ + public void setRsv(int rsv) { + if (!areExtensionsSupported() && rsv != 0) { + throw WebSocketMessages.MESSAGES.extensionsNotSupported(); + } + this.rsv = rsv; + } + + /** + * {@code true} if fragmentation is supported for the {@link WebSocketFrameType}. + */ + public boolean isFragmentationSupported() { + return false; + } + + /** + * {@code true} if extensions are supported for the {@link WebSocketFrameType}. + */ + public boolean areExtensionsSupported() { + return false; + } + + /** + * Return the {@link WebSocketFrameType} for which the {@link StreamSinkFrameChannel} was obtained. + */ + public WebSocketFrameType getType() { + return type; + } + + public WebSocketChannel getWebSocketChannel() { + return getChannel(); + } + + @Override + protected boolean isLastFrame() { + return type == WebSocketFrameType.CLOSE; + } + + public boolean isFinalFragment() { + return super.isFinalFrameQueued(); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/StreamSourceFrameChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/StreamSourceFrameChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/StreamSourceFrameChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,132 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import java.io.IOException; +import java.nio.ByteBuffer; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; + +/** + * Base class for processes Frame bases StreamSourceChannels. + * + * @author Norman Maurer + */ +public abstract class StreamSourceFrameChannel extends AbstractFramedStreamSourceChannel { + + protected final WebSocketFrameType type; + + private boolean finalFragment; + private final int rsv; + private final long payloadSize; + + protected StreamSourceFrameChannel(WebSocketChannel wsChannel, WebSocketFrameType type, long payloadSize, Pooled pooled, long frameLength) { + this(wsChannel, type, payloadSize, 0, true, pooled, frameLength); + } + + protected StreamSourceFrameChannel(WebSocketChannel wsChannel, WebSocketFrameType type, long payloadSize, int rsv, boolean finalFragment, Pooled pooled, long frameLength) { + super(wsChannel, pooled, frameLength); + this.type = type; + this.finalFragment = finalFragment; + this.rsv = rsv; + this.payloadSize = payloadSize; + } + + /** + * Return the {@link WebSocketFrameType} or {@code null} if its not known at the calling time. + */ + public WebSocketFrameType getType() { + return type; + } + + /** + * Flag to indicate if this frame is the final fragment in a message. The first fragment (frame) may also be the + * final fragment. + */ + public boolean isFinalFragment() { + return finalFragment; + } + + /** + * Return the rsv which is used for extensions. + */ + public int getRsv() { + return rsv; + } + + int getWebSocketFrameCount() { + return getReadFrameCount(); + } + + /** + * Discard the frame, which means all data that would be part of the frame will be discarded. + *

+ * Once all is discarded it will call {@link #close()} + */ + public void discard() throws IOException { + if (isOpen()) { + ChannelListener drainListener = ChannelListeners.drainListener(Long.MAX_VALUE, + new ChannelListener() { + @Override + public void handleEvent(StreamSourceChannel channel) { + IoUtils.safeClose(StreamSourceFrameChannel.this); + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSourceChannel channel, IOException exception) { + getFramedChannel().markReadsBroken(exception); + } + } + ); + getReadSetter().set(drainListener); + resumeReads(); + } else { + close(); + } + } + + @Override + protected WebSocketChannel getFramedChannel() { + return (WebSocketChannel) super.getFramedChannel(); + } + + public WebSocketChannel getWebSocketChannel() { + return getFramedChannel(); + } + + public void finalFrame() { + this.lastFrame(); + this.finalFragment = true; + } + + @Override + protected void handleHeaderData(FrameHeaderData headerData) { + super.handleHeaderData(headerData); + if (((WebSocketFrame) headerData).isFinalFragment()) { + finalFrame(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/UTF8Output.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/UTF8Output.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/UTF8Output.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,98 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import java.nio.ByteBuffer; + +import org.xnio.Buffers; + +/** + * Utility class which allows to extract a UTF8 String from bytes respecting valid code-points + */ +public final class UTF8Output { + private static final int UTF8_ACCEPT = 0; + + private static final byte[] TYPES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, + 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8}; + + private static final byte[] STATES = {0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, + 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, + 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 36, + 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12}; + + @SuppressWarnings("RedundantFieldInitialization") + private byte state = UTF8_ACCEPT; + private int codep; + + private final StringBuilder stringBuilder; + + public UTF8Output(ByteBuffer... payload) { + stringBuilder = new StringBuilder((int) Buffers.remaining(payload)); + write(payload); + } + + public UTF8Output() { + stringBuilder = new StringBuilder(); + } + + public void write(ByteBuffer... bytes) { + for (ByteBuffer buf : bytes) { + while (buf.hasRemaining()) { + write(buf.get()); + } + } + } + + private void write(byte b) { + byte type = TYPES[b & 0xFF]; + + codep = state != UTF8_ACCEPT ? b & 0x3f | codep << 6 : 0xff >> type & b; + + state = STATES[state + type]; + + if (state == UTF8_ACCEPT) { + for (char c : Character.toChars(codep)) { + stringBuilder.append(c); + } + } + } + + /** + * Extract a String holding the utf8 text + */ + public String extract() { + String text = stringBuilder.toString(); + stringBuilder.setLength(0); + return text; + } + + public boolean hasData() { + return stringBuilder.length() != 0; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketCallback.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketCallback.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketCallback.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,30 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * @author Stuart Douglas + */ +public interface WebSocketCallback { + + void complete(final WebSocketChannel channel, T context); + + void onError(final WebSocketChannel channel, T context, Throwable throwable); + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,418 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.conduits.IdleTimeoutConduit; +import io.undertow.server.protocol.framed.AbstractFramedChannel; +import io.undertow.server.protocol.framed.FrameHeaderData; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; +import org.xnio.channels.StreamSinkChannel; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * A {@link org.xnio.channels.ConnectedChannel} which can be used to send and receive WebSocket Frames. + * + * @author Norman Maurer + * @author Stuart Douglas + */ +public abstract class WebSocketChannel extends AbstractFramedChannel { + + private final boolean client; + + private final WebSocketVersion version; + private final String wsUrl; + + private boolean closeFrameReceived; + private boolean closeFrameSent; + private final String subProtocol; + private final boolean extensionsSupported; + /** + * an incoming frame that has not been created yet + */ + private volatile PartialFrame partialFrame; + + private final Map attributes = Collections.synchronizedMap(new HashMap()); + + protected StreamSourceFrameChannel fragmentedChannel; + + /** + * Represents all web socket channels that are attached to the same endpoint. + */ + private final Set peerConnections; + + /** + * Create a new {@link WebSocketChannel} + * 8 + * + * @param connectedStreamChannel The {@link org.xnio.channels.ConnectedStreamChannel} over which the WebSocket Frames should get send and received. + * Be aware that it already must be "upgraded". + * @param bufferPool The {@link org.xnio.Pool} which will be used to acquire {@link java.nio.ByteBuffer}'s from. + * @param version The {@link WebSocketVersion} of the {@link WebSocketChannel} + * @param wsUrl The url for which the channel was created. + * @param client + * @param peerConnections The concurrent set that is used to track open connections associtated with an endpoint + */ + protected WebSocketChannel(final StreamConnection connectedStreamChannel, Pool bufferPool, WebSocketVersion version, String wsUrl, String subProtocol, final boolean client, boolean extensionsSupported, Set peerConnections) { + super(connectedStreamChannel, bufferPool, new WebSocketFramePriority(), null); + this.client = client; + this.version = version; + this.wsUrl = wsUrl; + this.extensionsSupported = extensionsSupported; + this.subProtocol = subProtocol; + this.peerConnections = peerConnections; + addCloseTask(new ChannelListener() { + @Override + public void handleEvent(WebSocketChannel channel) { + WebSocketChannel.this.peerConnections.remove(WebSocketChannel.this); + } + }); + } + + @Override + protected IdleTimeoutConduit createIdleTimeoutChannel(final StreamConnection connectedStreamChannel) { + return new IdleTimeoutConduit(connectedStreamChannel.getSinkChannel().getConduit(), connectedStreamChannel.getSourceChannel().getConduit()) { + @Override + protected void doClose() { + WebSockets.sendClose(CloseMessage.GOING_AWAY, null, WebSocketChannel.this, null); + } + }; + } + + @Override + protected boolean isLastFrameSent() { + return closeFrameSent; + } + + @Override + protected boolean isLastFrameReceived() { + return closeFrameReceived; + } + + @Override + protected void markReadsBroken(Throwable cause) { + super.markReadsBroken(cause); + } + + @Override + protected void lastDataRead() { + if(!closeFrameReceived && !closeFrameSent) { + //the peer has likely already gone away, but try and send a close frame anyway + //this will likely just result in the write() failing an immediate connection termination + //which is what we want + closeFrameReceived = true; //not strictly true, but the read side is gone + try { + sendClose(); + } catch (IOException e) { + IoUtils.safeClose(this); + } + } + } + + protected boolean isReadsBroken() { + return super.isReadsBroken(); + } + + @Override + protected FrameHeaderData parseFrame(ByteBuffer data) throws IOException { + if (partialFrame == null) { + partialFrame = receiveFrame(); + } + try { + partialFrame.handle(data); + } catch (WebSocketException e) { + //the data was corrupt + markReadsBroken(e); + if (WebSocketLogger.REQUEST_LOGGER.isDebugEnabled()) { + WebSocketLogger.REQUEST_LOGGER.debugf(e, "receive failed due to Exception"); + } + //send a close message + WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + + throw new IOException(e); + } + if (partialFrame.isDone()) { + PartialFrame p = this.partialFrame; + this.partialFrame = null; + return p; + } + return null; + } + + + /** + * Create a new {@link io.undertow.websockets.core.StreamSourceFrameChannel} which can be used to read the data of the received Frame + * + * @return channel A {@link io.undertow.websockets.core.StreamSourceFrameChannel} will be used to read a Frame from. + * This will return {@code null} if the right {@link io.undertow.websockets.core.StreamSourceFrameChannel} could not be detected with the given + * buffer and so more data is needed. + */ + protected abstract PartialFrame receiveFrame(); + + @Override + protected StreamSourceFrameChannel createChannel(FrameHeaderData frameHeaderData, Pooled frameData) { + PartialFrame partialFrame = (PartialFrame) frameHeaderData; + StreamSourceFrameChannel channel = partialFrame.getChannel(frameData); + if (channel.getType() == WebSocketFrameType.CLOSE) { + closeFrameReceived = true; + } + return channel; + } + + public final boolean setAttribute(String key, Object value) { + if (value == null) { + return attributes.remove(key) != null; + } else { + return attributes.put(key, value) == null; + } + } + + public final Object getAttribute(String key) { + return attributes.get(key); + } + + /** + * Returns {@code true} if extensions are supported by this WebSocket Channel. + */ + public boolean areExtensionsSupported() { + return extensionsSupported; + } + + @Override + protected void handleBrokenSourceChannel(Throwable e) { + if (e instanceof UnsupportedEncodingException) { + getFramePriority().immediateCloseFrame(); + WebSockets.sendClose(new CloseMessage(CloseMessage.MSG_CONTAINS_INVALID_DATA, e.getMessage()).toByteBuffer(), this, null); + } else if (e instanceof WebSocketInvalidCloseCodeException) { + WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + } else if (e instanceof WebSocketFrameCorruptedException) { + getFramePriority().immediateCloseFrame(); + WebSockets.sendClose(new CloseMessage(CloseMessage.WRONG_CODE, e.getMessage()).toByteBuffer(), this, null); + } + } + + @Override + protected void handleBrokenSinkChannel(Throwable e) { + + } + + /** + * Returns an unmodifiable {@link Set} of the selected subprotocols if any. + */ + @Deprecated + public Set getSubProtocols() { + return Collections.singleton(subProtocol); + } + + public String getSubProtocol() { + return subProtocol; + } + + public boolean isCloseFrameReceived() { + return closeFrameReceived; + } + + public boolean isCloseFrameSent() { + return closeFrameSent; + } + + /** + * Get the request URI scheme. Normally this is one of {@code ws} or {@code wss}. + * + * @return the request URI scheme + */ + public String getRequestScheme() { + if (getUrl().startsWith("wss:")) { + return "wss"; + } else { + return "ws"; + } + } + + /** + * Return {@code true} if this is handled via WebSocket Secure. + */ + public boolean isSecure() { + return "wss".equals(getRequestScheme()); + } + + /** + * Return the URL of the WebSocket endpoint. + * + * @return url The URL of the endpoint + */ + public String getUrl() { + return wsUrl; + } + + /** + * Return the {@link WebSocketVersion} which is used + * + * @return version The {@link WebSocketVersion} which is in use + */ + public WebSocketVersion getVersion() { + return version; + } + + /** + * Get the source address of the WebSocket Channel. + * + * @return the source address of the WebSocket Channel + */ + public InetSocketAddress getSourceAddress() { + return getPeerAddress(InetSocketAddress.class); + } + + /** + * Get the destination address of the WebSocket Channel. + * + * @return the destination address of the WebSocket Channel + */ + public InetSocketAddress getDestinationAddress() { + return getLocalAddress(InetSocketAddress.class); + } + + public boolean isClient() { + return client; + } + + /** + * Returns a new {@link StreamSinkFrameChannel} for sending the given {@link WebSocketFrameType} with the given payload. + * If this method is called multiple times, subsequent {@link StreamSinkFrameChannel}'s will not be writable until all previous frames + * were completely written. + * + * @param type The {@link WebSocketFrameType} for which a {@link StreamSinkChannel} should be created + * @param payloadSize The size of the payload which will be included in the WebSocket Frame. This may be 0 if you want + * to transmit no payload at all. + */ + public final StreamSinkFrameChannel send(WebSocketFrameType type, long payloadSize) throws IOException { + if(closeFrameSent || (closeFrameReceived && type != WebSocketFrameType.CLOSE)) { + throw WebSocketMessages.MESSAGES.channelClosed(); + } + if (payloadSize < 0) { + throw WebSocketMessages.MESSAGES.negativePayloadLength(); + } + if (isWritesBroken()) { + throw WebSocketMessages.MESSAGES.streamIsBroken(); + } + + + StreamSinkFrameChannel ch = createStreamSinkChannel(type, payloadSize); + getFramePriority().addToOrderQueue(ch); + if (type == WebSocketFrameType.CLOSE) { + closeFrameSent = true; + } + return ch; + } + + /** + * Returns a new {@link StreamSinkFrameChannel} for sending the given {@link WebSocketFrameType} with the given payload. + * If this method is called multiple times, subsequent {@link StreamSinkFrameChannel}'s will not be writable until all previous frames + * were completely written. + * + * @param type The {@link WebSocketFrameType} for which a {@link StreamSinkChannel} should be created + */ + public final StreamSinkFrameChannel send(WebSocketFrameType type) throws IOException { + if (isWritesBroken()) { + throw WebSocketMessages.MESSAGES.streamIsBroken(); + } + StreamSinkFrameChannel ch = createStreamSinkChannel(type, -1); + getFramePriority().addToOrderQueue(ch); + if (type == WebSocketFrameType.CLOSE) { + closeFrameSent = true; + } + return ch; + } + + /** + * Send a Close frame without a payload + */ + public void sendClose() throws IOException { + StreamSinkFrameChannel closeChannel = send(WebSocketFrameType.CLOSE, 0); + closeChannel.shutdownWrites(); + if (!closeChannel.flush()) { + closeChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener( + null, new ChannelExceptionHandler() { + @Override + public void handleException(final StreamSinkChannel channel, final IOException exception) { + IoUtils.safeClose(WebSocketChannel.this); + } + } + )); + closeChannel.resumeWrites(); + } + } + + + /** + * Create a new StreamSinkFrameChannel which can be used to send a WebSocket Frame of the type {@link WebSocketFrameType}. + * + * @param type The {@link WebSocketFrameType} of the WebSocketFrame which will be send over this {@link StreamSinkFrameChannel} + * @param payloadSize The size of the payload to transmit. May be 0 if non payload at all should be included, or -1 if unknown + */ + protected abstract StreamSinkFrameChannel createStreamSinkChannel(WebSocketFrameType type, long payloadSize); + + + protected WebSocketFramePriority getFramePriority() { + return (WebSocketFramePriority) super.getFramePriority(); + } + + /** + * Returns all 'peer' web socket connections that were created from the same endpoint. + * + * + * @return all 'peer' web socket connections + */ + public Set getPeerConnections() { + return Collections.unmodifiableSet(peerConnections); + } + + /** + * Interface that represents a frame channel that is in the process of being created + */ + public interface PartialFrame extends FrameHeaderData { + + /** + * @return The channel, or null if the channel is not available yet + */ + StreamSourceFrameChannel getChannel(final Pooled data); + + /** + * Handles the data, any remaining data will be pushed back + */ + void handle(ByteBuffer data) throws WebSocketException; + + /** + * @return true if the channel is available + */ + boolean isDone(); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,46 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import java.io.IOException; + +/** + * Base class for all WebSocket Exceptions + * + * @author Norman Maurer + */ +public class WebSocketException extends IOException { + + private static final long serialVersionUID = -6784834646314672530L; + + public WebSocketException() { + } + + public WebSocketException(String msg, Throwable cause) { + super(msg, cause); + } + + public WebSocketException(String msg) { + super(msg); + } + + public WebSocketException(Throwable cause) { + super(cause); + } + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrame.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrame.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrame.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,28 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * @author Stuart Douglas + */ +public interface WebSocketFrame extends WebSocketChannel.PartialFrame { + + boolean isFinalFragment(); + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrameCorruptedException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrameCorruptedException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrameCorruptedException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * WebSocketException which will be thrown if a corrupted frame was detected + * + * @author Norman Maurer + */ +public class WebSocketFrameCorruptedException extends WebSocketException { + + private static final long serialVersionUID = -6784834646314476130L; + + public WebSocketFrameCorruptedException() { + } + + public WebSocketFrameCorruptedException(String msg, Throwable cause) { + super(msg, cause); + } + + public WebSocketFrameCorruptedException(String msg) { + super(msg); + } + + public WebSocketFrameCorruptedException(Throwable cause) { + super(cause); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFramePriority.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFramePriority.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFramePriority.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,153 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.server.protocol.framed.FramePriority; + +import java.util.Deque; +import java.util.List; +import java.util.Queue; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * Web socket frame priority + * + * @author Stuart Douglas + */ +public class WebSocketFramePriority implements FramePriority { + + /** + * Strict ordering queue. Makes sure that the initial frame for a stream is sent in the order that send() is called. + *

+ * Required to pass the autobahn test suite with no non-strict performance. + *

+ * TODO: provide a way to disable this. + */ + private final Queue strictOrderQueue = new ConcurrentLinkedDeque<>(); + private StreamSinkFrameChannel currentFragmentedSender; + boolean closed = false; + boolean immediateCloseFrame = false; + + @Override + public boolean insertFrame(StreamSinkFrameChannel newFrame, List pendingFrames) { + + if (newFrame.getType() != WebSocketFrameType.PONG && + newFrame.getType() != WebSocketFrameType.PING) { + StreamSinkFrameChannel order = strictOrderQueue.peek(); + if (order != null) { + if (order != newFrame && order.isOpen()) { + //generally we want to queue close frames immediately + //however if the close frame is initiated from this side we respect the ordering + //if the close frame is from the other side we have to echo it back immediately + if (newFrame.getType() != WebSocketFrameType.CLOSE) { + return false; + } else if (!newFrame.getWebSocketChannel().isCloseFrameReceived() && !immediateCloseFrame) { + return false; + } + } + if(order == newFrame && newFrame.isWritesShutdown()) { + strictOrderQueue.poll(); + } + } + } + + if (closed) { + //drop the frame + newFrame.markBroken(); + return true; + } + if (currentFragmentedSender == null) { + //we are not sending fragmented + if (!newFrame.isWritesShutdown()) { + //start of a fragmented message + currentFragmentedSender = newFrame; + } + if (pendingFrames.isEmpty()) { + pendingFrames.add(newFrame); + } else if (newFrame.getType() == WebSocketFrameType.PING || + newFrame.getType() == WebSocketFrameType.PONG) { + //add at the start of the queue + pendingFrames.add(1, newFrame); + } else { + pendingFrames.add(newFrame); + } + } else if (newFrame.getType() == WebSocketFrameType.PING || + newFrame.getType() == WebSocketFrameType.PONG) { + //we stick ping and pong in the middle of fragmentation + if (pendingFrames.isEmpty()) { + pendingFrames.add(newFrame); + } else { + pendingFrames.add(1, newFrame); + } + } else { + //we are currently sending fragmented, we can't queue and non control messages + if (currentFragmentedSender != newFrame) { + return false; + } else { + if (newFrame.isWritesShutdown()) { + currentFragmentedSender = null; + } + pendingFrames.add(newFrame); + } + } + if (newFrame.getType() == WebSocketFrameType.CLOSE) { + closed = true; + } + return true; + } + + @Override + public void frameAdded(StreamSinkFrameChannel addedFrame, List pendingFrames, Deque holdFrames) { + if (addedFrame.isFinalFragment()) { + while (true) { + StreamSinkFrameChannel frame = strictOrderQueue.peek(); + if(frame == null) { + break; + } + if(holdFrames.contains(frame)) { + if(insertFrame(frame, pendingFrames)) { + holdFrames.remove(frame); + } else { + break; + } + } else { + break; + } + } + while (!holdFrames.isEmpty()) { + StreamSinkFrameChannel frame = holdFrames.peek(); + if (insertFrame(frame, pendingFrames)) { + holdFrames.poll(); + } else { + return; + } + } + } + } + + void addToOrderQueue(final StreamSinkFrameChannel channel) { + if (channel.getType() != WebSocketFrameType.PING && channel.getType() != WebSocketFrameType.PONG) { + strictOrderQueue.add(channel); + } + } + + void immediateCloseFrame() { + this.immediateCloseFrame = true; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrameType.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrameType.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketFrameType.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * The different WebSocketFrame types which are out there. + * + * @author Norman Maurer + */ +public enum WebSocketFrameType { + + /** + * WebSocketFrame contains binary data + */ + BINARY, + + /** + * WebSocketFrame contains UTF-8 encoded {@link String} + */ + TEXT, + + /** + * WebSocketFrame which represent a ping request + */ + PING, + + /** + * WebSocketFrame which should be issued after a {@link #PING} was received + */ + PONG, + + /** + * WebSocketFrame which requests the close of the WebSockets connection + */ + CLOSE, + + /** + * WebSocketFrame which notify about more data to come + */ + CONTINUATION, + + /** + * Unknown frame-type + */ + UNKOWN, + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketHandshakeException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketHandshakeException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketHandshakeException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * {@link WebSocketException} which should be used during the WebSocket-Handshake processing. + * + * @author Norman Maurer + */ +public class WebSocketHandshakeException extends WebSocketException { + + private static final long serialVersionUID = 1L; + + public WebSocketHandshakeException() { + } + + public WebSocketHandshakeException(String s) { + super(s); + } + + public WebSocketHandshakeException(String s, Throwable throwable) { + super(s, throwable); + } + + public WebSocketHandshakeException(final Throwable cause) { + super(cause); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketInvalidCloseCodeException.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketInvalidCloseCodeException.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketInvalidCloseCodeException.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +/** + * WebSocketException which will be thrown if a corrupted frame was detected + * + * @author Norman Maurer + */ +public class WebSocketInvalidCloseCodeException extends WebSocketException { + + private static final long serialVersionUID = -6784834646314476130L; + + public WebSocketInvalidCloseCodeException() { + } + + public WebSocketInvalidCloseCodeException(String msg, Throwable cause) { + super(msg, cause); + } + + public WebSocketInvalidCloseCodeException(String msg) { + super(msg); + } + + public WebSocketInvalidCloseCodeException(Throwable cause) { + super(cause); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketLogger.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketLogger.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketLogger.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,68 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import org.jboss.logging.BasicLogger; +import org.jboss.logging.Logger; +import org.jboss.logging.annotations.Cause; +import org.jboss.logging.annotations.LogMessage; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageLogger; + +/** + * log messages start at 25000 + * + * @author Stuart Douglas + */ +@MessageLogger(projectCode = "UT") +public interface WebSocketLogger extends BasicLogger { + + WebSocketLogger ROOT_LOGGER = Logger.getMessageLogger(WebSocketLogger.class, WebSocketLogger.class.getPackage().getName()); + + WebSocketLogger REQUEST_LOGGER = Logger.getMessageLogger(WebSocketLogger.class, WebSocketLogger.class.getPackage().getName() + ".request"); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 25001, value = "WebSocket handshake failed") + void webSocketHandshakeFailed(@Cause Throwable cause); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 25002, value = "StreamSinkFrameChannel %s was closed before writing was finished, web socket connection is now unusable") + void closedBeforeFinishedWriting(StreamSinkFrameChannel streamSinkFrameChannel); + + @LogMessage(level = Logger.Level.DEBUG) + @Message(id = 25003, value = "Decoding WebSocket Frame with opCode %s") + void decodingFrameWithOpCode(int opCode); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 25004, value = "Failure during execution of SendCallback") + void sendCallbackExecutionError(@Cause Throwable cause); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 25005, value = "Failed to set idle timeout") + void setIdleTimeFailed(@Cause Throwable cause); + + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 25006, value = "Failed to get idle timeout") + void getIdleTimeFailed(@Cause Throwable cause); + + @LogMessage(level = Logger.Level.ERROR) + @Message(id = 25007, value = "Unhandled exception for annotated endpoint %s") + void unhandledErrorInAnnotatedEndpoint(Object instance, @Cause Throwable thr); +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketMessages.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketMessages.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketMessages.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,165 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.websockets.WebSocketExtension; +import org.jboss.logging.Messages; +import org.jboss.logging.annotations.Message; +import org.jboss.logging.annotations.MessageBundle; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.util.Collection; +import java.util.List; + +/** + * start at 20000 + * @author Stuart Douglas + */ +@MessageBundle(projectCode = "UT") +public interface WebSocketMessages { + + WebSocketMessages MESSAGES = Messages.getBundle(WebSocketMessages.class); + + @Message(id = 2001, value = "Not a WebSocket handshake request: missing %s in the headers") + WebSocketHandshakeException missingHeader(String header); + + @Message(id = 2002, value = "Channel is closed") + IOException channelClosed(); + + @Message(id = 2003, value = "Text frame contains non UTF-8 data") + UnsupportedEncodingException invalidTextFrameEncoding(); + + @Message(id = 2004, value = "Cannot call shutdownWrites, only %s of %s bytes written") + IOException notAllPayloadDataWritten(long written, long payloadSize); + + @Message(id = 2005, value = "Fragmented control frame") + WebSocketFrameCorruptedException fragmentedControlFrame(); + + @Message(id = 2006, value = "Control frame with payload length > 125 octets") + WebSocketFrameCorruptedException toBigControlFrame(); + + @Message(id = 2007, value = "Control frame using reserved opcode = %s") + WebSocketFrameCorruptedException reservedOpCodeInControlFrame(int opCode); + + @Message(id = 2008, value = "Received close control frame with payload len 1") + WebSocketFrameCorruptedException controlFrameWithPayloadLen1(); + + @Message(id = 2009, value = "Data frame using reserved opcode = %s") + WebSocketFrameCorruptedException reservedOpCodeInDataFrame(int opCode); + + @Message(id = 2010, value = "Received continuation data frame outside fragmented message") + WebSocketFrameCorruptedException continuationFrameOutsideFragmented(); + + @Message(id = 2011, value = "Received non-continuation data frame while inside fragmented message") + WebSocketFrameCorruptedException nonContinuationFrameInsideFragmented(); + + @Message(id = 2012, value = "Invalid data frame length (not using minimal length encoding)") + WebSocketFrameCorruptedException invalidDataFrameLength(); + + @Message(id = 2013, value = "Cannot decode web socket frame with opcode: %s") + IllegalStateException unsupportedOpCode(int opCode); + + @Message(id = 2014, value = "WebSocketFrameType %s is not supported by this WebSocketChannel\"") + IllegalArgumentException unsupportedFrameType(WebSocketFrameType type); + + @Message(id = 2015, value = "Extensions not allowed but received rsv of %s") + WebSocketFrameCorruptedException extensionsNotAllowed(int rsv); + + @Message(id = 2016, value = "Could not find supported protocol in request list %s. Supported protocols are %s") + WebSocketHandshakeException unsupportedProtocol(String requestedSubprotocols, Collection subprotocols); + + @Message(id = 2017, value = "No Length encoded in the frame") + WebSocketFrameCorruptedException noLengthEncodedInFrame(); + + @Message(id = 2018, value = "Payload is not support in CloseFrames when using WebSocket Version 00") + IllegalArgumentException payloadNotSupportedInCloseFrames(); + + @Message(id = 2019, value = "Invalid payload for PING (payload length must be <= 125, was %s)") + IllegalArgumentException invalidPayloadLengthForPing(long payloadLength); + + @Message(id = 2020, value = "Payload is not supported for Close Frames when using WebSocket 00") + IOException noPayloadAllowedForCloseFrames(); + + @Message(id = 2021, value = "Fragmentation not supported") + UnsupportedOperationException fragmentationNotSupported(); + + @Message(id = 2022, value = "Can only be changed before the write is in progress") + IllegalStateException writeInProgress(); + + @Message(id = 2023, value = "Extensions not supported") + UnsupportedOperationException extensionsNotSupported(); + + @Message(id = 2024, value = "The payload length must be >= 0") + IllegalArgumentException negativePayloadLength(); + + @Message(id = 2025, value = "Closed before all bytes where read") + IOException closedBeforeAllBytesWereRead(); + + @Message(id = 2026, value = "Invalid close frame status code: %s") + WebSocketInvalidCloseCodeException invalidCloseFrameStatusCode(int statusCode); + + @Message(id = 2027, value = "Could not send data, as the underlying web socket connection has been broken") + IOException streamIsBroken(); + + @Message(id = 2028, value = "Specified length is bigger the available size of the FileChannel") + IllegalArgumentException lengthBiggerThenFileChannel(); + + @Message(id = 2029, value = "FragmentedSender was complete already") + IllegalArgumentException fragmentedSenderCompleteAlready(); + + @Message(id = 2030, value = "Array of SenderCallbacks must be non empty") + IllegalArgumentException senderCallbacksEmpty(); + + @Message(id = 2031, value = "Only one FragmentedSender can be used at the same time") + IllegalStateException fragmentedSenderInUse(); + + @Message(id = 2032, value = "Close frame was send before") + IOException closeFrameSentBefore(); + + @Message(id = 2033, value = "Blocking operation was called in IO thread") + IllegalStateException blockingOperationInIoThread(); + + @Message(id = 2034, value = "Web socket frame was not masked") + WebSocketFrameCorruptedException frameNotMasked(); + + @Message(id = 2035, value = "The response did not contain an 'Upgrade: websocket' header") + IOException noWebSocketUpgradeHeader(); + + @Message(id = 2036, value = "The response did not contain a 'Connection: upgrade' header") + IOException noWebSocketConnectionHeader(); + + @Message(id = 2037, value = "Sec-WebSocket-Accept mismatch, expecting %s, received %s") + IOException webSocketAcceptKeyMismatch(String dKey, String acceptKey); + + @Message(id = 2038, value = "Cannot call method with frame type %s, only text or binary is allowed") + IllegalArgumentException incorrectFrameType(WebSocketFrameType type); + + @Message(id = 2039, value = "Data has already been released") + IllegalStateException dataHasBeenReleased(); + + @Message(id = 2040, value = "Message exceeded max message size of %s") + String messageToBig(long maxMessageSize); + + @Message(id = 2041, value = "Attempted to write more data than the specified payload length") + IOException messageOverflow(); + + @Message(id = 2042, value = "Server responded with unsupported extension %s. Supported extensions: %s") + IOException unsupportedExtension(String part, List supportedExtensions); +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketUtils.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketUtils.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketUtils.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,464 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import io.undertow.UndertowLogger; +import org.xnio.Buffers; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.ChannelListeners; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.Channel; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * Utility class which holds general useful utility methods which + * can be used within WebSocket implementations. + * + * @author Norman Maurer + */ +public final class WebSocketUtils { + + /** + * UTF-8 {@link Charset} which is used to encode Strings in WebSockets + */ + public static final Charset UTF_8 = Charset.forName("UTF-8"); + private static final String EMPTY = ""; + + /** + * Generate the MD5 hash out of the given {@link ByteBuffer} + */ + public static ByteBuffer md5(ByteBuffer buffer) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + md.update(buffer); + return ByteBuffer.wrap(md.digest()); + } catch (NoSuchAlgorithmException e) { + // Should never happen + throw new InternalError("MD5 not supported on this platform"); + } + } + + /** + * Create a {@link ByteBuffer} which holds the UTF8 encoded bytes for the + * given {@link String}. + * + * @param utfString The {@link String} to convert + * @return buffer The {@link ByteBuffer} which was created + */ + public static ByteBuffer fromUtf8String(CharSequence utfString) { + if (utfString == null || utfString.length() == 0) { + return Buffers.EMPTY_BYTE_BUFFER; + } else { + return ByteBuffer.wrap(utfString.toString().getBytes(UTF_8)); + } + } + + public static String toUtf8String(ByteBuffer buffer) { + if (!buffer.hasRemaining()) { + return EMPTY; + } + if (buffer.hasArray()) { + return new String(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining(), UTF_8); + } else { + byte[] content = new byte[buffer.remaining()]; + buffer.get(content); + return new String(content, UTF_8); + } + } + + public static String toUtf8String(ByteBuffer... buffers) { + int size = 0; + for (ByteBuffer buf: buffers) { + size += buf.remaining(); + } + if (size == 0) { + return EMPTY; + } + + int index = 0; + byte[] bytes = new byte[size]; + for (ByteBuffer buf: buffers) { + if (buf.hasArray()) { + int len = buf.remaining(); + System.arraycopy(buf.array(), buf.arrayOffset() + buf.position(), bytes, index, len); + index += len; + } else { + int len = buf.remaining(); + buf.get(bytes, index, len); + index += len; + } + } + return new String(bytes, UTF_8); + } + + /** + * Transfer the data from the source to the sink using the given through buffer to pass data through. + */ + public static long transfer(final ReadableByteChannel source, final long count, final ByteBuffer throughBuffer, final WritableByteChannel sink) throws IOException { + long total = 0L; + while (total < count) { + throughBuffer.clear(); + if (count - total < throughBuffer.remaining()) { + throughBuffer.limit((int) (count - total)); + } + + try { + long res = source.read(throughBuffer); + if (res <= 0) { + return total == 0L ? res : total; + } + } finally { + throughBuffer.flip(); + + } + while (throughBuffer.hasRemaining()) { + long res = sink.write(throughBuffer); + if (res <= 0) { + return total; + } + total += res; + } + } + return total; + } + + /** + * Echo back the frame to the sender + */ + public static void echoFrame(final WebSocketChannel channel, final StreamSourceFrameChannel ws) throws IOException { + + final WebSocketFrameType type; + switch (ws.getType()) { + case PONG: + // pong frames must be discarded + ws.discard(); + return; + case PING: + // if a ping is send the autobahn testsuite expects a PONG when echo back + type = WebSocketFrameType.PONG; + break; + default: + type = ws.getType(); + break; + } + final StreamSinkFrameChannel sink = channel.send(type); + sink.setRsv(ws.getRsv()); + initiateTransfer(ws, sink, new ChannelListener() { + @Override + public void handleEvent(StreamSourceFrameChannel streamSourceFrameChannel) { + IoUtils.safeClose(streamSourceFrameChannel); + } + }, new ChannelListener() { + @Override + public void handleEvent(StreamSinkFrameChannel streamSinkFrameChannel) { + try { + streamSinkFrameChannel.shutdownWrites(); + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(streamSinkFrameChannel, channel); + return; + } + try { + if (!streamSinkFrameChannel.flush()) { + streamSinkFrameChannel.getWriteSetter().set(ChannelListeners.flushingChannelListener( + new ChannelListener() { + @Override + public void handleEvent(StreamSinkFrameChannel streamSinkFrameChannel) { + streamSinkFrameChannel.getWriteSetter().set(null); + IoUtils.safeClose(streamSinkFrameChannel); + if (type == WebSocketFrameType.CLOSE) { + IoUtils.safeClose(channel); + } + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkFrameChannel streamSinkFrameChannel, IOException e) { + + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(streamSinkFrameChannel, channel); + + } + } + )); + streamSinkFrameChannel.resumeWrites(); + } else { + if (type == WebSocketFrameType.CLOSE) { + IoUtils.safeClose(channel); + } + streamSinkFrameChannel.getWriteSetter().set(null); + IoUtils.safeClose(streamSinkFrameChannel); + } + } catch (IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(streamSinkFrameChannel, channel); + + } + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSourceFrameChannel streamSourceFrameChannel, IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(streamSourceFrameChannel, channel); + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkFrameChannel streamSinkFrameChannel, IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + IoUtils.safeClose(streamSinkFrameChannel, channel); + } + }, channel.getBufferPool() + ); + + } + + /** + * Initiate a low-copy transfer between two stream channels. The pool should be a direct buffer pool for best + * performance. + * + * @param source the source channel + * @param sink the target channel + * @param sourceListener the source listener to set and call when the transfer is complete, or {@code null} to clear the listener at that time + * @param sinkListener the target listener to set and call when the transfer is complete, or {@code null} to clear the listener at that time + * @param readExceptionHandler the read exception handler to call if an error occurs during a read operation + * @param writeExceptionHandler the write exception handler to call if an error occurs during a write operation + * @param pool the pool from which the transfer buffer should be allocated + */ + public static void initiateTransfer(final I source, final O sink, final ChannelListener sourceListener, final ChannelListener sinkListener, final ChannelExceptionHandler readExceptionHandler, final ChannelExceptionHandler writeExceptionHandler, Pool pool) { + if (pool == null) { + throw new IllegalArgumentException("pool is null"); + } + final Pooled allocated = pool.allocate(); + boolean free = true; + try { + final ByteBuffer buffer = allocated.getResource(); + buffer.clear(); + long transferred; + do { + try { + transferred = source.transferTo(Long.MAX_VALUE, buffer, sink); + } catch (IOException e) { + ChannelListeners.invokeChannelExceptionHandler(source, readExceptionHandler, e); + return; + } + if (transferred == -1) { + source.suspendReads(); + sink.suspendWrites(); + ChannelListeners.invokeChannelListener(source, sourceListener); + ChannelListeners.invokeChannelListener(sink, sinkListener); + return; + } + while (buffer.hasRemaining()) { + final int res; + try { + res = sink.write(buffer); + } catch (IOException e) { + ChannelListeners.invokeChannelExceptionHandler(sink, writeExceptionHandler, e); + return; + } + if (res == 0) { + // write first listener + final TransferListener listener = new TransferListener<>(allocated, source, sink, sourceListener, sinkListener, writeExceptionHandler, readExceptionHandler, 1); + source.suspendReads(); + source.getReadSetter().set(listener); + sink.getWriteSetter().set(listener); + sink.resumeWrites(); + free = false; + return; + } else if (res == -1) { + source.suspendReads(); + sink.suspendWrites(); + ChannelListeners.invokeChannelListener(source, sourceListener); + ChannelListeners.invokeChannelListener(sink, sinkListener); + return; + } + } + } while (transferred > 0L); + final TransferListener listener = new TransferListener<>(allocated, source, sink, sourceListener, sinkListener, writeExceptionHandler, readExceptionHandler, 0); + sink.suspendWrites(); + sink.getWriteSetter().set(listener); + source.getReadSetter().set(listener); + // read first listener + sink.suspendWrites(); + source.resumeReads(); + free = false; + } finally { + if (free) { + allocated.free(); + } + } + } + + + static final class TransferListener implements ChannelListener { + private final Pooled pooledBuffer; + private final I source; + private final O sink; + private final ChannelListener sourceListener; + private final ChannelListener sinkListener; + private final ChannelExceptionHandler writeExceptionHandler; + private final ChannelExceptionHandler readExceptionHandler; + private volatile int state; + + TransferListener(final Pooled pooledBuffer, final I source, final O sink, final ChannelListener sourceListener, final ChannelListener sinkListener, final ChannelExceptionHandler writeExceptionHandler, final ChannelExceptionHandler readExceptionHandler, final int state) { + this.pooledBuffer = pooledBuffer; + this.source = source; + this.sink = sink; + this.sourceListener = sourceListener; + this.sinkListener = sinkListener; + this.writeExceptionHandler = writeExceptionHandler; + this.readExceptionHandler = readExceptionHandler; + this.state = state; + } + + @Override + public void handleEvent(final Channel channel) { + final ByteBuffer buffer = pooledBuffer.getResource(); + int state = this.state; + long lres; + int ires; + + switch (state) { + case 0: { + // read listener + for (; ; ) { + if(buffer.hasRemaining()) { + WebSocketLogger.REQUEST_LOGGER.error("BUFFER HAS REMAINING!!!!!"); + } + try { + lres = source.transferTo(Long.MAX_VALUE, buffer, sink); + } catch (IOException e) { + readFailed(e); + return; + } + if (lres == 0 && !buffer.hasRemaining()) { + return; + } + if (lres == -1) { + // possibly unexpected EOF + // it's OK; just be done + done(); + return; + } + while (buffer.hasRemaining()) { + try { + ires = sink.write(buffer); + } catch (IOException e) { + writeFailed(e); + return; + } + if (ires == 0) { + this.state = 1; + source.suspendReads(); + sink.resumeWrites(); + return; + } + } + } + } + case 1: { + // write listener + for (; ; ) { + while (buffer.hasRemaining()) { + try { + ires = sink.write(buffer); + } catch (IOException e) { + writeFailed(e); + return; + } + if (ires == 0) { + return; + } + } + try { + lres = source.transferTo(Long.MAX_VALUE, buffer, sink); + } catch (IOException e) { + readFailed(e); + return; + } + if (lres == 0 && !buffer.hasRemaining()) { + this.state = 0; + sink.suspendWrites(); + source.resumeReads(); + return; + } + if (lres == -1) { + done(); + return; + } + } + } + } + } + + private void writeFailed(final IOException e) { + try { + source.suspendReads(); + sink.suspendWrites(); + ChannelListeners.invokeChannelExceptionHandler(sink, writeExceptionHandler, e); + } finally { + pooledBuffer.free(); + } + } + + private void readFailed(final IOException e) { + try { + source.suspendReads(); + sink.suspendWrites(); + ChannelListeners.invokeChannelExceptionHandler(source, readExceptionHandler, e); + } finally { + pooledBuffer.free(); + } + } + + private void done() { + try { + final ChannelListener sourceListener = this.sourceListener; + final ChannelListener sinkListener = this.sinkListener; + final I source = this.source; + final O sink = this.sink; + source.suspendReads(); + sink.suspendWrites(); + + ChannelListeners.invokeChannelListener(source, sourceListener); + ChannelListeners.invokeChannelListener(sink, sinkListener); + } finally { + pooledBuffer.free(); + } + } + + public String toString() { + return "Transfer channel listener (" + source + " to " + sink + ") -> (" + sourceListener + " and " + sinkListener + ')'; + } + } + + private WebSocketUtils() { + // utility class + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketVersion.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketVersion.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSocketVersion.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,90 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + + +import io.undertow.util.AttachmentKey; + +/** + *

+ * Enum which list all the different versions of the WebSocket specification (to the current date). + *

+ *

+ * A specification is tied to one wire protocol version but a protocol version may have use by more than 1 version of + * the specification. + *

+ * + * @author Norman Maurer + */ +public enum WebSocketVersion { + + /** + * Unknown version of the protocol + */ + UNKNOWN, + + /** + * draft-ietf-hybi-thewebsocketprotocol- 00. + */ + V00, + + /** + * draft-ietf-hybi-thewebsocketprotocol- 07 + */ + V07, + + /** + * draft-ietf-hybi-thewebsocketprotocol- 10 + */ + V08, + + /** + * RFC 6455. This was originally draft-ietf-hybi-thewebsocketprotocol- + * 17 + */ + V13; + + /** + * Returns a {@link String} representation of the {@link WebSocketVersion} that can be used in the HTTP Headers. + */ + public String toHttpHeaderValue() { + if (this == V00) { + return "0"; + } + if (this == V07) { + return "7"; + } + if (this == V08) { + return "8"; + } + if (this == V13) { + return "13"; + } + // Should never hit here. + throw new IllegalStateException("Unknown WebSocket version: " + this); + } + + + public static final AttachmentKey ATTACHMENT_KEY = AttachmentKey.create(WebSocketVersion.class); +} + Index: 3rdParty_sources/undertow/io/undertow/websockets/core/WebSockets.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/WebSockets.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/WebSockets.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,405 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core; + +import org.xnio.Buffers; +import org.xnio.ChannelExceptionHandler; +import org.xnio.ChannelListener; +import org.xnio.IoUtils; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; + +import static org.xnio.ChannelListeners.flushingChannelListener; + +/** + * @author Stuart Douglas + */ +public class WebSockets { + + private static final Charset utf8 = Charset.forName("UTF-8"); + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param message + * @param wsChannel + * @param callback + */ + public static void sendText(final String message, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + final ByteBuffer data = ByteBuffer.wrap(message.getBytes(utf8)); + sendInternal(new ByteBuffer[]{data}, WebSocketFrameType.TEXT, wsChannel, callback); + } + + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param message + * @param wsChannel + * @param callback + */ + public static void sendText(final ByteBuffer message, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(new ByteBuffer[]{message}, WebSocketFrameType.TEXT, wsChannel, callback); + } + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param message + * @param wsChannel + */ + public static void sendTextBlocking(final String message, final WebSocketChannel wsChannel) throws IOException { + final ByteBuffer data = ByteBuffer.wrap(message.getBytes(utf8)); + sendBlockingInternal(new ByteBuffer[]{data}, WebSocketFrameType.TEXT, wsChannel); + } + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param message + * @param wsChannel + */ + public static void sendTextBlocking(final ByteBuffer message, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(new ByteBuffer[]{message}, WebSocketFrameType.TEXT, wsChannel); + } + + /** + * Sends a complete ping message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendPing(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(new ByteBuffer[]{data}, WebSocketFrameType.PING, wsChannel, callback); + } + + /** + * Sends a complete ping message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendPing(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(data, WebSocketFrameType.PING, wsChannel, callback); + } + + /** + * Sends a complete ping message using blocking IO + * + * @param data + * @param wsChannel + */ + public static void sendPingBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(new ByteBuffer[]{data}, WebSocketFrameType.PING, wsChannel); + } + + /** + * Sends a complete ping message using blocking IO + * + * @param data + * @param wsChannel + */ + public static void sendPingBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(data, WebSocketFrameType.PING, wsChannel); + } + + /** + * Sends a complete pong message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendPong(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(new ByteBuffer[]{data}, WebSocketFrameType.PONG, wsChannel, callback); + } + + + /** + * Sends a complete pong message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendPong(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(data, WebSocketFrameType.PONG, wsChannel, callback); + } + + /** + * Sends a complete pong message using blocking IO + * + * @param data + * @param wsChannel + */ + public static void sendPongBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(new ByteBuffer[]{data}, WebSocketFrameType.PONG, wsChannel); + } + + /** + * Sends a complete pong message using blocking IO + * + * @param data + * @param wsChannel + */ + public static void sendPongBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(data, WebSocketFrameType.PONG, wsChannel); + } + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendBinary(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(new ByteBuffer[]{data}, WebSocketFrameType.BINARY, wsChannel, callback); + } + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendBinary(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(data, WebSocketFrameType.BINARY, wsChannel, callback); + } + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param data + * @param wsChannel + */ + public static void sendBinaryBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(new ByteBuffer[]{data}, WebSocketFrameType.BINARY, wsChannel); + } + + /** + * Sends a complete text message, invoking the callback when complete + * + * @param data + * @param wsChannel + */ + public static void sendBinaryBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(data, WebSocketFrameType.BINARY, wsChannel); + } + + /** + * Sends a complete close message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendClose(final ByteBuffer data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(new ByteBuffer[]{data}, WebSocketFrameType.CLOSE, wsChannel, callback); + } + + /** + * Sends a complete close message, invoking the callback when complete + * + * @param data + * @param wsChannel + * @param callback + */ + public static void sendClose(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendInternal(data, WebSocketFrameType.CLOSE, wsChannel, callback); + } + + + /** + * Sends a complete close message, invoking the callback when complete + * + * @param code The close code + * @param wsChannel + * @param callback + */ + public static void sendClose(final int code, String reason, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + sendClose(new CloseMessage(code, reason).toByteBuffer(), wsChannel, callback); + } + + /** + * Sends a complete close message, invoking the callback when complete + * + * @param code + * @param wsChannel + */ + public static void sendCloseBlocking(final int code, String reason, final WebSocketChannel wsChannel) throws IOException { + sendCloseBlocking(new CloseMessage(code, reason).toByteBuffer(), wsChannel); + } + /** + * Sends a complete close message, invoking the callback when complete + * + * @param data + * @param wsChannel + */ + public static void sendCloseBlocking(final ByteBuffer data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(new ByteBuffer[]{data}, WebSocketFrameType.CLOSE, wsChannel); + } + + /** + * Sends a complete close message, invoking the callback when complete + * + * @param data + * @param wsChannel + */ + public static void sendCloseBlocking(final ByteBuffer[] data, final WebSocketChannel wsChannel) throws IOException { + sendBlockingInternal(data, WebSocketFrameType.CLOSE, wsChannel); + } + + private static void sendInternal(final ByteBuffer[] data, WebSocketFrameType type, final WebSocketChannel wsChannel, final WebSocketCallback callback) { + try { + long totalData = Buffers.remaining(data); + StreamSinkFrameChannel channel = wsChannel.send(type, totalData); + sendData(data, wsChannel, callback, channel, null); + } catch (IOException e) { + if (callback != null) { + callback.onError(wsChannel, null, e); + } else { + IoUtils.safeClose(wsChannel); + } + } + } + + private static void sendData(final ByteBuffer[] data, final WebSocketChannel wsChannel, final WebSocketCallback callback, StreamSinkFrameChannel channel, final T context) throws IOException { + boolean hasRemaining = true; + while (hasRemaining) { + long res = channel.write(data); + hasRemaining = Buffers.hasRemaining(data); + if (res == 0 && hasRemaining) { + channel.getWriteSetter().set(new ChannelListener() { + @Override + public void handleEvent(StreamSinkFrameChannel channel) { + do { + try { + long res = channel.write(data); + if (res == 0) { + return; + } + } catch (IOException e) { + handleIoException(channel, e, callback, context, wsChannel); + return; + } + } while (Buffers.hasRemaining(data)); + channel.suspendWrites(); + try { + flushChannelAsync(wsChannel, callback, channel, context); + } catch (IOException e) { + handleIoException(channel, e, callback, context, wsChannel); + } + } + }); + channel.resumeWrites(); + return; + } + } + flushChannelAsync(wsChannel, callback, channel, context); + } + + private static void handleIoException(StreamSinkFrameChannel channel, IOException e, WebSocketCallback callback, T context, WebSocketChannel wsChannel) { + if (callback != null) { + callback.onError(channel.getWebSocketChannel(), context, e); + } + IoUtils.safeClose(wsChannel); + channel.suspendWrites(); + } + + private static void flushChannelAsync(final WebSocketChannel wsChannel, final WebSocketCallback callback, StreamSinkFrameChannel channel, final T context) throws IOException { + final WebSocketFrameType type = channel.getType(); + channel.shutdownWrites(); + if (!channel.flush()) { + channel.getWriteSetter().set(flushingChannelListener( + new ChannelListener() { + @Override + public void handleEvent(StreamSinkFrameChannel channel) { + if (callback != null) { + callback.complete(wsChannel, context); + } + if (type == WebSocketFrameType.CLOSE && wsChannel.isCloseFrameReceived()) { + IoUtils.safeClose(wsChannel); + } + } + }, new ChannelExceptionHandler() { + @Override + public void handleException(StreamSinkFrameChannel channel, IOException exception) { + if (callback != null) { + callback.onError(wsChannel, context, exception); + } + if (type == WebSocketFrameType.CLOSE && wsChannel.isCloseFrameReceived()) { + IoUtils.safeClose(wsChannel); + } + } + } + )); + channel.resumeWrites(); + return; + } + if (callback != null) { + callback.complete(wsChannel, context); + } + if (type == WebSocketFrameType.CLOSE && wsChannel.isCloseFrameReceived()) { + IoUtils.safeClose(wsChannel); + } + } + + private static void sendBlockingInternal(final ByteBuffer[] data, WebSocketFrameType type, final WebSocketChannel wsChannel) throws IOException { + long totalData = Buffers.remaining(data); + StreamSinkFrameChannel channel = wsChannel.send(type, totalData); + for (ByteBuffer buf : data) { + while (buf.hasRemaining()) { + int res = channel.write(buf); + if (res == 0) { + channel.awaitWritable(); + } + } + } + channel.shutdownWrites(); + while (!channel.flush()) { + channel.awaitWritable(); + } + if (type == WebSocketFrameType.CLOSE && wsChannel.isCloseFrameReceived()) { + IoUtils.safeClose(wsChannel); + } + } + + private WebSockets() { + + } + + public static ByteBuffer mergeBuffers(ByteBuffer... payload) { + int size = (int) Buffers.remaining(payload); + if (size == 0) { + return Buffers.EMPTY_BYTE_BUFFER; + } + ByteBuffer buffer = ByteBuffer.allocate(size); + for (ByteBuffer buf : payload) { + buffer.put(buf); + } + buffer.flip(); + return buffer; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunction.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunction.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunction.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,60 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.function; + +import io.undertow.server.protocol.framed.FrameHeaderData; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +public interface ChannelFunction { + + + void newFrame(FrameHeaderData headerData); + + /** + * Is called on the {@link ByteBuffer} after a read operation completes + * + * @param buf the {@link ByteBuffer} to operate on + * @param position the index in the {@link ByteBuffer} to start from + * @param length the number of bytes to operate on + * @throws IOException thrown if an error occurs + */ + void afterRead(ByteBuffer buf, int position, int length) throws IOException; + + /** + * Is called on the {@link ByteBuffer} before a write operation completes + * + * @param buf the {@link ByteBuffer} to operate on + * @param position the index in the {@link ByteBuffer} to start from + * @param length the number of bytes to operate on + * @throws IOException thrown if an error occurs + */ + void beforeWrite(ByteBuffer buf, int position, int length) throws IOException; + + /** + * Is called to complete the {@link ChannelFunction}. Access it after complete + * is called may result in unexpected behavior. + * + * @throws IOException thrown if an error occurs + */ + void complete() throws IOException; +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionFileChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionFileChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionFileChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,165 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.function; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.MappedByteBuffer; +import java.nio.channels.FileChannel; +import java.nio.channels.FileLock; +import java.nio.channels.ReadableByteChannel; +import java.nio.channels.WritableByteChannel; + +/** + * @author Norman Maurer + */ +public class ChannelFunctionFileChannel extends FileChannel { + private final ChannelFunction[] functions; + private final FileChannel channel; + + public ChannelFunctionFileChannel(FileChannel channel, ChannelFunction... functions) { + this.channel = channel; + this.functions = functions; + } + + @Override + public long position() throws IOException { + return channel.position(); + } + + @Override + public FileChannel position(long newPosition) throws IOException { + return new ChannelFunctionFileChannel(channel.position(newPosition), functions); + } + + @Override + public long size() throws IOException { + return channel.size(); + } + + @Override + public FileChannel truncate(long size) throws IOException { + return new ChannelFunctionFileChannel(channel.truncate(size), functions); + } + + @Override + public void force(boolean metaData) throws IOException { + channel.force(metaData); + } + + @Override + public MappedByteBuffer map(MapMode mode, long position, long size) throws IOException { + return channel.map(mode, position, size); + } + + @Override + public FileLock lock(long position, long size, boolean shared) throws IOException { + return channel.lock(position, size, shared); + } + + @Override + public FileLock tryLock(long position, long size, boolean shared) throws IOException { + return channel.tryLock(position, size, shared); + } + + @Override + protected void implCloseChannel() throws IOException { + channel.close(); + } + + @Override + public int write(ByteBuffer src, long position) throws IOException { + beforeWriting(src); + return channel.write(src, position); + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int pos = dst.position(); + int r = channel.read(dst); + if (r > 0) { + afterReading(dst, pos, r); + } + return r; + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + int[] positions = new int[length]; + for (int i = 0; i < positions.length; i++) { + positions[i] = dsts[i].position(); + } + long r = channel.read(dsts, offset, length); + if (r > 0) { + for (int i = offset; i < length; i++) { + ByteBuffer dst = dsts[i]; + afterReading(dst, positions[i], dst.position()); + } + } + return r; + } + + @Override + public int write(ByteBuffer src) throws IOException { + beforeWriting(src); + return channel.write(src); + } + + @Override + public long write(ByteBuffer[] srcs, int offset, int length) throws IOException { + for (int i = offset; i < length; i++) { + beforeWriting(srcs[i]); + } + return channel.write(srcs, offset, length); + } + + @Override + public int read(ByteBuffer dst, long position) throws IOException { + int pos = dst.position(); + int r = channel.read(dst, position); + if (r > 0) { + afterReading(dst, pos, r); + } + return r; + } + + @Override + public long transferTo(long position, long count, WritableByteChannel target) throws IOException { + return channel.transferTo(position, count, new ChannelFunctionWritableByteChannel(target, functions)); + } + + @Override + public long transferFrom(ReadableByteChannel src, long position, long count) throws IOException { + return channel.transferFrom(new ChannelFunctionReadableByteChannel(channel, functions) ,position, count); + } + + + private void beforeWriting(ByteBuffer buffer) throws IOException { + for (ChannelFunction func: functions) { + int pos = buffer.position(); + func.beforeWrite(buffer, pos, buffer.limit() - pos); + } + } + + private void afterReading(ByteBuffer buffer, int position, int length) throws IOException { + for (ChannelFunction func: functions) { + func.afterRead(buffer, position, length); + } + } + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionReadableByteChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionReadableByteChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionReadableByteChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,62 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.function; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.ReadableByteChannel; + +/** + * @author Norman Maurer + */ +public class ChannelFunctionReadableByteChannel implements ReadableByteChannel { + + private final ChannelFunction[] functions; + private final ReadableByteChannel channel; + + public ChannelFunctionReadableByteChannel(ReadableByteChannel channel, ChannelFunction... functions) { + this.channel = channel; + this.functions = functions; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + int pos = dst.position(); + int r = 0; + try { + r = channel.read(dst); + return r; + } finally { + if (r > 0) { + for (ChannelFunction func: functions) { + func.afterRead(dst, pos, r); + } + } + } + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void close() throws IOException { + channel.close(); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionStreamSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionStreamSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionStreamSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,183 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.function; + +import org.xnio.ChannelListener; +import org.xnio.Option; +import org.xnio.XnioExecutor; +import org.xnio.XnioIoThread; +import org.xnio.XnioWorker; +import org.xnio.channels.StreamSinkChannel; +import org.xnio.channels.StreamSourceChannel; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; +import java.util.concurrent.TimeUnit; + +/** + * @author Norman Maurer + */ +public class ChannelFunctionStreamSourceChannel implements StreamSourceChannel { + private final StreamSourceChannel channel; + private final ChannelFunction[] functions; + + public ChannelFunctionStreamSourceChannel(StreamSourceChannel channel, ChannelFunction... functions) { + this.channel = channel; + this.functions = functions; + } + + @Override + public long transferTo(long position, long count, FileChannel target) throws IOException { + return channel.transferTo(position, count, new ChannelFunctionFileChannel(target, functions)); + } + + @Override + public long transferTo(long count, ByteBuffer throughBuffer, StreamSinkChannel target) throws IOException { + return target.transferFrom(this, count, throughBuffer); + } + + @Override + public ChannelListener.Setter getReadSetter() { + return channel.getReadSetter(); + } + + @Override + public ChannelListener.Setter getCloseSetter() { + return channel.getCloseSetter(); + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + long r = 0; + for (int a = offset; a < length; a++) { + int i = read(dsts[a]); + if (i < 1) { + break; + } + r += i; + } + return r; + } + + @Override + public long read(ByteBuffer[] dsts) throws IOException { + long r = 0; + for (ByteBuffer buf: dsts) { + int i = read(buf); + if (i < 1) { + break; + } + r += i; + } + return r; + } + + @Override + public void suspendReads() { + channel.suspendReads(); + } + + @Override + public void resumeReads() { + channel.resumeReads(); + } + + @Override + public boolean isReadResumed() { + return channel.isReadResumed(); + } + + @Override + public void wakeupReads() { + channel.wakeupReads(); + } + + @Override + public void shutdownReads() throws IOException { + channel.shutdownReads(); + } + + @Override + public void awaitReadable() throws IOException { + channel.awaitReadable(); + } + + @Override + public void awaitReadable(long time, TimeUnit timeUnit) throws IOException { + channel.awaitReadable(time, timeUnit); + } + + @Override + public XnioExecutor getReadThread() { + return channel.getReadThread(); + } + + + @Override + public int read(ByteBuffer dst) throws IOException { + int position = dst.position(); + int r = channel.read(dst); + if (r > 0) { + afterReading(dst, position, r); + } + return r; + } + + + @Override + public XnioWorker getWorker() { + return channel.getWorker(); + } + + @Override + public XnioIoThread getIoThread() { + return channel.getIoThread(); + } + + @Override + public boolean supportsOption(Option option) { + return channel.supportsOption(option); + } + + @Override + public T getOption(Option option) throws IOException { + return channel.getOption(option); + } + + @Override + public T setOption(Option option, T value) throws IOException { + return channel.setOption(option, value); + } + + private void afterReading(ByteBuffer buffer, int position, int length) throws IOException { + for (ChannelFunction func: functions) { + func.afterRead(buffer, position, length); + } + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void close() throws IOException { + channel.close(); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionWritableByteChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionWritableByteChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/function/ChannelFunctionWritableByteChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,54 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.function; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.WritableByteChannel; + +/** + * @author Norman Maurer + */ +public class ChannelFunctionWritableByteChannel implements WritableByteChannel { + private final ChannelFunction[] functions; + private final WritableByteChannel channel; + + public ChannelFunctionWritableByteChannel(WritableByteChannel channel, ChannelFunction... functions) { + this.channel = channel; + this.functions = functions; + } + + @Override + public int write(ByteBuffer src) throws IOException { + for(ChannelFunction func : functions) { + int pos = src.position(); + func.beforeWrite(src, pos, src.limit() - pos); + } + return channel.write(src); + } + + @Override + public boolean isOpen() { + return channel.isOpen(); + } + + @Override + public void close() throws IOException { + channel.close(); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/Handshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/Handshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/Handshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,177 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol; + +import io.undertow.util.Headers; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.spi.WebSocketHttpExchange; +import org.xnio.IoFuture; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * Abstract base class for doing a WebSocket Handshake. + * + * @author Mike Brock + */ +public abstract class Handshake { + private final WebSocketVersion version; + private final String hashAlgorithm; + private final String magicNumber; + protected final Set subprotocols; + private static final byte[] EMPTY = new byte[0]; + private static final Pattern PATTERN = Pattern.compile(","); + + protected Handshake(WebSocketVersion version, String hashAlgorithm, String magicNumber, final Set subprotocols) { + this.version = version; + this.hashAlgorithm = hashAlgorithm; + this.magicNumber = magicNumber; + this.subprotocols = subprotocols; + } + + /** + * Return the version for which the {@link Handshake} can be used. + */ + public WebSocketVersion getVersion() { + return version; + } + + /** + * Return the algorithm that is used to hash during the handshake + */ + public String getHashAlgorithm() { + return hashAlgorithm; + } + + /** + * Return the magic number which will be mixed in + */ + public String getMagicNumber() { + return magicNumber; + } + + /** + * Return the full url of the websocket location of the given {@link WebSocketHttpExchange} + */ + protected static String getWebSocketLocation(WebSocketHttpExchange exchange) { + String scheme; + if ("https".equals(exchange.getRequestScheme())) { + scheme = "wss"; + } else { + scheme = "ws"; + } + return scheme + "://" + exchange.getRequestHeader(Headers.HOST_STRING) + exchange.getRequestURI(); + } + + /** + * Issue the WebSocket upgrade + * + * @param exchange The {@link WebSocketHttpExchange} for which the handshake and upgrade should occur. + */ + public final void handshake(final WebSocketHttpExchange exchange) { + exchange.putAttachment(WebSocketVersion.ATTACHMENT_KEY, version); + handshakeInternal(exchange); + } + + protected abstract void handshakeInternal(final WebSocketHttpExchange exchange); + + /** + * Return {@code true} if this implementation can be used to issue a handshake. + */ + public abstract boolean matches(WebSocketHttpExchange exchange); + + /** + * Create the {@link WebSocketChannel} from the {@link WebSocketHttpExchange} + */ + public abstract WebSocketChannel createChannel(WebSocketHttpExchange exchange, final StreamConnection channel, final Pool pool); + + /** + * convenience method to perform the upgrade + */ + protected final void performUpgrade(final WebSocketHttpExchange exchange, final byte[] data) { + exchange.setResponseHeader(Headers.CONTENT_LENGTH_STRING, String.valueOf(data.length)); + exchange.setResponseHeader(Headers.UPGRADE_STRING, "WebSocket"); + exchange.setResponseHeader(Headers.CONNECTION_STRING, "Upgrade"); + upgradeChannel(exchange, data); + } + + protected void upgradeChannel(final WebSocketHttpExchange exchange, final byte[] data) { + if (data.length > 0) { + writePayload(exchange, ByteBuffer.wrap(data)); + } else { + exchange.endExchange(); + } + } + + private static void writePayload(final WebSocketHttpExchange exchange, final ByteBuffer payload) { + exchange.sendData(payload).addNotifier(new IoFuture.Notifier() { + @Override + public void notify(final IoFuture ioFuture, final Object attachment) { + if (ioFuture.getStatus() == IoFuture.Status.DONE) { + exchange.endExchange(); + } else { + exchange.close(); + } + } + }, null); + } + + /** + * Perform the upgrade using no payload + */ + protected final void performUpgrade(final WebSocketHttpExchange exchange) { + performUpgrade(exchange, EMPTY); + } + + /** + * Selects the first matching supported sub protocol and add it the the headers of the exchange. + * + */ + protected final void selectSubprotocol(final WebSocketHttpExchange exchange) { + String requestedSubprotocols = exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING); + if (requestedSubprotocols == null) { + return; + } + + String[] requestedSubprotocolArray = PATTERN.split(requestedSubprotocols); + String subProtocol = supportedSubprotols(requestedSubprotocolArray); + if (subProtocol != null) { + exchange.setResponseHeader(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING, subProtocol); + } + + } + + protected String supportedSubprotols(String[] requestedSubprotocolArray) { + for (String p : requestedSubprotocolArray) { + String requestedSubprotocol = p.trim(); + + for (String supportedSubprotocol : subprotocols) { + if (requestedSubprotocol.equals(supportedSubprotocol)) { + return supportedSubprotocol; + } + } + } + return null; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Base64.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Base64.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Base64.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,1912 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.UndertowLogger; + +/** + *

+ * Encodes and decodes to and from Base64 notation. + *

+ *

+ * Homepage: http://iharder.net/base64. + *

+ * + *

+ * Example: + *

+ * + * String encoded = Base64.encode( myByteArray );
+ * byte[] myByteArray = Base64.decode( encoded ); + * + *

+ * The options parameter, which appears in a few places, is used to pass several pieces of information to the encoder. + * In the "higher level" methods such as encodeBytes( bytes, options ) the options parameter can be used to indicate such things + * as first gzipping the bytes before encoding them, not inserting linefeeds, and encoding using the URL-safe and Ordered + * dialects. + *

+ * + *

+ * Note, according to RFC3548, Section 2.1, implementations should not add + * line feeds unless explicitly told to do so. I've got Base64 set to this behavior now, although earlier versions broke lines + * by default. + *

+ * + *

+ * The constants defined in Base64 can be OR-ed together to combine options, so you might make a call like this: + *

+ * + * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); + *

+ * to compress the data before encoding it and then making the output have newline characters. + *

+ *

+ * Also... + *

+ * String encoded = Base64.encodeBytes( crazyString.getBytes() ); + * + * + * + *

+ * Change Log: + *

+ *
    + *
  • v2.3.7 - Fixed subtle bug when base 64 input stream contained the value 01111111, which is an invalid base 64 character + * but should not throw an ArrayIndexOutOfBoundsException either. Led to discovery of mishandling (or potential for better + * handling) of other bad input characters. You should now get an IOException if you try decoding something that has bad + * characters in it.
  • + *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded string ended in the last column; the buffer was + * not properly shrunk and contained an extra (null) byte that made it into the string.
  • + *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size was wrong for files of size 31, 34, and 37 + * bytes.
  • + *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing the Base64.OutputStream closed the Base64 encoding + * (by padding with equals signs) too soon. Also added an option to suppress the automatic decoding of gzipped streams. Also + * added experimental support for specifying a class loader when using the + * {@link #decodeToObject(String, int, ClassLoader)} method.
  • + *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java footprint with its CharEncoders and so + * forth. Fixed some javadocs that were inconsistent. Removed imports and specified things like java.io.IOException explicitly + * inline.
  • + *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the final encoded data will be so that the + * code doesn't have to create two output arrays: an oversized initial one and then a final, exact-sized one. Big win when using + * the {@link #encodeBytesToBytes(byte[])} family of methods (and not using the gzip options which uses a different mechanism + * with streams and stuff).
  • + *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some similar helper methods to be more efficient + * with memory by not returning a String but just a byte array.
  • + *
  • v2.3 - This is not a drop-in replacement! This is two years of comments and bug fixes queued up and + * finally executed. Thanks to everyone who sent me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone + * else. Much bad coding was cleaned up including throwing exceptions where necessary instead of returning null values or + * something similar. Here are some changes that may affect you: + *
      + *
    • Does not break lines, by default. This is to keep in compliance with RFC3548.
    • + *
    • Throws exceptions instead of returning null values. Because some operations (especially those that may permit + * the GZIP option) use IO streams, there is a possibility of an java.io.IOException being thrown. After some discussion and + * thought, I've changed the behavior of the methods to throw java.io.IOExceptions rather than return null if ever there's an + * error. I think this is more appropriate, though it will require some changes to your code. Sorry, it should have been done + * this way to begin with.
    • + *
    • Removed all references to System.out, System.err, and the like. Shame on me. All I can say is sorry they were + * ever there.
    • + *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed such as when passed arrays are null or + * offsets are invalid.
    • + *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. This was especially annoying before for people who + * were thorough in their own projects and then had gobs of javadoc warnings on this file.
    • + *
    + *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug when using very small files (~< 40 bytes).
  • + *
  • v2.2 - Added some helper methods for encoding/decoding directly from one file to the next. Also added a main() method to + * support command line encoding/decoding from one file to the next. Also added these Base64 dialects: + *
      + *
    1. The default is RFC3548 format.
    2. + *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates URL and file name friendly format as described in + * Section 4 of RFC3548. http://www.faqs.org/rfcs/rfc3548.html
    4. + *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates URL and file name friendly format that preserves + * lexical ordering as described in http://www.faqs.org/qa/rfcc-1940.html
    6. + *
    + * Special thanks to Jim Kellerman at http://www.powerset.com/ for contributing the new + * Base64 dialects.
  • + * + *
  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added some convenience methods for reading and + * writing to and from files.
  • + *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems with other encodings (like EBCDIC).
  • + *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the encoded data was a single byte.
  • + *
  • v2.0 - I got rid of methods that used booleans to set options. Now everything is more consolidated and cleaner. The code + * now detects when data that's being decoded is gzip-compressed and will decompress it automatically. Generally things are + * cleaner. You'll probably have to change some method calls that you were making to support the new options format ( + * ints that you "OR" together).
  • + *
  • v1.5.1 - Fixed bug when decompressing and decoding to a byte[] using decode( String s, boolean gzipCompressed ). + * Added the ability to "suspend" encoding in the Output Stream so you can turn on and off the encoding if you need to embed + * base64 data in an otherwise "normal" stream (like an XML file).
  • + *
  • v1.5 - Output stream passes on flush() command but doesn't do anything itself. This helps when using GZIP streams. Added + * the ability to GZip-compress objects before encoding them.
  • + *
  • v1.4 - Added helper methods to read/write files.
  • + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream where last buffer being read, if not + * completely full, was not returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ * + *

+ * I am placing this code in the Public Domain. Do with it as you will. This software comes with no guarantees or warranties but + * with plenty of well-wishing instead! Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.7 + */ +class Base64 { + + /* ******** P U B L I C F I E L D S ******** */ + + /** No options specified. Value is zero. */ + public static final int NO_OPTIONS = 0; + + /** Specify encoding in first bit. Value is one. */ + public static final int ENCODE = 1; + + /** Specify decoding in first bit. Value is zero. */ + public static final int DECODE = 0; + + /** Specify that data should be gzip-compressed in second bit. Value is two. */ + public static final int GZIP = 2; + + /** Specify that gzipped data should not be automatically gunzipped. */ + public static final int DONT_GUNZIP = 4; + + /** Do break lines when encoding. Value is 8. */ + public static final int DO_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. It is important to note that data + * encoded this way is not officially valid Base64, or at the very least should not be called Base64 without also + * specifying that is was encoded using the URL- and Filename-safe dialect. + */ + public static final int URL_SAFE = 16; + + /** + * Encode using the special "ordered" dialect of Base64 described here: http://www.faqs.org/qa/rfcc-1940.html. + */ + public static final int ORDERED = 32; + + /* ******** P R I V A T E F I E L D S ******** */ + + /** Maximum line length (76) of Base64 output. */ + private static final int MAX_LINE_LENGTH = 76; + + /** The equals sign (=) as a byte. */ + private static final byte EQUALS_SIGN = (byte) '='; + + /** The new line character (\n) as a byte. */ + private static final byte NEW_LINE = (byte) '\n'; + + /** Preferred encoding. */ + private static final String PREFERRED_ENCODING = "US-ASCII"; + + private static final byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private static final byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** The 64 valid Base64 values. */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private static final byte[] _STANDARD_ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', + (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', + (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', + (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', + (byte) '8', (byte) '9', (byte) '+', (byte) '/' }; + + /** + * Translates a Base64 value to either its 6-bit reconstruction value or a negative number indicating some other meaning. + **/ + private static final byte[] _STANDARD_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 + }; + + /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. Notice that the last two bytes + * become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private static final byte[] _URL_SAFE_ALPHABET = { (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', + (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', + (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W', (byte) 'X', + (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', + (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', + (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6', (byte) '7', + (byte) '8', (byte) '9', (byte) '-', (byte) '_' }; + + /** + * Used in decoding URL- and Filename-safe dialects of Base64. + */ + private static final byte[] _URL_SAFE_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 + }; + + /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, and it is described here: http://www.faqs.org/qa/rfcc-1940.html. + */ + private static final byte[] _ORDERED_ALPHABET = { (byte) '-', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', + (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', + (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', + (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', + (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', + (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', + (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', + (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z' }; + + /** + * Used in decoding the "ordered" dialect of Base64. + */ + private static final byte[] _ORDERED_DECODABET = { -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' + -9, -9, -9, -9, -9 // Decimal 123 - 127 + , -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 128 - 139 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 140 - 152 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 153 - 165 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 166 - 178 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 179 - 191 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 192 - 204 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 205 - 217 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 218 - 230 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 231 - 243 + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9 // Decimal 244 - 255 + }; + + /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on the options specified. It's possible, though silly, to + * specify ORDERED and URLSAFE in which case one of them will be picked, though there is no guarantee as to which one + * will be picked. + */ + private static byte[] getAlphabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_ALPHABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_ALPHABET; + } else { + return _STANDARD_ALPHABET; + } + } // end getAlphabet + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on the options specified. It's possible, though silly, to + * specify ORDERED and URL_SAFE in which case one of them will be picked, though there is no guarantee as to which one will + * be picked. + */ + private static byte[] getDecodabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_DECODABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_DECODABET; + } else { + return _STANDARD_DECODABET; + } + } // end getAlphabet + + /** Defeats instantiation. */ + private Base64() { + } + + /* ******** E N C O D I N G M E T H O D S ******** */ + + /** + * Encodes up to the first three bytes of array threeBytes and returns a four-byte array in Base64 notation. The + * actual number of significant bytes in your array is given by numSigBytes. The array threeBytes + * needs only be as big as numSigBytes. Code can reuse a byte array by passing a four-byte array as + * b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); + return b4; + } // end encode3to4 + + /** + *

+ * Encodes up to three bytes of the array source and writes the resulting four Base64 bytes to + * destination. The source and destination arrays can be manipulated anywhere along their length by specifying + * srcOffset and destOffset. This method does not check to make sure your arrays are large enough to + * accomodate srcOffset + 3 for the source array or destOffset + 4 for the + * destination array. The actual number of significant bytes in your array is given by numSigBytes. + *

+ *

+ * This is the lowest level of the encoding methods with all possible parameters. + *

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4(byte[] source, int srcOffset, int numSigBytes, byte[] destination, int destOffset, + int options) { + + byte[] ALPHABET = getAlphabet(options); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + /** + * Performs Base64 encoding on the raw ByteBuffer, writing it to the encoded ByteBuffer. This is + * an experimental feature. Currently it does not pass along any options (such as {@link #DO_BREAK_LINES} or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode(java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while (raw.hasRemaining()) { + int rem = Math.min(3, raw.remaining()); + raw.get(raw3, 0, rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + encoded.put(enc4); + } // end input remaining + } + + /** + * Performs Base64 encoding on the raw ByteBuffer, writing it to the encoded CharBuffer. This is + * an experimental feature. Currently it does not pass along any options (such as {@link #DO_BREAK_LINES} or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode(java.nio.ByteBuffer raw, java.nio.CharBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while (raw.hasRemaining()) { + int rem = Math.min(3, raw.remaining()); + raw.get(raw3, 0, rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + for (int i = 0; i < 4; i++) { + encoded.put((char) (enc4[i] & 0xFF)); + } + } // end input remaining + } + + /** + * Serializes an object and returns the Base64-encoded version of that serialized object. + * + *

+ * As of v 2.3, if the object cannot be serialized or there is another error, the method will throw an java.io.IOException. + * This is new to v2.3! In earlier versions, it just returned a null value, but in retrospect that's a pretty poor + * way to handle it. + *

+ * + * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @throws java.io.IOException if there is an error + * @throws NullPointerException if serializedObject is null + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) throws java.io.IOException { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + /** + * Serializes an object and returns the Base64-encoded version of that serialized object. + * + *

+ * As of v 2.3, if the object cannot be serialized or there is another error, the method will throw an java.io.IOException. + * This is new to v2.3! In earlier versions, it just returned a null value, but in retrospect that's a pretty poor + * way to handle it. + *

+ * + * The object is not GZip-compressed before being encoded. + *

+ * Example options: + * + *

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     * 
+ *

+ * Example: encodeObject( myObj, Base64.GZIP ) or + *

+ * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, int options) throws java.io.IOException { + + if (serializableObject == null) { + throw new NullPointerException("Cannot serialize a null object."); + } // end if: null + + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.util.zip.GZIPOutputStream gzos = null; + java.io.ObjectOutputStream oos = null; + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + if ((options & GZIP) != 0) { + // Gzip + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream(gzos); + } else { + // Not gzipped + oos = new java.io.ObjectOutputStream(b64os); + } + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + // Fall back to some Java default + return new String(baos.toByteArray()); + } // end catch + + } // end encode + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + * @param source The data to convert + * @return The data in Base64-encoded form + * @throws NullPointerException if source array is null + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options: + * + *

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

+ * As of v 2.3, if there is an error with the GZIP stream, the method will throw an java.io.IOException. This is new to + * v2.3! In earlier versions, it just returned a null value, but in retrospect that's a pretty poor way to handle it. + *

+ * + * + * @param source The data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) throws java.io.IOException { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. Does not GZip-compress data. + * + *

+ * As of v 2.3, if there is an error, the method will throw an java.io.IOException. This is new to v2.3! In earlier + * versions, it just returned a null value, but in retrospect that's a pretty poor way to handle it. + *

+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @return The Base64-encoded data as a String + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, off, len, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options: + * + *

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * + *

+ * As of v 2.3, if there is an error with the GZIP stream, the method will throw an java.io.IOException. This is new to + * v2.3! In earlier versions, it just returned a null value, but in retrospect that's a pretty poor way to handle it. + *

+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, int options) throws java.io.IOException { + byte[] encoded = encodeBytesToBytes(source, off, len, options); + + // Return value according to relevant encoding. + try { + return new String(encoded, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(encoded); + } // end catch + + } // end encodeBytes + + /** + * Similar to {@link #encodeBytes(byte[])} but returns a byte array instead of instantiating a String. This is more + * efficient if you're working with I/O streams and have large data sets to encode. + * + * + * @param source The data to convert + * @return The Base64-encoded data as a byte[] (of ASCII characters) + * @throws NullPointerException if source array is null + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source) { + byte[] encoded = null; + try { + encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return encoded; + } + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns a byte array instead of instantiating a String. This + * is more efficient if you're working with I/O streams and have large data sets to encode. + * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) throws java.io.IOException { + + if (source == null) { + throw new NullPointerException("Cannot serialize a null array."); + } // end if: null + + if (off < 0) { + throw new IllegalArgumentException("Cannot have negative offset: " + off); + } // end if: off < 0 + + if (len < 0) { + throw new IllegalArgumentException("Cannot have length offset: " + len); + } // end if: len < 0 + + if (off + len > source.length) { + throw new IllegalArgumentException(String.format( + "Cannot have offset of %d and length of %d with array of length %d", off, len, source.length)); + } // end if: off < 0 + + // Compress? + if ((options & GZIP) != 0) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + return baos.toByteArray(); + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + boolean breakLines = (options & DO_BREAK_LINES) != 0; + + // int len43 = len * 4 / 3; + // byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding + if (breakLines) { + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + } + byte[] outBuff = new byte[encLen]; + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e, options); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, options); + e += 4; + } // end if: some padding needed + + // Only resize array if we didn't guess it right. + if (e <= outBuff.length - 1) { + // If breaking lines and the last byte falls right at + // the line length (76 bytes per line), there will be + // one extra byte, and the array will need to be resized. + // Not too bad of an estimate on array size, I'd say. + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff, 0, finalOut, 0, e); + // System.err.println("Having to resize array from " + outBuff.length + " to " + e ); + return finalOut; + } else { + // System.err.println("No need to resize array."); + return outBuff; + } + + } // end else: don't compress + + } // end encodeBytesToBytes + + /* ******** D E C O D I N G M E T H O D S ******** */ + + /** + * Decodes four bytes from array source and writes the resulting bytes (up to three of them) to + * destination. The source and destination arrays can be manipulated anywhere along their length by specifying + * srcOffset and destOffset. This method does not check to make sure your arrays are large enough to + * accommodate srcOffset + 4 for the source array or destOffset + 3 for the + * destination array. This method returns the actual number of bytes that were converted from the Base64 + * encoding. + *

+ * This is the lowest level of the decoding methods with all possible parameters. + *

+ * + * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @throws NullPointerException if source or destination arrays are null + * @throws IllegalArgumentException if srcOffset or destOffset are invalid or there is not enough room in the array. + * @since 1.3 + */ + private static int decode4to3(byte[] source, int srcOffset, byte[] destination, int destOffset, int options) { + + // Lots of error checking and exception throwing + if (source == null) { + throw new NullPointerException("Source array was null."); + } // end if + if (destination == null) { + throw new NullPointerException("Destination array was null."); + } // end if + if (srcOffset < 0 || srcOffset + 3 >= source.length) { + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, + srcOffset)); + } // end if + if (destOffset < 0 || destOffset + 2 >= destination.length) { + throw new IllegalArgumentException(String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", + destination.length, destOffset)); + } // end if + + byte[] DECODABET = getDecodabet(options); + + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + // int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } + } // end decodeToBytes + + /** + * Low-level access to decoding ASCII characters in the form of a byte array. Ignores GUNZIP option, if it's + * set. This is not generally a recommended method, although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, if you need more speed and reduced memory footprint (and + * aren't gzipping), consider this method. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 2.3.1 + */ + public static byte[] decode(byte[] source) throws java.io.IOException { + byte[] decoded = null; + // try { + decoded = decode(source, 0, source.length, Base64.NO_OPTIONS); + // } catch( java.io.IOException ex ) { + // assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + // } + return decoded; + } + + /** + * Low-level access to decoding ASCII characters in the form of a byte array. Ignores GUNZIP option, if it's + * set. This is not generally a recommended method, although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, if you need more speed and reduced memory footprint (and + * aren't gzipping), consider this method. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param options Can specify options such as alphabet type to use + * @return decoded data + * @throws java.io.IOException If bogus characters exist in source data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len, int options) throws java.io.IOException { + + // Lots of error checking and exception throwing + if (source == null) { + throw new NullPointerException("Cannot decode null source array."); + } // end if + if (off < 0 || off + len > source.length) { + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len)); + } // end if + + if (len == 0) { + return new byte[0]; + } else if (len < 4) { + throw new IllegalArgumentException( + "Base64-encoded string must have at least four characters, but length specified was " + len); + } // end if + + byte[] DECODABET = getDecodabet(options); + + int len34 = len * 3 / 4; // Estimate on array size + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; // Keep track of where we're writing + + byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space + int b4Posn = 0; // Keep track of four byte input buffer + int i = 0; // Source array counter + byte sbiDecode = 0; // Special value from DECODABET + + for (i = off; i < off + len; i++) { // Loop through source + + sbiDecode = DECODABET[source[i] & 0xFF]; + + // White space, Equals sign, or legit Base64 character + // Note the values such as -5 and -9 in the + // DECODABETs at the top of the file. + if (sbiDecode >= WHITE_SPACE_ENC) { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = source[i]; // Save non-whitespace + if (b4Posn > 3) { // Time to decode? + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (source[i] == EQUALS_SIGN) { + break; + } // end if: equals sign + } // end if: quartet built + } // end if: equals sign or better + } // end if: white space, equals sign or better + else { + // There's a bad input character in the Base64 stream. + throw new java.io.IOException(String.format("Bad Base64 input character decimal %d in array position %d", + ((int) source[i]) & 0xFF, i)); + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + /** + * Decodes data from Base64 notation, automatically detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @throws java.io.IOException If there is a problem + * @since 1.4 + */ + public static byte[] decode(String s) throws java.io.IOException { + return decode(s, NO_OPTIONS); + } + + /** + * Decodes data from Base64 notation, automatically detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * @return the decoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if s is null + * @since 1.4 + */ + public static byte[] decode(String s, int options) throws java.io.IOException { + + if (s == null) { + throw new NullPointerException("Input string was null."); + } // end if + + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length, options); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + boolean dontGunzip = (options & DONT_GUNZIP) != 0; + if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) { + + int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) { + baos.write(buffer, 0, length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(e); + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. Returns null if there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a class that cannot be found by the JVM + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) throws java.io.IOException, ClassNotFoundException { + return decodeToObject(encodedObject, NO_OPTIONS, null); + } + + /** + * Attempts to decode Base64 data and deserialize a Java Object within. Returns null if there was an error. If + * loader is not null, it will be the class loader used when deserializing. + * + * @param encodedObject The Base64 data to decode + * @param options Various parameters related to decoding + * @param loader Optional class loader to use in deserializing classes. + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a class that cannot be found by the JVM + * @since 2.3.4 + */ + public static Object decodeToObject(String encodedObject, int options, final ClassLoader loader) + throws java.io.IOException, ClassNotFoundException { + + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject, options); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + + // If no custom class loader is provided, use Java's builtin OIS. + if (loader == null) { + ois = new java.io.ObjectInputStream(bais); + } // end if: no loader provided + + // Else make a customized object input stream that uses + // the provided class loader. + else { + ois = new java.io.ObjectInputStream(bais) { + @Override + public Class resolveClass(java.io.ObjectStreamClass streamClass) throws java.io.IOException, + ClassNotFoundException { + Class c = Class.forName(streamClass.getName(), false, loader); + if (c == null) { + return super.resolveClass(streamClass); + } else { + return c; // Class loader knows of this class. + } // end else: not null + } // end resolveClass + }; // end ois + } // end else: no custom class loader + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + catch (ClassNotFoundException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + /** + * Convenience method for encoding data to a file. + * + *

+ * As of v 2.3, if there is a error, the method will throw an java.io.IOException. This is new to v2.3! In earlier + * versions, it just returned false, but in retrospect that's a pretty poor way to handle it. + *

+ * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if dataToEncode is null + * @since 2.1 + */ + public static void encodeToFile(byte[] dataToEncode, String filename) throws java.io.IOException { + + if (dataToEncode == null) { + throw new NullPointerException("Data to encode was null."); + } // end iff + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream(new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + } // end encodeToFile + + /** + * Convenience method for decoding data to a file. + * + *

+ * As of v 2.3, if there is a error, the method will throw an java.io.IOException. This is new to v2.3! In earlier + * versions, it just returned false, but in retrospect that's a pretty poor way to handle it. + *

+ * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static void decodeToFile(String dataToDecode, String filename) throws java.io.IOException { + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream(new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + } // end decodeToFile + + /** + * Convenience method for reading a base64-encoded file and decoding it. + * + *

+ * As of v 2.3, if there is a error, the method will throw an java.io.IOException. This is new to v2.3! In earlier + * versions, it just returned false, but in retrospect that's a pretty poor way to handle it. + *

+ * + * @param filename Filename for reading encoded data + * @return decoded byte array + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) throws java.io.IOException { + + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) { + throw new java.io.IOException("File is too big for this convenience method (" + file.length() + " bytes)."); + } // end if: file too big for int index + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream(new java.io.FileInputStream(file)), Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } // end while + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + /** + * Convenience method for reading a binary file and base64-encoding it. + * + *

+ * As of v 2.3, if there is a error, the method will throw an java.io.IOException. This is new to v2.3! In earlier + * versions, it just returned false, but in retrospect that's a pretty poor way to handle it. + *

+ * + * @param filename Filename for reading binary data + * @return base64-encoded string + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static String encodeFromFile(String filename) throws java.io.IOException { + + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = new byte[Math.max((int) (file.length() * 1.4 + 1), 40)]; // Need max() for math on small files + // (v2.2.1); Need +1 for a few corner cases + // (v2.3.5) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream(new java.io.BufferedInputStream(new java.io.FileInputStream(file)), Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } // end while + + // Save in a variable to return + encodedData = new String(buffer, 0, length, Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void encodeFileToFile(String infile, String outfile) throws java.io.IOException { + + String encoded = Base64.encodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outfile)); + out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output. + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { + out.close(); + } catch (Exception ex) { + } + } // end finally + } // end encodeFileToFile + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void decodeFileToFile(String infile, String outfile) throws java.io.IOException { + + byte[] decoded = Base64.decodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new java.io.BufferedOutputStream(new java.io.FileOutputStream(outfile)); + out.write(decoded); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { + out.close(); + } catch (Exception ex) { + } + } // end finally + } // end decodeFileToFile + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + /** + * A {@link Base64.InputStream} will read data from another java.io.InputStream, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + + private boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private byte[] buffer; // Small buffer holding converted data + private int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private boolean breakLines; // Break lines at less than 80 characters + private int options; // Record options used to create the stream. + private byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the java.io.InputStream from which to read data. + * @since 1.3 + */ + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor + + /** + * Constructs a {@link Base64.InputStream} in either ENCODE or DECODE mode. + *

+ * Valid options: + * + *

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.InputStream( in, Base64.DECODE ) + * + * + * @param in the java.io.InputStream from which to read data. + * @param options Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public InputStream(java.io.InputStream in, int options) { + + super(in); + this.options = options; // Record for later + this.breakLines = (options & DO_BREAK_LINES) > 0; + this.encode = (options & ENCODE) > 0; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + this.decodabet = getDecodabet(options); + } // end constructor + + /** + * Reads enough of the input stream to convert to/from Base64 and returns the next byte. + * + * @return next byte + * @since 1.3 + */ + @Override + public int read() throws java.io.IOException { + + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } else { + break; // out of for loop + } // end else: end of stream + + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; // Must be end of stream + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { + b = in.read(); + } while (b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) { + break; // Reads a -1 if end of stream + } // end if: end of stream + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0, options); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException("Improperly padded Base64 input."); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if ( /* !encode && */position >= numSigBytes) { + return -1; + } // end if: got data + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) { + position = -1; + } // end if: end + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + throw new java.io.IOException("Error in Base64 code reading stream."); + } // end else + } // end read + + /** + * Calls {@link #read()} repeatedly until the end of stream is reached or len bytes are read. Returns number + * of bytes read into array or -1 if end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + @Override + public int read(byte[] dest, int off, int len) throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + if (b >= 0) { + dest[off + i] = (byte) b; + } else if (i == 0) { + return -1; + } else { + break; // Out of 'for' loop + } // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + /** + * A {@link Base64.OutputStream} will write data to another java.io.OutputStream, given in the constructor, and + * encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private int options; // Record for later + private byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + /** + * Constructs a {@link Base64.OutputStream} in either ENCODE or DECODE mode. + *

+ * Valid options: + * + *

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DO_BREAK_LINES) != 0; + this.encode = (options & ENCODE) != 0; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.decodabet = getDecodabet(options); + } // end constructor + + /** + * Writes the byte to the output stream after converting to/from Base64 notation. When encoding, bytes are buffered + * three at a time before the output stream actually gets a write() call. When decoding, bytes are buffered four at a + * time. + * + * @param theByte the byte to write + * @since 1.3 + */ + @Override + public void write(int theByte) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theByte); + return; + } // end if: suspended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to encode. + + this.out.write(encode3to4(b4, buffer, bufferLength, options)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + this.out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to output. + + int len = Base64.decode4to3(buffer, 0, b4, 0, options); + out.write(b4, 0, len); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException("Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + + /** + * Calls {@link #write(int)} repeatedly until len bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + @Override + public void write(byte[] theBytes, int off, int len) throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theBytes, off, len); + return; + } // end if: suspended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + + } // end write + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] This pads the buffer without closing the stream. + * + * @throws java.io.IOException if there's an error. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position, options)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException("Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + /** + * Suspends encoding of the stream. May be helpful if you need to embed a piece of base64-encoded data in a stream. + * + * @throws java.io.IOException if there's an error flushing + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + /** + * Resumes encoding of the stream. May be helpful if you need to embed a piece of base64-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + } // end inner class OutputStream + +} // end class Base64 Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Hybi07Handshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Hybi07Handshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Hybi07Handshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,105 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + + +import io.undertow.util.Headers; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketUtils; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.protocol.Handshake; +import io.undertow.websockets.spi.WebSocketHttpExchange; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Set; + +/** + * The handshaking protocol implementation for Hybi-07. + * + * @author Mike Brock + */ +public class Hybi07Handshake extends Handshake { + + public static final String MAGIC_NUMBER = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + protected final boolean allowExtensions; + + protected Hybi07Handshake(final WebSocketVersion version, final Set subprotocols, boolean allowExtensions) { + super(version, "SHA1", MAGIC_NUMBER, subprotocols); + this.allowExtensions = allowExtensions; + } + + public Hybi07Handshake(final Set subprotocols, boolean allowExtensions) { + this(WebSocketVersion.V07, subprotocols, allowExtensions); + } + + public Hybi07Handshake() { + this(WebSocketVersion.V07, Collections.emptySet(), false); + } + + @Override + public boolean matches(final WebSocketHttpExchange exchange) { + if (exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_KEY_STRING) != null && + exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_VERSION_STRING) != null) { + return exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_VERSION_STRING) + .equals(getVersion().toHttpHeaderValue()); + } + return false; + } + + protected void handshakeInternal(final WebSocketHttpExchange exchange) { + + String origin = exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_ORIGIN_STRING); + if (origin != null) { + exchange.setResponseHeader(Headers.SEC_WEB_SOCKET_ORIGIN_STRING, origin); + } + selectSubprotocol(exchange); + exchange.setResponseHeader(Headers.SEC_WEB_SOCKET_LOCATION_STRING, getWebSocketLocation(exchange)); + + final String key = exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_KEY_STRING); + try { + final String solution = solve(key); + exchange.setResponseHeader(Headers.SEC_WEB_SOCKET_ACCEPT_STRING, solution); + performUpgrade(exchange); + } catch (NoSuchAlgorithmException e) { + IoUtils.safeClose(exchange); + exchange.endExchange(); + return; + } + + } + + protected final String solve(final String nonceBase64) throws NoSuchAlgorithmException { + final String concat = nonceBase64.trim() + getMagicNumber(); + final MessageDigest digest = MessageDigest.getInstance(getHashAlgorithm()); + digest.update(concat.getBytes(WebSocketUtils.UTF_8)); + return Base64.encodeBytes(digest.digest()).trim(); + } + + @Override + public WebSocketChannel createChannel(WebSocketHttpExchange exchange, final StreamConnection channel, final Pool pool) { + return new WebSocket07Channel(channel, pool, getWebSocketLocation(exchange), exchange.getResponseHeader(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING), false, allowExtensions, exchange.getPeerConnections()); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Masker.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Masker.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/Masker.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,79 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.websockets.core.function.ChannelFunction; + +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +final class Masker implements ChannelFunction { + + private byte[] maskingKey; + int m; + + Masker(int maskingKey) { + this.maskingKey = createsMaskingKey(maskingKey); + } + + public void setMaskingKey(int maskingKey) { + this.maskingKey = createsMaskingKey(maskingKey); + m = 0; + } + + private static byte[] createsMaskingKey(int maskingKey) { + byte[] key = new byte[4]; + key[0] = (byte) (maskingKey >> 24 & 0xFF); + key[1] = (byte) (maskingKey >> 16 & 0xFF); + key[2] = (byte) (maskingKey >> 8 & 0xFF); + key[3] = (byte) (maskingKey & 0xFF); + return key; + } + + private void mask(ByteBuffer buf, int position, int length) { + int limit = position + length; + for (int i = position ; i < limit; ++i) { + buf.put(i, (byte) (buf.get(i) ^ maskingKey[m++])); + m %= 4; + } + } + + @Override + public void newFrame(FrameHeaderData headerData) { + WebSocket07Channel.WebSocketFrameHeader header = (WebSocket07Channel.WebSocketFrameHeader) headerData; + setMaskingKey(header.getMaskingKey()); + } + + @Override + public void afterRead(ByteBuffer buf, int position, int length) { + mask(buf, position, length); + } + + @Override + public void beforeWrite(ByteBuffer buf, int position, int length) { + mask(buf, position, length); + } + + @Override + public void complete() { + // noop + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/UTF8Checker.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/UTF8Checker.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/UTF8Checker.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,105 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.server.protocol.framed.FrameHeaderData; +import io.undertow.websockets.core.WebSocketMessages; +import io.undertow.websockets.core.function.ChannelFunction; + +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +/** + * An utility class which can be used to check if a sequence of bytes or ByteBuffers contain non UTF-8 data. + *

+ * Please use a new instance per stream. + * + * @author Norman Maurer + */ +final class UTF8Checker implements ChannelFunction { + + + private static final int UTF8_ACCEPT = 0; + private static final int UTF8_REJECT = 12; + + private static final byte[] TYPES = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, + 1, 1, 1, 1, 1, 1, 1, 1, 1, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 7, 7, 7, 7, + 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 8, + 8, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, + 2, 2, 10, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 4, 3, 3, 11, 6, 6, 6, 5, 8, 8, 8, 8, 8, + 8, 8, 8, 8, 8, 8}; + + private static final byte[] STATES = {0, 12, 24, 36, 60, 96, 84, 12, 12, 12, 48, 72, 12, 12, + 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 0, 12, 12, 12, 12, 12, 0, 12, 0, 12, 12, + 12, 24, 12, 12, 12, 12, 12, 24, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, + 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 24, 12, 12, 12, 12, 12, 12, 12, 12, 12, 36, + 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, 12, 36, 12, 36, 12, 12, 12, 36, 12, 12, 12, 12, + 12, 12, 12, 12, 12, 12}; + + private int state = UTF8_ACCEPT; + + private void checkUTF8(int b) throws UnsupportedEncodingException { + byte type = TYPES[b & 0xFF]; + + state = STATES[state + type]; + + if (state == UTF8_REJECT) { + throw WebSocketMessages.MESSAGES.invalidTextFrameEncoding(); + } + } + + /** + * Check if the given ByteBuffer contains non UTF-8 data. + * + * @param buf the ByteBuffer to check + * @param position the index in the {@link ByteBuffer} to start from + * @param length the number of bytes to operate on + * @throws UnsupportedEncodingException is thrown if non UTF-8 data is found + */ + private void checkUTF8(ByteBuffer buf, int position, int length) throws UnsupportedEncodingException { + int limit = position + length; + for (int i = position; i < limit; i++) { + checkUTF8(buf.get(i)); + } + } + + @Override + public void newFrame(FrameHeaderData headerData) { + } + + @Override + public void afterRead(ByteBuffer buf, int position, int length) throws UnsupportedEncodingException{ + checkUTF8(buf, position, length); + } + + @Override + public void beforeWrite(ByteBuffer buf, int position, int length) throws UnsupportedEncodingException{ + checkUTF8(buf, position, length); + } + + @Override + public void complete() throws UnsupportedEncodingException { + if (state != UTF8_ACCEPT) { + throw WebSocketMessages.MESSAGES.invalidTextFrameEncoding(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07BinaryFrameSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07BinaryFrameSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07BinaryFrameSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.WebSocketFrameType; + +/** + * @author Norman Maurer + */ +class WebSocket07BinaryFrameSinkChannel extends WebSocket07FrameSinkChannel { + + WebSocket07BinaryFrameSinkChannel(WebSocket07Channel wsChannel, long payloadSize) { + super(wsChannel, WebSocketFrameType.BINARY, payloadSize); + } + + @Override + public boolean isFragmentationSupported() { + return true; + } + + @Override + public boolean areExtensionsSupported() { + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07BinaryFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07BinaryFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07BinaryFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,39 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.FixedPayloadFrameSourceChannel; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + + +/** + * @author Norman Maurer + */ +class WebSocket07BinaryFrameSourceChannel extends FixedPayloadFrameSourceChannel { + WebSocket07BinaryFrameSourceChannel(WebSocketChannel wsChannel, long payloadSize, int rsv, boolean finalFragment, Masker masker, Pooled pooled, long frameLength) { + super(wsChannel, WebSocketFrameType.BINARY, payloadSize, rsv, finalFragment, pooled, frameLength, masker); + } + + WebSocket07BinaryFrameSourceChannel(WebSocketChannel wsChannel, long payloadSize, int rsv, boolean finalFragment, Pooled pooled, long frameLength) { + super(wsChannel, WebSocketFrameType.BINARY, payloadSize, rsv, finalFragment, pooled, frameLength); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07Channel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07Channel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07Channel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,483 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.server.protocol.framed.AbstractFramedStreamSourceChannel; +import io.undertow.websockets.core.StreamSinkFrameChannel; +import io.undertow.websockets.core.StreamSourceFrameChannel; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketException; +import io.undertow.websockets.core.WebSocketFrame; +import io.undertow.websockets.core.WebSocketFrameCorruptedException; +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.websockets.core.WebSocketLogger; +import io.undertow.websockets.core.WebSocketMessages; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.function.ChannelFunction; + +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.util.Set; + + +/** + * {@link WebSocketChannel} which is used for {@link WebSocketVersion#V08} + * + * @author Norman Maurer + */ +public class WebSocket07Channel extends WebSocketChannel { + + private enum State { + READING_FIRST, + READING_SECOND, + READING_EXTENDED_SIZE1, + READING_EXTENDED_SIZE2, + READING_EXTENDED_SIZE3, + READING_EXTENDED_SIZE4, + READING_EXTENDED_SIZE5, + READING_EXTENDED_SIZE6, + READING_EXTENDED_SIZE7, + READING_EXTENDED_SIZE8, + READING_MASK_1, + READING_MASK_2, + READING_MASK_3, + READING_MASK_4, + DONE, + } + + private int fragmentedFramesCount; + private final ByteBuffer lengthBuffer = ByteBuffer.allocate(8); + + private UTF8Checker checker; + + protected static final byte OPCODE_CONT = 0x0; + protected static final byte OPCODE_TEXT = 0x1; + protected static final byte OPCODE_BINARY = 0x2; + protected static final byte OPCODE_CLOSE = 0x8; + protected static final byte OPCODE_PING = 0x9; + protected static final byte OPCODE_PONG = 0xA; + + private static final ChannelFunction[] EMPTY_FUNCTIONS = new ChannelFunction[0]; + + /** + * Create a new {@link WebSocket07Channel} + * + * @param channel The {@link StreamConnection} over which the WebSocket Frames should get send and received. + * Be aware that it already must be "upgraded". + * @param bufferPool The {@link Pool} which will be used to acquire {@link ByteBuffer}'s from. + * @param wsUrl The url for which the {@link WebSocket07Channel} was created. + */ + public WebSocket07Channel(StreamConnection channel, Pool bufferPool, + String wsUrl, String subProtocol, final boolean client, boolean allowExtensions, Set openConnections) { + super(channel, bufferPool, WebSocketVersion.V08, wsUrl, subProtocol, client, allowExtensions, openConnections); + } + + @Override + protected PartialFrame receiveFrame() { + return new WebSocketFrameHeader(); + } + + @Override + protected void markReadsBroken(Throwable cause) { + super.markReadsBroken(cause); + } + + @Override + protected void closeSubChannels() { + IoUtils.safeClose(fragmentedChannel); + } + + @Override + protected StreamSinkFrameChannel createStreamSinkChannel(WebSocketFrameType type, long payloadSize) { + switch (type) { + case TEXT: + return new WebSocket07TextFrameSinkChannel(this, payloadSize); + case BINARY: + return new WebSocket07BinaryFrameSinkChannel(this, payloadSize); + case CLOSE: + return new WebSocket07CloseFrameSinkChannel(this, payloadSize); + case PONG: + return new WebSocket07PongFrameSinkChannel(this, payloadSize); + case PING: + return new WebSocket07PingFrameSinkChannel(this, payloadSize); + default: + throw WebSocketMessages.MESSAGES.unsupportedFrameType(type); + } + } + + class WebSocketFrameHeader implements WebSocketFrame { + + private boolean frameFinalFlag; + private int frameRsv; + private int frameOpcode; + private int maskingKey; + private boolean frameMasked; + private long framePayloadLength; + private State state = State.READING_FIRST; + private int framePayloadLen1; + private boolean done = false; + + @Override + public StreamSourceFrameChannel getChannel(Pooled pooled) { + StreamSourceFrameChannel channel = createChannel(pooled); + if (frameFinalFlag) { + channel.finalFrame(); + } else { + fragmentedChannel = channel; + } + return channel; + } + + public StreamSourceFrameChannel createChannel(Pooled pooled) { + + + // Processing ping/pong/close frames because they cannot be + // fragmented as per spec + if (frameOpcode == OPCODE_PING) { + if (frameMasked) { + return new WebSocket07PingFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, new Masker(maskingKey), pooled, framePayloadLength); + } else { + return new WebSocket07PingFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, pooled, framePayloadLength); + } + } + if (frameOpcode == OPCODE_PONG) { + if (frameMasked) { + return new WebSocket07PongFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, new Masker(maskingKey), pooled, framePayloadLength); + } else { + return new WebSocket07PongFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, pooled, framePayloadLength); + } + } + if (frameOpcode == OPCODE_CLOSE) { + if (frameMasked) { + return new WebSocket07CloseFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, new Masker(maskingKey), pooled, framePayloadLength); + } else { + return new WebSocket07CloseFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, pooled, framePayloadLength); + } + } + + if (frameOpcode == OPCODE_TEXT) { + // try to grab the checker which was used before + UTF8Checker checker = WebSocket07Channel.this.checker; + if (checker == null) { + checker = new UTF8Checker(); + } + + if (!frameFinalFlag) { + // if this is not the final fragment store the used checker to use it in later fragments also + WebSocket07Channel.this.checker = checker; + } else { + // was the final fragment reset the checker to null + WebSocket07Channel.this.checker = null; + } + + if (frameMasked) { + return new WebSocket07TextFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, frameFinalFlag, new Masker(maskingKey), checker, pooled, framePayloadLength); + } else { + return new WebSocket07TextFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, frameFinalFlag, checker, pooled, framePayloadLength); + } + } else if (frameOpcode == OPCODE_BINARY) { + if (frameMasked) { + return new WebSocket07BinaryFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, frameFinalFlag, new Masker(maskingKey), pooled, framePayloadLength); + } else { + return new WebSocket07BinaryFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, frameFinalFlag, pooled, framePayloadLength); + } + } else if (frameOpcode == OPCODE_CONT) { + final ChannelFunction[] functions; + if (frameMasked && checker != null) { + functions = new ChannelFunction[2]; + functions[0] = new Masker(maskingKey); + functions[1] = checker; + } else if (frameMasked) { + functions = new ChannelFunction[1]; + functions[0] = new Masker(maskingKey); + } else if (checker != null) { + functions = new ChannelFunction[1]; + functions[0] = checker; + } else { + functions = EMPTY_FUNCTIONS; + } + if (frameMasked) { + return new WebSocket07ContinuationFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, frameFinalFlag, pooled, framePayloadLength, functions); + } else { + return new WebSocket07ContinuationFrameSourceChannel(WebSocket07Channel.this, framePayloadLength, frameRsv, frameFinalFlag, pooled, framePayloadLength, functions); + } + } else { + throw WebSocketMessages.MESSAGES.unsupportedOpCode(frameOpcode); + } + } + + @Override + public void handle(final ByteBuffer buffer) throws WebSocketException { + if (!buffer.hasRemaining()) { + return; + } + while (state != State.DONE) { + byte b; + switch (state) { + case READING_FIRST: + // Read FIN, RSV, OPCODE + b = buffer.get(); + frameFinalFlag = (b & 0x80) != 0; + frameRsv = (b & 0x70) >> 4; + frameOpcode = b & 0x0F; + + if (WebSocketLogger.REQUEST_LOGGER.isDebugEnabled()) { + WebSocketLogger.REQUEST_LOGGER.decodingFrameWithOpCode(frameOpcode); + } + state = State.READING_SECOND; + // clear the lengthBuffer to reuse it later + lengthBuffer.clear(); + case READING_SECOND: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + // Read MASK, PAYLOAD LEN 1 + // + frameMasked = (b & 0x80) != 0; + framePayloadLen1 = b & 0x7F; + + if (frameRsv != 0 && !areExtensionsSupported()) { + throw WebSocketMessages.MESSAGES.extensionsNotAllowed(frameRsv); + } + + if (frameOpcode > 7) { // control frame (have MSB in opcode set) + validateControlFrame(); + } else { // data frame + validateDataFrame(); + } + if (framePayloadLen1 == 126 || framePayloadLen1 == 127) { + state = State.READING_EXTENDED_SIZE1; + } else { + framePayloadLength = framePayloadLen1; + if (frameMasked) { + state = State.READING_MASK_1; + } else { + state = State.DONE; + } + continue; + } + case READING_EXTENDED_SIZE1: + // Read frame payload length + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + state = State.READING_EXTENDED_SIZE2; + case READING_EXTENDED_SIZE2: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + + if (framePayloadLen1 == 126) { + lengthBuffer.flip(); + // must be unsigned short + framePayloadLength = lengthBuffer.getShort() & 0xFFFF; + + if (frameMasked) { + state = State.READING_MASK_1; + } else { + state = State.DONE; + } + continue; + } + state = State.READING_EXTENDED_SIZE3; + case READING_EXTENDED_SIZE3: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + + state = State.READING_EXTENDED_SIZE4; + case READING_EXTENDED_SIZE4: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + state = State.READING_EXTENDED_SIZE5; + case READING_EXTENDED_SIZE5: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + state = State.READING_EXTENDED_SIZE6; + case READING_EXTENDED_SIZE6: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + state = State.READING_EXTENDED_SIZE7; + case READING_EXTENDED_SIZE7: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + state = State.READING_EXTENDED_SIZE8; + case READING_EXTENDED_SIZE8: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + lengthBuffer.put(b); + + lengthBuffer.flip(); + framePayloadLength = lengthBuffer.getLong(); + if (frameMasked) { + state = State.READING_MASK_1; + } else { + state = State.DONE; + break; + } + state = State.READING_MASK_1; + case READING_MASK_1: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + maskingKey = b & 0xFF; + state = State.READING_MASK_2; + case READING_MASK_2: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + maskingKey = maskingKey << 8 | b & 0xFF; + state = State.READING_MASK_3; + case READING_MASK_3: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + maskingKey = maskingKey << 8 | b & 0xFF; + state = State.READING_MASK_4; + case READING_MASK_4: + if (!buffer.hasRemaining()) { + return; + } + b = buffer.get(); + maskingKey = maskingKey << 8 | b & 0xFF; + state = State.DONE; + break; + default: + throw new IllegalStateException(state.toString()); + } + } + if (frameFinalFlag) { + // check if the frame is a ping frame as these are allowed in the middle + if (frameOpcode != OPCODE_PING) { + fragmentedFramesCount = 0; + } + } else { + // Increment counter + fragmentedFramesCount++; + } + done = true; + } + + private void validateDataFrame() throws WebSocketFrameCorruptedException { + + if (!isClient() && !frameMasked) { + throw WebSocketMessages.MESSAGES.frameNotMasked(); + } + + // check for reserved data frame opcodes + if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) { + throw WebSocketMessages.MESSAGES.reservedOpCodeInDataFrame(frameOpcode); + } + + // check opcode vs message fragmentation state 1/2 + if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { + throw WebSocketMessages.MESSAGES.continuationFrameOutsideFragmented(); + } + + // check opcode vs message fragmentation state 2/2 + if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT) { + throw WebSocketMessages.MESSAGES.nonContinuationFrameInsideFragmented(); + } + } + + private void validateControlFrame() throws WebSocketFrameCorruptedException { + // control frames MUST NOT be fragmented + if (!frameFinalFlag) { + throw WebSocketMessages.MESSAGES.fragmentedControlFrame(); + } + + // control frames MUST have payload 125 octets or less as stated in the spec + if (framePayloadLen1 > 125) { + throw WebSocketMessages.MESSAGES.toBigControlFrame(); + } + + // check for reserved control frame opcodes + if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) { + throw WebSocketMessages.MESSAGES.reservedOpCodeInControlFrame(frameOpcode); + } + + // close frame : if there is a body, the first two bytes of the + // body MUST be a 2-byte unsigned integer (in network byte + // order) representing a status code + if (frameOpcode == 8 && framePayloadLen1 == 1) { + throw WebSocketMessages.MESSAGES.controlFrameWithPayloadLen1(); + } + } + + @Override + public boolean isDone() { + return done; + } + + @Override + public long getFrameLength() { + return framePayloadLength; + } + + int getMaskingKey() { + return maskingKey; + } + + @Override + public AbstractFramedStreamSourceChannel getExistingChannel() { + if (frameOpcode == OPCODE_CONT) { + StreamSourceFrameChannel ret = fragmentedChannel; + if(frameFinalFlag) { + fragmentedChannel = null; + } + return ret; + } + return null; + } + + @Override + public boolean isFinalFragment() { + return frameFinalFlag; + } + } + + +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07CloseFrameSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07CloseFrameSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07CloseFrameSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.WebSocketFrameType; + +/** + * @author Norman Maurer + */ +class WebSocket07CloseFrameSinkChannel extends WebSocket07FrameSinkChannel { + WebSocket07CloseFrameSinkChannel(WebSocket07Channel wsChannel, long payloadSize) { + super(wsChannel, WebSocketFrameType.CLOSE, payloadSize); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07CloseFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07CloseFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07CloseFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,144 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.FixedPayloadFrameSourceChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.websockets.core.WebSocketMessages; +import org.xnio.Pooled; + +import java.io.IOException; +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +class WebSocket07CloseFrameSourceChannel extends FixedPayloadFrameSourceChannel { + private final ByteBuffer status = ByteBuffer.allocate(2); + private boolean statusValidated; + private final Masker masker; + enum State { + EOF, + DONE, + VALIDATE + } + + WebSocket07CloseFrameSourceChannel(WebSocket07Channel wsChannel, long payloadSize, int rsv, Masker masker, Pooled pooled, long frameLength) { + // no fragmentation allowed per spec + super(wsChannel, WebSocketFrameType.CLOSE, payloadSize, rsv, true, pooled, frameLength, masker, new UTF8Checker()); + this.masker = masker; + } + + WebSocket07CloseFrameSourceChannel(WebSocket07Channel wsChannel, long payloadSize, int rsv, Pooled pooled, long frameLength) { + // no fragmentation allowed per spec + super(wsChannel, WebSocketFrameType.CLOSE, payloadSize, rsv, true, pooled, frameLength, new UTF8Checker()); + masker = null; + } + + @Override + public int read(ByteBuffer dst) throws IOException { + switch (validateStatus()) { + case DONE: + if (status.hasRemaining()) { + int copied = 0; + while(dst.hasRemaining() && status.hasRemaining()) { + dst.put(status.get()); + copied++; + } + return copied; + } else { + return super.read(dst); + } + case EOF: + return -1; + default: + return 0; + } + } + + @Override + public long read(ByteBuffer[] dsts, int offset, int length) throws IOException { + switch (validateStatus()) { + case DONE: + if (status.hasRemaining()) { + int copied = 0; + for (int i = offset; i < length; i++) { + ByteBuffer dst = dsts[i]; + while(dst.hasRemaining() && status.hasRemaining()) { + dst.put(status.get()); + copied++; + } + if (dst.hasRemaining()) { + return copied + super.read(dsts, offset, length); + } + } + + return copied; + } else { + return super.read(dsts, offset, length); + } + case EOF: + return -1; + default: + return 0; + } + } + + private State validateStatus() throws IOException{ + if (statusValidated) { + return State.DONE; + } + for (;;) { + int r = super.read(status); + if (r == -1) { + return State.EOF; + } + if (!status.hasRemaining()) { + statusValidated = true; + + status.flip(); + // Must have 2 byte integer within the valid range + int statusCode = status.getShort(0); + + if (statusCode >= 0 && statusCode <= 999 || statusCode >= 1004 && statusCode <= 1006 + || statusCode >= 1012 && statusCode <= 2999) { + IOException exception = WebSocketMessages.MESSAGES.invalidCloseFrameStatusCode(statusCode); + ((WebSocket07Channel)getFramedChannel()).markReadsBroken(exception); + throw exception; + } + return State.DONE; + } + if (r == 0) { + return State.VALIDATE; + } + } + } + + @Override + protected void afterRead(ByteBuffer buffer, int position, int length) throws IOException { + // not check for utf8 when read the status code + if (!statusValidated) { + if (masker != null) { + masker.afterRead(buffer, position, length); + } + return; + } + super.afterRead(buffer, position, length); + + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07ContinuationFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07ContinuationFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07ContinuationFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,34 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.FixedPayloadFrameSourceChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.websockets.core.function.ChannelFunction; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +class WebSocket07ContinuationFrameSourceChannel extends FixedPayloadFrameSourceChannel { + WebSocket07ContinuationFrameSourceChannel(WebSocket07Channel wsChannel, long payloadSize, int rsv, boolean finalFragment, Pooled pooled, long frameLength, final ChannelFunction... function) { + super(wsChannel, WebSocketFrameType.CONTINUATION, payloadSize, rsv, finalFragment, pooled, frameLength, function); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07FrameSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07FrameSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07FrameSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,210 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.server.protocol.framed.SendFrameHeader; +import io.undertow.websockets.core.StreamSinkFrameChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.websockets.core.WebSocketMessages; +import org.xnio.Buffers; +import org.xnio.Pooled; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Random; + +/** + * {@link StreamSinkFrameChannel} implementation for writing WebSocket Frames on {@link io.undertow.websockets.core.WebSocketVersion#V08} connections + * + * @author Norman Maurer + */ +public abstract class WebSocket07FrameSinkChannel extends StreamSinkFrameChannel { + + private final int maskingKey; + private final Masker masker; + private final long payloadSize; + private boolean dataWritten = false; + long toWrite; + + protected WebSocket07FrameSinkChannel(WebSocket07Channel wsChannel, WebSocketFrameType type, + long payloadSize) { + super(wsChannel, type); + this.payloadSize = payloadSize; + this.toWrite = payloadSize; + if(wsChannel.isClient()) { + maskingKey = new Random().nextInt(); + masker = new Masker(maskingKey); + } else { + masker = null; + maskingKey = 0; + } + } + + @Override + protected void handleFlushComplete(boolean finalFrame) { + dataWritten = true; + } + + + /** + * If a stream sink channel is closed while in the middle of sending fragmented data we need to close the connection. + * @throws IOException + */ + protected void channelForciblyClosed() throws IOException { + getChannel().sendClose(); + } + + private byte opCode() { + if(dataWritten) { + return WebSocket07Channel.OPCODE_CONT; + } + switch (getType()) { + case CONTINUATION: + return WebSocket07Channel.OPCODE_CONT; + case TEXT: + return WebSocket07Channel.OPCODE_TEXT; + case BINARY: + return WebSocket07Channel.OPCODE_BINARY; + case CLOSE: + return WebSocket07Channel.OPCODE_CLOSE; + case PING: + return WebSocket07Channel.OPCODE_PING; + case PONG: + return WebSocket07Channel.OPCODE_PONG; + default: + throw WebSocketMessages.MESSAGES.unsupportedFrameType(getType()); + } + } + + @Override + protected SendFrameHeader createFrameHeader() { + if(payloadSize >= 0 && dataWritten) { + //for fixed length we don't need more than one header + return null; + } + Pooled start = getChannel().getBufferPool().allocate(); + byte b0 = 0; + //if writes are shutdown this is the final fragment + if (isFinalFrameQueued() || payloadSize >= 0) { + b0 |= 1 << 7; + } + b0 |= (getRsv() & 7) << 4; + b0 |= opCode() & 0xf; + + final ByteBuffer header = start.getResource(); + //int maskLength = 0; // handle masking for clients but we are currently only + // support servers this is not a priority by now + byte maskKey = 0; + if(masker != null) { + maskKey |= 1 << 7; + } + long payloadSize; + if(this.payloadSize >= 0) { + payloadSize = this.payloadSize; + } else { + payloadSize = getBuffer().remaining(); + } + if (payloadSize <= 125) { + header.put(b0); + header.put((byte)((payloadSize | maskKey) & 0xFF)); + } else if (payloadSize <= 0xFFFF) { + header.put(b0); + header.put((byte) ((126 | maskKey) & 0xFF)); + header.put((byte) (payloadSize >>> 8 & 0xFF)); + header.put((byte) (payloadSize & 0xFF)); + } else { + header.put(b0); + header.put((byte) ((127 | maskKey) & 0xFF)); + header.putLong(payloadSize); + } + if(masker != null) { + header.put((byte)((maskingKey >> 24) & 0xFF)); + header.put((byte)((maskingKey >> 16) & 0xFF)); + header.put((byte)((maskingKey >> 8) & 0xFF)); + header.put((byte)((maskingKey & 0xFF))); + } + header.flip(); + return new SendFrameHeader(0, start); + } + + @Override + public long write(final ByteBuffer[] srcs) throws IOException { + return write(srcs, 0, srcs.length); + } + + @Override + public long write(final ByteBuffer[] srcs, final int offset, final int length) throws IOException { + if(toWrite >= 0 && Buffers.remaining(srcs) > toWrite) { + throw WebSocketMessages.MESSAGES.messageOverflow(); + } + if(masker == null) { + return super.write(srcs, offset, length); + } else { + final Pooled buffer = getChannel().getBufferPool().allocate(); + try { + ByteBuffer[] copy = new ByteBuffer[length]; + for(int i = 0; i < length; ++i) { + copy[i] = srcs[offset + i].duplicate(); + } + Buffers.copy(buffer.getResource(), copy, 0, length); + buffer.getResource().flip(); + masker.beforeWrite(buffer.getResource(), 0, buffer.getResource().remaining()); + long written = super.write(buffer.getResource()); + long toAllocate = written; + for(int i = offset; i < length; ++i) { + ByteBuffer thisBuf = srcs[i]; + if(toAllocate < thisBuf.remaining()) { + thisBuf.position((int) (thisBuf.position() + toAllocate)); + break; + } else { + toAllocate -= thisBuf.remaining(); + thisBuf.position(thisBuf.limit()); + } + } + toWrite -= written; + return written; + } finally { + buffer.free(); + } + } + } + + @Override + public int write(final ByteBuffer src) throws IOException { + if(toWrite >= 0 && src.remaining() > toWrite) { + throw WebSocketMessages.MESSAGES.messageOverflow(); + } + if(masker == null) { + return super.write(src); + } else { + final Pooled buffer = getChannel().getBufferPool().allocate(); + try { + ByteBuffer copy = src.duplicate(); + Buffers.copy(buffer.getResource(), copy); + buffer.getResource().flip(); + masker.beforeWrite(buffer.getResource(), 0, buffer.getResource().remaining()); + int written = super.write(buffer.getResource()); + src.position(src.position() + written); + toWrite -= written; + return written; + } finally { + buffer.free(); + } + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PingFrameSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PingFrameSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PingFrameSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,33 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.WebSocketFrameType; +import io.undertow.websockets.core.WebSocketMessages; + +/** + * @author Norman Maurer + */ +class WebSocket07PingFrameSinkChannel extends WebSocket07FrameSinkChannel { + WebSocket07PingFrameSinkChannel(WebSocket07Channel wsChannel, long payloadSize) { + super(wsChannel, WebSocketFrameType.PING, payloadSize); + if (payloadSize > 125) { + throw WebSocketMessages.MESSAGES.invalidPayloadLengthForPing(payloadSize); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PingFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PingFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PingFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.FixedPayloadFrameSourceChannel; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +class WebSocket07PingFrameSourceChannel extends FixedPayloadFrameSourceChannel { + WebSocket07PingFrameSourceChannel(WebSocketChannel wsChannel, long payloadSize, int rsv, Masker masker, Pooled pooled, long frameLength) { + // can not be fragmented + super(wsChannel, WebSocketFrameType.PING, payloadSize, rsv, true, pooled, frameLength, masker); + } + + WebSocket07PingFrameSourceChannel(WebSocketChannel wsChannel, long payloadSize, int rsv, Pooled pooled, long frameLength) { + // can not be fragmented + super(wsChannel, WebSocketFrameType.PING, payloadSize, rsv, true, pooled, frameLength); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PongFrameSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PongFrameSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PongFrameSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,29 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.WebSocketFrameType; + +/** + * @author Norman Maurer + */ +class WebSocket07PongFrameSinkChannel extends WebSocket07FrameSinkChannel { + WebSocket07PongFrameSinkChannel(WebSocket07Channel wsChannel, long payloadSize) { + super(wsChannel, WebSocketFrameType.PONG, payloadSize); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PongFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PongFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07PongFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,40 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.FixedPayloadFrameSourceChannel; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +class WebSocket07PongFrameSourceChannel extends FixedPayloadFrameSourceChannel { + WebSocket07PongFrameSourceChannel(WebSocketChannel wsChannel, long payloadSize, int rsv, final Masker masker, Pooled pooled, long frameLength) { + // can not be fragmented + super(wsChannel, WebSocketFrameType.PONG, payloadSize, rsv, true, pooled, frameLength, masker); + } + + WebSocket07PongFrameSourceChannel(WebSocketChannel wsChannel, long payloadSize, int rsv, Pooled pooled, long frameLength) { + // can not be fragmented + super(wsChannel, WebSocketFrameType.PONG, payloadSize, rsv, true, pooled, frameLength); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07TextFrameSinkChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07TextFrameSinkChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07TextFrameSinkChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,43 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.WebSocketFrameType; + +/** + * WebSocket08FrameSinkChannel that is used to write WebSocketFrameType#TEXT frames. + * + * + * @author Norman Maurer + */ +class WebSocket07TextFrameSinkChannel extends WebSocket07FrameSinkChannel { + + WebSocket07TextFrameSinkChannel(WebSocket07Channel wsChannel, long payloadSize) { + super(wsChannel, WebSocketFrameType.TEXT, payloadSize); + } + + @Override + public boolean isFragmentationSupported() { + return true; + } + + @Override + public boolean areExtensionsSupported() { + return true; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07TextFrameSourceChannel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07TextFrameSourceChannel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version07/WebSocket07TextFrameSourceChannel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,38 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version07; + +import io.undertow.websockets.core.FixedPayloadFrameSourceChannel; +import io.undertow.websockets.core.WebSocketFrameType; +import org.xnio.Pooled; + +import java.nio.ByteBuffer; + +/** + * @author Norman Maurer + */ +class WebSocket07TextFrameSourceChannel extends FixedPayloadFrameSourceChannel { + + WebSocket07TextFrameSourceChannel(WebSocket07Channel wsChannel, long payloadSize, int rsv, boolean finalFragment, Masker masker, UTF8Checker checker, Pooled pooled, long frameLength) { + super(wsChannel, WebSocketFrameType.TEXT, payloadSize, rsv, finalFragment, pooled, frameLength, masker, checker); + } + + WebSocket07TextFrameSourceChannel(WebSocket07Channel wsChannel, long payloadSize, int rsv, boolean finalFragment, UTF8Checker checker, Pooled pooled, long frameLength) { + super(wsChannel, WebSocketFrameType.TEXT, payloadSize, rsv, finalFragment, pooled, frameLength, checker); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version08/Hybi08Handshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version08/Hybi08Handshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version08/Hybi08Handshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,53 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version08; + +import io.undertow.util.Headers; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.protocol.version07.Hybi07Handshake; +import io.undertow.websockets.spi.WebSocketHttpExchange; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.Set; + +/** + * The handshaking protocol implementation for Hybi-07, which is identical to Hybi-08, and thus is just a thin + * subclass of {@link Hybi07Handshake} that sets a different version number. + * + * @author Mike Brock + */ +public class Hybi08Handshake extends Hybi07Handshake { + public Hybi08Handshake() { + super(WebSocketVersion.V08, Collections.emptySet(), false); + } + + public Hybi08Handshake(Set subprotocols, boolean allowExtensions) { + super(WebSocketVersion.V08, subprotocols, allowExtensions); + } + + @Override + public WebSocketChannel createChannel(final WebSocketHttpExchange exchange, final StreamConnection channel, final Pool pool) { + return new WebSocket08Channel(channel, pool, getWebSocketLocation(exchange), exchange.getResponseHeader(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING), false, allowExtensions, exchange.getPeerConnections()); + + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version08/WebSocket08Channel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version08/WebSocket08Channel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version08/WebSocket08Channel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,45 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version08; + +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.protocol.version07.WebSocket07Channel; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.util.Set; + + +/** + * {@link io.undertow.websockets.core.WebSocketChannel} which is used for {@link WebSocketVersion#V08} + * + * @author Norman Maurer + */ +public class WebSocket08Channel extends WebSocket07Channel { + public WebSocket08Channel(StreamConnection channel, Pool bufferPool, String wsUrl, String subProtocols, final boolean client, boolean allowExtensions, Set openConnections) { + super(channel, bufferPool, wsUrl, subProtocols, client, allowExtensions, openConnections); + } + + @Override + public WebSocketVersion getVersion() { + return WebSocketVersion.V08; + } +} + Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version13/Hybi13Handshake.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version13/Hybi13Handshake.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version13/Hybi13Handshake.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,75 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version13; + +import io.undertow.util.Headers; +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.protocol.version07.Hybi07Handshake; +import io.undertow.websockets.spi.WebSocketHttpExchange; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.security.NoSuchAlgorithmException; +import java.util.Collections; +import java.util.Set; + +/** + * The handshaking protocol implementation for Hybi-13. + * + * @author Mike Brock + * @author Stuart Douglas + */ +public class Hybi13Handshake extends Hybi07Handshake { + public Hybi13Handshake() { + super(WebSocketVersion.V13, Collections.emptySet(), false); + } + + public Hybi13Handshake(Set subprotocols, boolean allowExtensions) { + super(WebSocketVersion.V13, subprotocols, allowExtensions); + } + + @Override + protected void handshakeInternal(final WebSocketHttpExchange exchange) { + String origin = exchange.getRequestHeader(Headers.ORIGIN_STRING); + if (origin != null) { + exchange.setResponseHeader(Headers.ORIGIN_STRING, origin); + } + selectSubprotocol(exchange); + exchange.setResponseHeader(Headers.SEC_WEB_SOCKET_LOCATION_STRING, getWebSocketLocation(exchange)); + + final String key = exchange.getRequestHeader(Headers.SEC_WEB_SOCKET_KEY_STRING); + try { + final String solution = solve(key); + exchange.setResponseHeader(Headers.SEC_WEB_SOCKET_ACCEPT_STRING, solution); + performUpgrade(exchange); + } catch (NoSuchAlgorithmException e) { + IoUtils.safeClose(exchange); + exchange.endExchange(); + return; + } + } + + @Override + public WebSocketChannel createChannel(WebSocketHttpExchange exchange, final StreamConnection channel, final Pool pool) { + return new WebSocket13Channel(channel, pool, getWebSocketLocation(exchange), exchange.getResponseHeader(Headers.SEC_WEB_SOCKET_PROTOCOL_STRING), false, allowExtensions, exchange.getPeerConnections()); + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version13/WebSocket13Channel.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version13/WebSocket13Channel.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/core/protocol/version13/WebSocket13Channel.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,44 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.core.protocol.version13; + +import io.undertow.websockets.core.WebSocketChannel; +import io.undertow.websockets.core.WebSocketVersion; +import io.undertow.websockets.core.protocol.version07.WebSocket07Channel; +import org.xnio.Pool; +import org.xnio.StreamConnection; + +import java.nio.ByteBuffer; +import java.util.Set; + +/** + * + * A WebSocketChannel that handles version 13 + * + * @author Norman Maurer + */ +public class WebSocket13Channel extends WebSocket07Channel { + public WebSocket13Channel(StreamConnection channel, Pool bufferPool, String wsUrl, String subProtocols, final boolean client, boolean allowExtensions, Set openConnections) { + super(channel, bufferPool, wsUrl, subProtocols, client, allowExtensions, openConnections); + } + + @Override + public WebSocketVersion getVersion() { + return WebSocketVersion.V13; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/spi/AsyncWebSocketHttpServerExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/spi/AsyncWebSocketHttpServerExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/spi/AsyncWebSocketHttpServerExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,298 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.spi; + +import io.undertow.UndertowLogger; +import io.undertow.io.IoCallback; +import io.undertow.io.Sender; +import io.undertow.security.api.SecurityContext; +import io.undertow.security.idm.Account; +import io.undertow.server.HttpServerExchange; +import io.undertow.server.HttpUpgradeListener; +import io.undertow.server.session.SessionConfig; +import io.undertow.server.session.SessionManager; +import io.undertow.util.AttachmentKey; +import io.undertow.util.HeaderMap; +import io.undertow.util.HttpString; +import io.undertow.websockets.core.WebSocketChannel; +import org.xnio.ChannelListener; +import org.xnio.FinishedIoFuture; +import org.xnio.FutureResult; +import org.xnio.IoFuture; +import org.xnio.IoUtils; +import org.xnio.Pool; +import org.xnio.Pooled; +import org.xnio.channels.StreamSourceChannel; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; + +/** + * @author Stuart Douglas + */ +public class AsyncWebSocketHttpServerExchange implements WebSocketHttpExchange { + + private final HttpServerExchange exchange; + private Sender sender; + private final Set peerConnections; + + public AsyncWebSocketHttpServerExchange(final HttpServerExchange exchange, Set peerConnections) { + this.exchange = exchange; + this.peerConnections = peerConnections; + } + + + @Override + public void putAttachment(final AttachmentKey key, final T value) { + exchange.putAttachment(key, value); + } + + @Override + public T getAttachment(final AttachmentKey key) { + return exchange.getAttachment(key); + } + + @Override + public String getRequestHeader(final String headerName) { + return exchange.getRequestHeaders().getFirst(HttpString.tryFromString(headerName)); + } + + @Override + public Map> getRequestHeaders() { + Map> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + for (final HttpString header : exchange.getRequestHeaders().getHeaderNames()) { + headers.put(header.toString(), new ArrayList<>(exchange.getRequestHeaders().get(header))); + } + return Collections.unmodifiableMap(headers); + } + + @Override + public String getResponseHeader(final String headerName) { + return exchange.getResponseHeaders().getFirst(HttpString.tryFromString(headerName)); + } + + @Override + public Map> getResponseHeaders() { + Map> headers = new HashMap<>(); + for (final HttpString header : exchange.getResponseHeaders().getHeaderNames()) { + headers.put(header.toString(), new ArrayList<>(exchange.getResponseHeaders().get(header))); + } + return Collections.unmodifiableMap(headers); + } + + @Override + public void setResponseHeaders(final Map> headers) { + HeaderMap map = exchange.getRequestHeaders(); + map.clear(); + for (Map.Entry> header : headers.entrySet()) { + map.addAll(HttpString.tryFromString(header.getKey()), header.getValue()); + } + } + + @Override + public void setResponseHeader(final String headerName, final String headerValue) { + exchange.getResponseHeaders().put(HttpString.tryFromString(headerName), headerValue); + } + + @Override + public void upgradeChannel(final HttpUpgradeListener upgradeCallback) { + exchange.upgradeChannel(upgradeCallback); + } + + @Override + public IoFuture sendData(final ByteBuffer data) { + if (sender == null) { + this.sender = exchange.getResponseSender(); + } + final FutureResult future = new FutureResult<>(); + sender.send(data, new IoCallback() { + @Override + public void onComplete(final HttpServerExchange exchange, final Sender sender) { + future.setResult(null); + } + + @Override + public void onException(final HttpServerExchange exchange, final Sender sender, final IOException exception) { + UndertowLogger.REQUEST_IO_LOGGER.ioException(exception); + future.setException(exception); + + } + }); + return future.getIoFuture(); + } + + @Override + public IoFuture readRequestData() { + final ByteArrayOutputStream data = new ByteArrayOutputStream(); + final Pooled pooled = exchange.getConnection().getBufferPool().allocate(); + final ByteBuffer buffer = pooled.getResource(); + final StreamSourceChannel channel = exchange.getRequestChannel(); + int res; + for (; ; ) { + try { + res = channel.read(buffer); + if (res == -1) { + return new FinishedIoFuture<>(data.toByteArray()); + } else if (res == 0) { + //callback + final FutureResult future = new FutureResult<>(); + channel.getReadSetter().set(new ChannelListener() { + @Override + public void handleEvent(final StreamSourceChannel channel) { + int res; + try { + res = channel.read(buffer); + if (res == -1) { + future.setResult(data.toByteArray()); + channel.suspendReads(); + return; + } else if (res == 0) { + return; + } else { + buffer.flip(); + while (buffer.hasRemaining()) { + data.write(buffer.get()); + } + buffer.clear(); + } + + } catch (IOException e) { + future.setException(e); + } + } + }); + channel.resumeReads(); + return future.getIoFuture(); + } else { + buffer.flip(); + while (buffer.hasRemaining()) { + data.write(buffer.get()); + } + buffer.clear(); + } + + } catch (IOException e) { + final FutureResult future = new FutureResult<>(); + future.setException(e); + return future.getIoFuture(); + } + } + + + } + + @Override + public void endExchange() { + exchange.endExchange(); + } + + @Override + public void close() { + try { + exchange.endExchange(); + } finally { + IoUtils.safeClose(exchange.getConnection()); + } + } + + @Override + public String getRequestScheme() { + return exchange.getRequestScheme(); + } + + @Override + public String getRequestURI() { + String q = exchange.getQueryString(); + if (q == null || q.isEmpty()) { + return exchange.getRequestURI(); + } else { + return exchange.getRequestURI() + "?" + q; + } + } + + @Override + public Pool getBufferPool() { + return exchange.getConnection().getBufferPool(); + } + + @Override + public String getQueryString() { + return exchange.getQueryString(); + } + + @Override + public Object getSession() { + SessionManager sm = exchange.getAttachment(SessionManager.ATTACHMENT_KEY); + SessionConfig sessionCookieConfig = exchange.getAttachment(SessionConfig.ATTACHMENT_KEY); + if(sm != null && sessionCookieConfig != null) { + return sm.getSession(exchange, sessionCookieConfig); + } + return null; + } + + @Override + public Map> getRequestParameters() { + Map> params = new HashMap<>(); + for (Map.Entry> param : exchange.getQueryParameters().entrySet()) { + params.put(param.getKey(), new ArrayList<>(param.getValue())); + } + return params; + } + + @Override + public Principal getUserPrincipal() { + SecurityContext sc = exchange.getSecurityContext(); + if(sc == null) { + return null; + } + Account authenticatedAccount = sc.getAuthenticatedAccount(); + if(authenticatedAccount == null) { + return null; + } + return authenticatedAccount.getPrincipal(); + } + + @Override + public boolean isUserInRole(String role) { + SecurityContext sc = exchange.getSecurityContext(); + if(sc == null) { + return false; + } + Account authenticatedAccount = sc.getAuthenticatedAccount(); + if(authenticatedAccount == null) { + return false; + } + return authenticatedAccount.getRoles().contains(role); + } + + @Override + public Set getPeerConnections() { + return peerConnections; + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/spi/BlockingWebSocketHttpServerExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/spi/BlockingWebSocketHttpServerExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/spi/BlockingWebSocketHttpServerExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,78 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.spi; + +import io.undertow.server.HttpServerExchange; +import io.undertow.websockets.core.WebSocketChannel; +import org.xnio.FinishedIoFuture; +import org.xnio.FutureResult; +import org.xnio.IoFuture; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.util.Set; + +/** + * @author Stuart Douglas + */ +public class BlockingWebSocketHttpServerExchange extends AsyncWebSocketHttpServerExchange { + + private final OutputStream out; + private final InputStream in; + + public BlockingWebSocketHttpServerExchange(final HttpServerExchange exchange, Set peerConnections) { + super(exchange, peerConnections); + out = exchange.getOutputStream(); + in = exchange.getInputStream(); + } + + @Override + public IoFuture sendData(final ByteBuffer data) { + try { + while (data.hasRemaining()) { + out.write(data.get()); + } + return new FinishedIoFuture<>(null); + } catch (IOException e) { + final FutureResult ioFuture = new FutureResult<>(); + ioFuture.setException(e); + return ioFuture.getIoFuture(); + } + } + + @Override + public IoFuture readRequestData() { + final ByteArrayOutputStream data = new ByteArrayOutputStream(); + try { + byte[] buf = new byte[1024]; + int r; + while ((r = in.read(buf)) != -1) { + data.write(buf, 0, r); + } + return new FinishedIoFuture<>(data.toByteArray()); + } catch (IOException e) { + final FutureResult ioFuture = new FutureResult<>(); + ioFuture.setException(e); + return ioFuture.getIoFuture(); + } + } +} Index: 3rdParty_sources/undertow/io/undertow/websockets/spi/WebSocketHttpExchange.java =================================================================== diff -u --- 3rdParty_sources/undertow/io/undertow/websockets/spi/WebSocketHttpExchange.java (revision 0) +++ 3rdParty_sources/undertow/io/undertow/websockets/spi/WebSocketHttpExchange.java (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -0,0 +1,163 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2014 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * 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 io.undertow.websockets.spi; + +import io.undertow.server.HttpUpgradeListener; +import io.undertow.util.AttachmentKey; +import io.undertow.websockets.core.WebSocketChannel; +import org.xnio.IoFuture; +import org.xnio.Pool; + +import java.io.Closeable; +import java.nio.ByteBuffer; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.Set; + + +/** + * An abstraction for a Http exchange. Undertow uses 3 different types of exchanges: + *

+ * - async + * - blocking + * - servlet + *

+ * This class provides a way to operate on the underling exchange while providing the + * correct semantics regardless of the underlying exchange type. + *

+ * The main use case for this is web sockets. Web sockets should be able to perform + * a handshake regardless of the nature of the underlying request, while still respecting + * servlet filters, security etc. + * + * @author Stuart Douglas + */ +public interface WebSocketHttpExchange extends Closeable { + + void putAttachment(final AttachmentKey key, T value); + + T getAttachment(final AttachmentKey key); + + /** + * gets the first request header with the specified name + * + * @param headerName The header name + * @return The header value, or null + */ + String getRequestHeader(final String headerName); + + /** + * @return An unmodifiable map of request headers + */ + Map> getRequestHeaders(); + + /** + * get a response header + * + * @param headerName The header name + * @return The header value, or null + */ + String getResponseHeader(final String headerName); + + /** + * @return An unmodifiable map of response headers + */ + Map> getResponseHeaders(); + + + /** + * Sets the response headers + */ + void setResponseHeaders(final Map> headers); + + /** + * Set a response header + * + * @param headerName The header name + * @param headerValue The header value + */ + void setResponseHeader(final String headerName, final String headerValue); + + /** + * Upgrade the underlying channel + * + * @param upgradeCallback + */ + void upgradeChannel(final HttpUpgradeListener upgradeCallback); + + /** + * Send some data + * + * @param data The data + */ + IoFuture sendData(final ByteBuffer data); + + /** + * Gets the body of the request. + */ + IoFuture readRequestData(); + + /** + * End the exchange normally. If this is a blocking exchange this may be a noop, and the exchange + * will actually end when the call stack returns + */ + void endExchange(); + + /** + * Forcibly close the exchange. + */ + void close(); + + /** + * Get the request scheme, usually http or https + * + * @return The request scheme + */ + String getRequestScheme(); + + /** + * @return The request URI, including the query string + */ + String getRequestURI(); + + /** + * @return The buffer pool + */ + Pool getBufferPool(); + + /** + * @return The query string + */ + String getQueryString(); + + /** + * Gets the session, if any + * + * @return The session object, or null + */ + Object getSession(); + + Map> getRequestParameters(); + + Principal getUserPrincipal(); + + boolean isUserInRole(String role); + + Set getPeerConnections(); +} Index: lams_build/build_base.xml =================================================================== diff -u -r3efaec62ed5bdcd05ef0f9c96fcc8316cc1fe592 -ra0b759c0e2c1f081e1d2d10e5e8e6565b4e74505 --- lams_build/build_base.xml (.../build_base.xml) (revision 3efaec62ed5bdcd05ef0f9c96fcc8316cc1fe592) +++ lams_build/build_base.xml (.../build_base.xml) (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -26,6 +26,8 @@ that are also present in lams_build/lib. --> + + @@ -46,6 +48,7 @@ + Index: lams_build/conf/j2ee/jboss-deployment-structure.xml =================================================================== diff -u -r63e907748a8fe4a8fe50c705e9e01074606d451f -ra0b759c0e2c1f081e1d2d10e5e8e6565b4e74505 --- lams_build/conf/j2ee/jboss-deployment-structure.xml (.../jboss-deployment-structure.xml) (revision 63e907748a8fe4a8fe50c705e9e01074606d451f) +++ lams_build/conf/j2ee/jboss-deployment-structure.xml (.../jboss-deployment-structure.xml) (revision a0b759c0e2c1f081e1d2d10e5e8e6565b4e74505) @@ -48,7 +48,8 @@ - + +