diff --git a/pom.xml b/pom.xml index 42cb821c00..8cb88b486d 100644 --- a/pom.xml +++ b/pom.xml @@ -112,25 +112,31 @@ org.eclipse.jetty jetty-server - 7.1.4.v20100610 + 8.1.0.RC1 test org.eclipse.jetty jetty-servlet - 7.1.4.v20100610 + 8.1.0.RC1 + test + + + org.eclipse.jetty + jetty-websocket + 8.1.0.RC1 test org.eclipse.jetty jetty-servlets - 7.1.4.v20100610 + 8.1.0.RC1 test org.eclipse.jetty jetty-security - 7.1.4.v20100610 + 8.1.0.RC1 test @@ -443,7 +449,7 @@ org.glassfish.grizzly - grizzly-http + grizzly-websockets 2.2-SNAPSHOT true diff --git a/src/main/java/com/ning/http/client/AsyncHandler.java b/src/main/java/com/ning/http/client/AsyncHandler.java index 4498056f82..ec9bc77022 100644 --- a/src/main/java/com/ning/http/client/AsyncHandler.java +++ b/src/main/java/com/ning/http/client/AsyncHandler.java @@ -53,7 +53,11 @@ public static enum STATE { /** * Continue the processing */ - CONTINUE + CONTINUE, + /** + * Upgrade the protocol. When specified, the AsyncHttpProvider will try to invoke the {@link UpgradeHandler#onReady} + */ + UPGRADE } /** diff --git a/src/main/java/com/ning/http/client/RequestBuilderBase.java b/src/main/java/com/ning/http/client/RequestBuilderBase.java index 2a80fd409a..aef61c55ba 100644 --- a/src/main/java/com/ning/http/client/RequestBuilderBase.java +++ b/src/main/java/com/ning/http/client/RequestBuilderBase.java @@ -127,11 +127,13 @@ private String toUrl(boolean encode) { url = "http://localhost"; } - String uri; - try { - uri = URI.create(url).toURL().toString(); - } catch (Throwable e) { - throw new IllegalArgumentException("Illegal URL: " + url, e); + String uri = url; + if (!uri.startsWith("ws")) { + try { + uri = URI.create(url).toURL().toString(); + } catch (Throwable e) { + throw new IllegalArgumentException("Illegal URL: " + url, e); + } } if (queryParams != null && !queryParams.isEmpty()) { diff --git a/src/main/java/com/ning/http/client/UpgradeHandler.java b/src/main/java/com/ning/http/client/UpgradeHandler.java new file mode 100644 index 0000000000..0ca0cc53a2 --- /dev/null +++ b/src/main/java/com/ning/http/client/UpgradeHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client; + +/** + * Invoked when an {@link AsyncHandler.STATE#UPGRADE} is returned. Currently the library only support {@link WebSocket} + * as type. + * + * @param + */ +public interface UpgradeHandler { + + /** + * If the HTTP Upgrade succeed (response's status code equals 101), the {@link AsyncHttpProvider} will invoke that + * method + * + * @param t an Upgradable entity + */ + void onSuccess(T t); + + /** + * If the upgrade fail. + * @param t a {@link Throwable} + */ + void onFailure(Throwable t); + +} diff --git a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java index b5078b8d19..5441655412 100644 --- a/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java +++ b/src/main/java/com/ning/http/client/providers/grizzly/GrizzlyAsyncHttpProvider.java @@ -34,9 +34,16 @@ import com.ning.http.client.Request; import com.ning.http.client.RequestBuilder; import com.ning.http.client.Response; +import com.ning.http.client.UpgradeHandler; import com.ning.http.client.filter.FilterContext; import com.ning.http.client.filter.ResponseFilter; import com.ning.http.client.listener.TransferCompletionHandler; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketByteListener; +import com.ning.http.client.websocket.WebSocketListener; +import com.ning.http.client.websocket.WebSocketPingListener; +import com.ning.http.client.websocket.WebSocketTextListener; +import com.ning.http.client.websocket.WebSocketUpgradeHandler; import com.ning.http.multipart.MultipartRequestEntity; import com.ning.http.util.AsyncHttpProviderUtils; import com.ning.http.util.AuthenticatorUtils; @@ -46,6 +53,8 @@ import org.glassfish.grizzly.Buffer; import org.glassfish.grizzly.CompletionHandler; import org.glassfish.grizzly.Connection; +import org.glassfish.grizzly.EmptyCompletionHandler; +import org.glassfish.grizzly.FileTransfer; import org.glassfish.grizzly.Grizzly; import org.glassfish.grizzly.WriteResult; import org.glassfish.grizzly.attributes.Attribute; @@ -85,6 +94,13 @@ import org.glassfish.grizzly.utils.BufferOutputStream; import org.glassfish.grizzly.utils.DelayedExecutor; import org.glassfish.grizzly.utils.IdleTimeoutFilter; +import org.glassfish.grizzly.websockets.DataFrame; +import org.glassfish.grizzly.websockets.DefaultWebSocket; +import org.glassfish.grizzly.websockets.HandShake; +import org.glassfish.grizzly.websockets.ProtocolHandler; +import org.glassfish.grizzly.websockets.Version; +import org.glassfish.grizzly.websockets.WebSocketEngine; +import org.glassfish.grizzly.websockets.WebSocketFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -97,6 +113,7 @@ import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.net.URI; +import java.net.URISyntaxException; import java.net.URLEncoder; import java.security.NoSuchAlgorithmException; import java.util.Collection; @@ -123,7 +140,10 @@ public class GrizzlyAsyncHttpProvider implements AsyncHttpProvider { private final static Logger LOGGER = LoggerFactory.getLogger(GrizzlyAsyncHttpProvider.class); - + private static final boolean SEND_FILE_SUPPORT; + static { + SEND_FILE_SUPPORT = configSendFileSupport(); + } private final Attribute REQUEST_STATE_ATTR = Grizzly.DEFAULT_ATTRIBUTE_BUILDER.createAttribute(HttpTransactionContext.class.getName()); @@ -138,6 +158,7 @@ public class GrizzlyAsyncHttpProvider implements AsyncHttpProvider { + // ------------------------------------------------------------ Constructors @@ -372,6 +393,7 @@ public void onTimeout(Connection connection) { } else { doDefaultTransportConfig(); } + fcb.add(new WebSocketFilter()); clientTransport.setProcessor(fcb.build()); @@ -405,6 +427,29 @@ void touchConnection(final Connection c, final Request request) { // --------------------------------------------------------- Private Methods + + + private static boolean configSendFileSupport() { + + return !((System.getProperty("os.name").equalsIgnoreCase("linux") + && !linuxSendFileSupported()) + || System.getProperty("os.name").equalsIgnoreCase("HP-UX")); + } + + + private static boolean linuxSendFileSupported() { + final String version = System.getProperty("java.version"); + if (version.startsWith("1.6")) { + int idx = version.indexOf('_'); + if (idx == -1) { + return false; + } + final int patchRev = Integer.parseInt(version.substring(idx + 1)); + return (patchRev >= 18); + } else { + return version.startsWith("1.7") || version.startsWith("1.8"); + } + } private void doDefaultTransportConfig() { final ExecutorService service = clientConfig.executorService(); @@ -509,7 +554,7 @@ boolean sendRequest(final FilterChainContext ctx, } - private static boolean requestHasEntityBody(Request request) { + private static boolean requestHasEntityBody(final Request request) { final String method = request.getMethod(); return (Method.POST.matchesMethod(method) @@ -560,6 +605,12 @@ final class HttpTransactionContext { String lastRedirectURI; AtomicLong totalBodyWritten = new AtomicLong(); AsyncHandler.STATE currentState; + + String wsRequestURI; + boolean isWSRequest; + HandShake handshake; + ProtocolHandler protocolHandler; + WebSocket webSocket; // -------------------------------------------------------- Constructors @@ -715,6 +766,8 @@ public NextAction handleWrite(final FilterChainContext ctx) if (!sendAsGrizzlyRequest((Request) message, ctx)) { return ctx.getSuspendAction(); } + } else if (message instanceof Buffer) { + return ctx.getInvokeAction(); } return ctx.getStopAction(); @@ -735,20 +788,39 @@ public NextAction handleEvent(final FilterChainContext ctx, } +// @Override +// public NextAction handleRead(FilterChainContext ctx) throws IOException { +// Object message = ctx.getMessage(); +// if (HttpPacket.isHttp(message)) { +// final HttpPacket packet = (HttpPacket) message; +// HttpResponsePacket responsePacket; +// if (HttpContent.isContent(packet)) { +// responsePacket = (HttpResponsePacket) ((HttpContent) packet).getHttpHeader(); +// } else { +// responsePacket = (HttpResponsePacket) packet; +// } +// if (HttpStatus.SWITCHING_PROTOCOLS_101.statusMatches(responsePacket.getStatus())) { +// return ctx.getStopAction(); +// } +// } +// return super.handleRead(ctx); +// } // ----------------------------------------------------- Private Methods - - private boolean sendAsGrizzlyRequest(final Request request, final FilterChainContext ctx) throws IOException { final HttpTransactionContext httpCtx = getHttpTransactionContext(ctx.getConnection()); + if (isUpgradeRequest(httpCtx.handler) && isWSRequest(httpCtx.requestUrl)) { + httpCtx.isWSRequest = true; + convertToUpgradeRequest(httpCtx); + } final URI uri = AsyncHttpProviderUtils.createUri(httpCtx.requestUrl); final HttpRequestPacket.Builder builder = HttpRequestPacket.builder(); - + boolean secure = "https".equals(uri.getScheme()); builder.method(request.getMethod()); builder.protocol(Protocol.HTTP_1_1); String host = request.getVirtualHost(); @@ -764,7 +836,7 @@ private boolean sendAsGrizzlyRequest(final Request request, final ProxyServer proxy = getProxyServer(request); final boolean useProxy = (proxy != null); if (useProxy) { - if ("https".equals(uri.getScheme())) { + if (secure) { builder.method(Method.CONNECT); builder.uri(AsyncHttpProviderUtils.getAuthority(uri)); } else { @@ -783,7 +855,21 @@ private boolean sendAsGrizzlyRequest(final Request request, } } - final HttpRequestPacket requestPacket = builder.build(); + HttpRequestPacket requestPacket; + if (httpCtx.isWSRequest) { + try { + final URI wsURI = new URI(httpCtx.wsRequestURI); + httpCtx.protocolHandler = Version.DRAFT17.createHandler(true); + httpCtx.handshake = httpCtx.protocolHandler.createHandShake(wsURI); + requestPacket = (HttpRequestPacket) + httpCtx.handshake.composeHeaders().getHttpHeader(); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid WS URI: " + httpCtx.wsRequestURI); + } + } else { + requestPacket = builder.build(); + } + requestPacket.setSecure(true); if (!useProxy) { addQueryString(request, requestPacket); } @@ -804,16 +890,42 @@ private boolean sendAsGrizzlyRequest(final Request request, } } final AsyncHandler h = httpCtx.handler; - if (TransferCompletionHandler.class.isAssignableFrom(h.getClass())) { - final FluentCaseInsensitiveStringsMap map = - new FluentCaseInsensitiveStringsMap(request.getHeaders()); - TransferCompletionHandler.class.cast(h).transferAdapter(new GrizzlyTransferAdapter(map)); + if (h != null) { + if (TransferCompletionHandler.class.isAssignableFrom(h.getClass())) { + final FluentCaseInsensitiveStringsMap map = + new FluentCaseInsensitiveStringsMap(request.getHeaders()); + TransferCompletionHandler.class.cast(h).transferAdapter(new GrizzlyTransferAdapter(map)); + } } return sendRequest(ctx, request, requestPacket); } + private boolean isUpgradeRequest(final AsyncHandler handler) { + return (handler instanceof UpgradeHandler); + } + + + private boolean isWSRequest(final String requestUri) { + return (requestUri.charAt(0) == 'w' && requestUri.charAt(1) == 's'); + } + + + private void convertToUpgradeRequest(final HttpTransactionContext ctx) { + final int colonIdx = ctx.requestUrl.indexOf(':'); + + if (colonIdx < 2 || colonIdx > 3) { + throw new IllegalArgumentException("Invalid websocket URL: " + ctx.requestUrl); + } + + final StringBuilder sb = new StringBuilder(ctx.requestUrl); + sb.replace(0, colonIdx, ((colonIdx == 2) ? "http" : "https")); + ctx.wsRequestURI = ctx.requestUrl; + ctx.requestUrl = sb.toString(); + } + + private ProxyServer getProxyServer(Request request) { ProxyServer proxyServer = request.getProxyServer(); @@ -990,8 +1102,10 @@ protected void onHttpContentParsed(HttpContent content, protected void onHttpHeadersEncoded(HttpHeader httpHeader, FilterChainContext ctx) { final HttpTransactionContext context = provider.getHttpTransactionContext(ctx.getConnection()); final AsyncHandler handler = context.handler; - if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) { - ((TransferCompletionHandler) handler).onHeaderWriteCompleted(); + if (handler != null) { + if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) { + ((TransferCompletionHandler) handler).onHeaderWriteCompleted(); + } } } @@ -999,13 +1113,15 @@ protected void onHttpHeadersEncoded(HttpHeader httpHeader, FilterChainContext ct protected void onHttpContentEncoded(HttpContent content, FilterChainContext ctx) { final HttpTransactionContext context = provider.getHttpTransactionContext(ctx.getConnection()); final AsyncHandler handler = context.handler; - if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) { - final int written = content.getContent().remaining(); - final long total = context.totalBodyWritten.addAndGet(written); - ((TransferCompletionHandler) handler).onContentWriteProgress( - written, - total, - content.getHttpHeader().getContentLength()); + if (handler != null) { + if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) { + final int written = content.getContent().remaining(); + final long total = context.totalBodyWritten.addAndGet(written); + ((TransferCompletionHandler) handler).onContentWriteProgress( + written, + total, + content.getHttpHeader().getContentLength()); + } } } @@ -1080,10 +1196,12 @@ protected void onInitialLineParsed(HttpHeader httpHeader, } + @Override protected void onHttpHeaderError(final HttpHeader httpHeader, - final FilterChainContext ctx, - final Throwable t) throws IOException { + final FilterChainContext ctx, + final Throwable t) throws IOException { + t.printStackTrace(); httpHeader.setSkipRemainder(true); final HttpTransactionContext context = @@ -1162,15 +1280,33 @@ protected void onHttpHeadersParsed(HttpHeader httpHeader, } if (context.currentState != AsyncHandler.STATE.ABORT) { + boolean upgrade = context.currentState == AsyncHandler.STATE.UPGRADE; + try { + context.currentState = handler.onHeadersReceived( + new GrizzlyResponseHeaders((HttpResponsePacket) httpHeader, + null, + provider)); + } catch (Exception e) { + httpHeader.setSkipRemainder(true); + context.abort(e); + return; + } + if (upgrade) { try { - context.currentState = handler.onHeadersReceived( - new GrizzlyResponseHeaders((HttpResponsePacket) httpHeader, - null, - provider)); + context.protocolHandler.setConnection(ctx.getConnection()); + DefaultWebSocket ws = new DefaultWebSocket(context.protocolHandler); + ws.onConnect(); + context.webSocket = new GrizzlyWebSocketAdapter(ws); + WebSocketEngine.getEngine().setWebSocketHolder(ctx.getConnection(), + context.protocolHandler, + ws); + ((WebSocketUpgradeHandler) context.handler).onSuccess(context.webSocket); + context.result(handler.onCompleted()); } catch (Exception e) { httpHeader.setSkipRemainder(true); context.abort(e); - } + } + } } } @@ -1263,7 +1399,6 @@ private static boolean isRedirect(final int status) { } - // ------------------------------------------------------- Inner Classes @@ -1391,7 +1526,6 @@ public boolean handleStatus(final HttpResponsePacket responsePacket, responsePacket, httpTransactionContext); } else { - //requestToSend = httpTransactionContext.request; httpTransactionContext.statusHandler = null; httpTransactionContext.invocationStatus = InvocationStatus.CONTINUE; try { @@ -1478,7 +1612,7 @@ private static Request newRequest(final URI uri, } - } // END AsyncHttpClientFilter + } // END AsyncHttpClientEventFilter private static final class ClientEncodingFilter implements EncodingFilter { @@ -1897,7 +2031,7 @@ public boolean doHandle(final FilterChainContext ctx, } // END PartsBodyHandler - private static final class FileBodyHandler implements BodyHandler { + private final class FileBodyHandler implements BodyHandler { // -------------------------------------------- Methods from BodyHandler @@ -1913,34 +2047,57 @@ public boolean doHandle(final FilterChainContext ctx, throws IOException { final File f = request.getFile(); - final FileInputStream fis = new FileInputStream(request.getFile()); - final MemoryManager mm = ctx.getMemoryManager(); - AtomicInteger written = new AtomicInteger(); - boolean last = false; requestPacket.setContentLengthLong(f.length()); - try { - for (byte[] buf = new byte[MAX_CHUNK_SIZE]; !last; ) { - Buffer b = null; - int read; - if ((read = fis.read(buf)) < 0) { - last = true; - b = Buffers.EMPTY_BUFFER; + final HttpTransactionContext context = getHttpTransactionContext(ctx.getConnection()); + if (!SEND_FILE_SUPPORT || requestPacket.isSecure()) { + final FileInputStream fis = new FileInputStream(request.getFile()); + final MemoryManager mm = ctx.getMemoryManager(); + AtomicInteger written = new AtomicInteger(); + boolean last = false; + try { + for (byte[] buf = new byte[MAX_CHUNK_SIZE]; !last; ) { + Buffer b = null; + int read; + if ((read = fis.read(buf)) < 0) { + last = true; + b = Buffers.EMPTY_BUFFER; + } + if (b != Buffers.EMPTY_BUFFER) { + written.addAndGet(read); + b = Buffers.wrap(mm, buf, 0, read); + } + + final HttpContent content = + requestPacket.httpContentBuilder().content(b). + last(last).build(); + ctx.write(content, ((!requestPacket.isCommitted()) ? ctx.getTransportContext().getCompletionHandler() : null)); } - if (b != Buffers.EMPTY_BUFFER) { - written.addAndGet(read); - b = Buffers.wrap(mm, buf, 0, read); + } finally { + try { + fis.close(); + } catch (IOException ignored) { } - - final HttpContent content = - requestPacket.httpContentBuilder().content(b). - last(last).build(); - ctx.write(content, ((!requestPacket.isCommitted()) ? ctx.getTransportContext().getCompletionHandler() : null)); - } - } finally { - try { - fis.close(); - } catch (IOException ignored) { } + } else { + // write the headers + ctx.write(requestPacket, ((!requestPacket.isCommitted()) ? ctx.getTransportContext().getCompletionHandler() : null)); + ctx.write(new FileTransfer(f), new EmptyCompletionHandler() { + + @Override + public void updated(WriteResult result) { + final AsyncHandler handler = context.handler; + if (handler != null) { + if (TransferCompletionHandler.class.isAssignableFrom(handler.getClass())) { + final long written = result.getWrittenSize(); + final long total = context.totalBodyWritten.addAndGet(written); + ((TransferCompletionHandler) handler).onContentWriteProgress( + written, + total, + requestPacket.getContentLength()); + } + } + } + }); } return true; @@ -2364,5 +2521,116 @@ public void getBytes(byte[] bytes) { // TODO implement } - } + } // END GrizzlyTransferAdapter + + + private static final class GrizzlyWebSocketAdapter implements WebSocket { + + private final org.glassfish.grizzly.websockets.WebSocket gWebSocket; + + // -------------------------------------------------------- Constructors + + + GrizzlyWebSocketAdapter(final org.glassfish.grizzly.websockets.WebSocket gWebSocket) { + this.gWebSocket = gWebSocket; + } + + + // ------------------------------------------ Methods from AHC WebSocket + + + @Override + public WebSocket sendMessage(byte[] message) { + gWebSocket.send(message); + return this; + } + + @Override + public WebSocket sendTextMessage(String message) { + gWebSocket.send(message); + return this; + } + + @Override + public WebSocket addMessageListener(WebSocketListener l) { + gWebSocket.add(new AHCWebSocketListenerAdapter(l, this)); + return this; + } + + @Override + public void close() { + gWebSocket.close(); + } + + } // END GrizzlyWebSocketAdapter + + + private static final class AHCWebSocketListenerAdapter implements org.glassfish.grizzly.websockets.WebSocketListener { + + private final WebSocketListener ahcListener; + private final WebSocket webSocket; + + // -------------------------------------------------------- Constructors + + + AHCWebSocketListenerAdapter(final WebSocketListener ahcListener, WebSocket webSocket) { + this.ahcListener = ahcListener; + this.webSocket = webSocket; + } + + + // ------------------------------ Methods from Grizzly WebSocketListener + + + @Override + public void onClose(org.glassfish.grizzly.websockets.WebSocket gWebSocket, DataFrame dataFrame) { + ahcListener.onClose(webSocket); + } + + @Override + public void onConnect(org.glassfish.grizzly.websockets.WebSocket webSocket) { + // no-op + } + + @Override + public void onMessage(org.glassfish.grizzly.websockets.WebSocket webSocket, String s) { + if (WebSocketTextListener.class.isAssignableFrom(ahcListener.getClass())) { + WebSocketTextListener.class.cast(ahcListener).onMessage(s); + } + } + + @Override + public void onMessage(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes) { + if (WebSocketByteListener.class.isAssignableFrom(ahcListener.getClass())) { + WebSocketByteListener.class.cast(ahcListener).onMessage(bytes); + } + } + + @Override + public void onPing(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes) { + if (WebSocketPingListener.class.isAssignableFrom(ahcListener.getClass())) { + WebSocketPingListener.class.cast(ahcListener).onPing(bytes); + } + } + + @Override + public void onPong(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes) { + // no-op + } + + @Override + public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, String s, boolean b) { + // no-op + } + + @Override + public void onFragment(org.glassfish.grizzly.websockets.WebSocket webSocket, byte[] bytes, boolean b) { + // no-op + } + + } // END AHCWebSocketListenerAdapter + } + + + diff --git a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java index c07046f67a..9726073dde 100644 --- a/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java +++ b/src/main/java/com/ning/http/client/providers/netty/NettyAsyncHttpProvider.java @@ -46,6 +46,10 @@ import com.ning.http.client.ntlm.NTLMEngine; import com.ning.http.client.ntlm.NTLMEngineException; import com.ning.http.client.providers.netty.spnego.SpnegoEngine; +import com.ning.http.client.providers.netty.netty4.WebSocket08FrameDecoder; +import com.ning.http.client.providers.netty.netty4.WebSocket08FrameEncoder; +import com.ning.http.client.providers.netty.netty4.WebSocketFrame; +import com.ning.http.client.websocket.WebSocketUpgradeHandler; import com.ning.http.multipart.MultipartBody; import com.ning.http.multipart.MultipartRequestEntity; import com.ning.http.util.AsyncHttpProviderUtils; @@ -86,7 +90,9 @@ import org.jboss.netty.handler.codec.http.HttpHeaders; import org.jboss.netty.handler.codec.http.HttpMethod; import org.jboss.netty.handler.codec.http.HttpRequest; +import org.jboss.netty.handler.codec.http.HttpRequestEncoder; import org.jboss.netty.handler.codec.http.HttpResponse; +import org.jboss.netty.handler.codec.http.HttpResponseDecoder; import org.jboss.netty.handler.codec.http.HttpVersion; import org.jboss.netty.handler.ssl.SslHandler; import org.jboss.netty.handler.stream.ChunkedFile; @@ -128,23 +134,19 @@ import static org.jboss.netty.channel.Channels.pipeline; public class NettyAsyncHttpProvider extends SimpleChannelUpstreamHandler implements AsyncHttpProvider { + private final static String WEBSOCKET_KEY = "Sec-WebSocket-Key"; private final static String HTTP_HANDLER = "httpHandler"; - final static String SSL_HANDLER = "sslHandler"; + protected final static String SSL_HANDLER = "sslHandler"; private final static String HTTPS = "https"; private final static String HTTP = "http"; - + private static final String WEBSOCKET = "ws"; private final static Logger log = LoggerFactory.getLogger(NettyAsyncHttpProvider.class); - private final ClientBootstrap plainBootstrap; - private final ClientBootstrap secureBootstrap; - + private final ClientBootstrap webSocketBootstrap; private final static int MAX_BUFFERED_BYTES = 8192; - private final AsyncHttpClientConfig config; - private final AtomicBoolean isClose = new AtomicBoolean(false); - private final ClientSocketChannelFactory socketChannelFactory; private final ChannelGroup openChannels = new @@ -158,25 +160,17 @@ public boolean remove(Object o) { return removed; } }; - - private final ConnectionsPool connectionsPool; - private Semaphore freeConnections = null; - private final NettyAsyncHttpProviderConfig asyncHttpProviderConfig; - private boolean executeConnectAsync = true; - public static final ThreadLocal IN_IO_THREAD = new ThreadLocalBoolean(); - private final boolean trackConnections; - private final boolean useRawUrl; - private final static NTLMEngine ntlmEngine = new NTLMEngine(); - private final static SpnegoEngine spnegoEngine = new SpnegoEngine(); + private final Protocol httpProtocol = new HttpProtocol(); + private final Protocol webSocketProtocol = new WebSocketProtocol(); public NettyAsyncHttpProvider(AsyncHttpClientConfig config) { @@ -203,6 +197,7 @@ public NettyAsyncHttpProvider(AsyncHttpClientConfig config) { } plainBootstrap = new ClientBootstrap(socketChannelFactory); secureBootstrap = new ClientBootstrap(socketChannelFactory); + webSocketBootstrap = new ClientBootstrap(socketChannelFactory); configureNetty(); this.config = config; @@ -271,6 +266,18 @@ public ChannelPipeline getPipeline() throws Exception { DefaultChannelFuture.setUseDeadLockChecker(true); } } + + webSocketBootstrap.setPipelineFactory(new ChannelPipelineFactory() { + + /* @Override */ + public ChannelPipeline getPipeline() throws Exception { + ChannelPipeline pipeline = pipeline(); + pipeline.addLast("ws-decoder", new HttpResponseDecoder()); + pipeline.addLast("ws-encoder", new HttpRequestEncoder()); + pipeline.addLast("httpProcessor", NettyAsyncHttpProvider.this); + return pipeline; + } + }); } void constructSSLPipeline(final NettyConnectListener cl) { @@ -492,9 +499,9 @@ public void operationComplete(ChannelFuture cf) { } } - + private static boolean isProxyServer(AsyncHttpClientConfig config, Request request) { - return request.getProxyServer() != null || config.getProxyServer() != null; + return request.getProxyServer() != null || config.getProxyServer() != null; } protected final static HttpRequest buildRequest(AsyncHttpClientConfig config, Request request, URI uri, @@ -525,16 +532,24 @@ private static HttpRequest construct(AsyncHttpClientConfig config, nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_0, m, AsyncHttpProviderUtils.getAuthority(uri)); } else { StringBuilder path = null; - if(isProxyServer(config, request)) - path = new StringBuilder(uri.toString()); + if (isProxyServer(config, request)) + path = new StringBuilder(uri.toString()); else { - path = new StringBuilder(uri.getRawPath()); - if (uri.getQuery() != null) { - path.append("?").append(uri.getRawQuery()); - } + path = new StringBuilder(uri.getRawPath()); + if (uri.getQuery() != null) { + path.append("?").append(uri.getRawQuery()); + } } nettyRequest = new DefaultHttpRequest(HttpVersion.HTTP_1_1, m, path.toString()); } + boolean webSocket = uri.getScheme().equalsIgnoreCase(WEBSOCKET); + if (webSocket) { + nettyRequest.addHeader(HttpHeaders.Names.UPGRADE, HttpHeaders.Values.WEBSOCKET); + nettyRequest.addHeader(HttpHeaders.Names.CONNECTION, HttpHeaders.Values.UPGRADE); + nettyRequest.addHeader("Sec-WebSocket-Origin", "http://" + uri.getHost()); + nettyRequest.addHeader(WEBSOCKET_KEY, WebSocketUtil.getKey()); + nettyRequest.addHeader("Sec-WebSocket-Version", "8"); + } if (host != null) { if (uri.getPort() == -1) { @@ -629,7 +644,7 @@ private static HttpRequest construct(AsyncHttpClientConfig config, } } - if (!request.getHeaders().containsKey(HttpHeaders.Names.CONNECTION)) { + if (!webSocket && !request.getHeaders().containsKey(HttpHeaders.Names.CONNECTION)) { nettyRequest.setHeader(HttpHeaders.Names.CONNECTION, "keep-alive"); } @@ -747,7 +762,7 @@ private static HttpRequest construct(AsyncHttpClientConfig config, /** * TODO: AHC-78: SSL + zero copy isn't supported by the MultiPart class and pretty complex to implements. */ - if (uri.toString().startsWith("https")) { + if (uri.toString().startsWith(HTTPS)) { ChannelBuffer b = ChannelBuffers.dynamicBuffer(lenght); mre.writeRequest(new ChannelBufferOutputStream(b)); nettyRequest.setContent(b); @@ -794,6 +809,7 @@ public void close() { socketChannelFactory.releaseExternalResources(); plainBootstrap.releaseExternalResources(); secureBootstrap.releaseExternalResources(); + webSocketBootstrap.releaseExternalResources(); } catch (Throwable t) { log.warn("Unexpected error on close", t); } @@ -828,6 +844,10 @@ private ListenableFuture doConnect(final Request request, final AsyncHand throw new IOException("Closed"); } + if (request.getUrl().startsWith(WEBSOCKET) && !validateWebSocketRequest(request, asyncHandler)) { + throw new IOException("WebSocket method must be a GET"); + } + ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer(); String requestUrl; if (useRawUrl) { @@ -918,7 +938,6 @@ private ListenableFuture doConnect(final Request request, final AsyncHand } } - NettyConnectListener c = new NettyConnectListener.Builder(config, request, asyncHandler, f, this, bufferedBytes).build(uri); boolean avoidProxy = ProxyUtils.avoidProxy(proxyServer, uri.getHost()); @@ -927,7 +946,7 @@ private ListenableFuture doConnect(final Request request, final AsyncHand } ChannelFuture channelFuture; - ClientBootstrap bootstrap = useSSl ? secureBootstrap : plainBootstrap; + ClientBootstrap bootstrap = request.getUrl().startsWith(WEBSOCKET) ? webSocketBootstrap : (useSSl ? secureBootstrap : plainBootstrap); bootstrap.setOption("connectTimeoutMillis", config.getConnectionTimeoutInMs()); // Do no enable this with win. @@ -1036,7 +1055,6 @@ private void finishChannel(final ChannelHandlerContext ctx) { public void messageReceived(final ChannelHandlerContext ctx, MessageEvent e) throws Exception { //call super to reset the read timeout super.messageReceived(ctx, e); - IN_IO_THREAD.set(Boolean.TRUE); if (ctx.getAttachment() == null) { log.debug("ChannelHandlerContext wasn't having any attachment"); @@ -1068,276 +1086,8 @@ public void messageReceived(final ChannelHandlerContext ctx, MessageEvent e) thr return; } - final NettyResponseFuture future = (NettyResponseFuture) ctx.getAttachment(); - future.touch(); - - // The connect timeout occured. - if (future.isCancelled() || future.isDone()) { - finishChannel(ctx); - return; - } - - HttpRequest nettyRequest = future.getNettyRequest(); - AsyncHandler handler = future.getAsyncHandler(); - Request request = future.getRequest(); - HttpResponse response = null; - try { - if (e.getMessage() instanceof HttpResponse) { - response = (HttpResponse) e.getMessage(); - - log.debug("\n\nRequest {}\n\nResponse {}\n", nettyRequest, response); - - // Required if there is some trailing headers. - future.setHttpResponse(response); - - int statusCode = response.getStatus().getCode(); - - String ka = response.getHeader(HttpHeaders.Names.CONNECTION); - future.setKeepAlive(ka == null || ka.toLowerCase().equals("keep-alive")); - - List wwwAuth = getAuthorizationToken(response.getHeaders(), HttpHeaders.Names.WWW_AUTHENTICATE); - Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); - - HttpResponseStatus status = new ResponseStatus(future.getURI(), response, this); - FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(handler).request(request).responseStatus(status).build(); - for (ResponseFilter asyncFilter : config.getResponseFilters()) { - try { - fc = asyncFilter.filter(fc); - if (fc == null) { - throw new NullPointerException("FilterContext is null"); - } - } catch (FilterException efe) { - abort(future, efe); - } - } - - // The request has changed - if (fc.replayRequest()) { - replayRequest(future, fc, response, ctx); - return; - } - - Realm newRealm = null; - ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer(); - final FluentCaseInsensitiveStringsMap headers = request.getHeaders(); - final RequestBuilder builder = new RequestBuilder(future.getRequest()); - - if (realm != null && !future.getURI().getPath().equalsIgnoreCase(realm.getUri())) { - builder.setUrl(future.getURI().toString()); - } - - if (statusCode == 401 - && wwwAuth.size() > 0 - && !future.getAndSetAuth(true)) { - - future.setState(NettyResponseFuture.STATE.NEW); - // NTLM - if (!wwwAuth.contains("Kerberos") && (wwwAuth.contains("NTLM") || (wwwAuth.contains("Negotiate")))) { - newRealm = ntlmChallenge(wwwAuth, request, proxyServer, headers, realm, future); - // SPNEGO KERBEROS - } else if (wwwAuth.contains("Negotiate")) { - newRealm = kerberosChallenge(wwwAuth, request, proxyServer, headers, realm, future); - if (newRealm == null) return; - } else { - Realm.RealmBuilder realmBuilder; - if (realm != null) { - realmBuilder = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme()) - ; - } else { - realmBuilder = new Realm.RealmBuilder(); - } - newRealm = realmBuilder - .setUri(URI.create(request.getUrl()).getPath()) - .setMethodName(request.getMethod()) - .setUsePreemptiveAuth(true) - .parseWWWAuthenticateHeader(wwwAuth.get(0)) - .build(); - } - - final Realm nr = newRealm; - - log.debug("Sending authentication to {}", request.getUrl()); - AsyncCallable ac = new AsyncCallable(future) { - public Object call() throws Exception { - drainChannel(ctx, future, future.getKeepAlive(), future.getURI()); - nextRequest(builder.setHeaders(headers).setRealm(nr).build(), future); - return null; - } - }; - - if (future.getKeepAlive() && response.isChunked()) { - // We must make sure there is no bytes left before executing the next request. - ctx.setAttachment(ac); - } else { - ac.call(); - } - return; - } - - if (statusCode == 100) { - future.getAndSetWriteHeaders(false); - future.getAndSetWriteBody(true); - writeRequest(ctx.getChannel(), config, future, nettyRequest); - return; - } - - List proxyAuth = getAuthorizationToken(response.getHeaders(), HttpHeaders.Names.PROXY_AUTHENTICATE); - if (statusCode == 407 - && proxyAuth.size() > 0 - && !future.getAndSetAuth(true)) { - - log.debug("Sending proxy authentication to {}", request.getUrl()); - - future.setState(NettyResponseFuture.STATE.NEW); - - if (!proxyAuth.contains("Kerberos") && (proxyAuth.get(0).contains("NTLM") || (proxyAuth.contains("Negotiate")))) { - newRealm = ntlmProxyChallenge(proxyAuth, request, proxyServer, headers, realm, future); - // SPNEGO KERBEROS - } else if (proxyAuth.contains("Negotiate")) { - newRealm = kerberosChallenge(proxyAuth, request, proxyServer, headers, realm, future); - if (newRealm == null) return; - } else { - newRealm = future.getRequest().getRealm(); - } - - Request req = builder.setHeaders(headers).setRealm(newRealm).build(); - future.setReuseChannel(true); - future.setConnectAllowed(true); - nextRequest(req, future); - return; - } - - if (future.getNettyRequest().getMethod().equals(HttpMethod.CONNECT) - && statusCode == 200) { - - log.debug("Connected to {}:{}", proxyServer.getHost(), proxyServer.getPort()); - - if (future.getKeepAlive()) { - future.attachChannel(ctx.getChannel(), true); - } - - try { - log.debug("Connecting to proxy {} for scheme {}", proxyServer, request.getUrl()); - upgradeProtocol(ctx.getChannel().getPipeline(), request.getUrl()); - } catch (Throwable ex) { - abort(future, ex); - } - Request req = builder.build(); - future.setReuseChannel(true); - future.setConnectAllowed(false); - nextRequest(req, future); - return; - } - - boolean redirectEnabled = request.isRedirectOverrideSet()? request.isRedirectEnabled() : config.isRedirectEnabled(); - if (redirectEnabled && (statusCode == 302 || statusCode == 301 || statusCode == 307)) { - - if (future.incrementAndGetCurrentRedirectCount() < config.getMaxRedirects()) { - // We must allow 401 handling again. - future.getAndSetAuth(false); - - String location = response.getHeader(HttpHeaders.Names.LOCATION); - URI uri = AsyncHttpProviderUtils.getRedirectUri(future.getURI(), location); - boolean stripQueryString = config.isRemoveQueryParamOnRedirect(); - - if (!uri.toString().equalsIgnoreCase(future.getURI().toString())) { - final RequestBuilder nBuilder = stripQueryString ? - new RequestBuilder(future.getRequest()).setQueryParameters(null) - : new RequestBuilder(future.getRequest()); - - final URI initialConnectionUri = future.getURI(); - final boolean initialConnectionKeepAlive = future.getKeepAlive(); - future.setURI(uri); - final String newUrl = uri.toString(); - - log.debug("Redirecting to {}", newUrl); - for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE)) { - Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); - nBuilder.addOrReplaceCookie(c); - } - - for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE2)) { - Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); - nBuilder.addOrReplaceCookie(c); - } - - AsyncCallable ac = new AsyncCallable(future) { - public Object call() throws Exception { - if (initialConnectionKeepAlive && ctx.getChannel().isReadable() && - connectionsPool.offer(AsyncHttpProviderUtils.getBaseUrl(initialConnectionUri), ctx.getChannel())) { - return null; - } - finishChannel(ctx); - return null; - } - }; - - if (response.isChunked()) { - // We must make sure there is no bytes left before executing the next request. - ctx.setAttachment(ac); - } else { - ac.call(); - } - nextRequest(nBuilder.setUrl(newUrl).build(), future); - return; - } - } else { - throw new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects()); - } - } - - if (!future.getAndSetStatusReceived(true) && updateStatusAndInterrupt(handler, status)) { - finishUpdate(future, ctx, response.isChunked()); - return; - } else if (updateHeadersAndInterrupt(handler, new ResponseHeaders(future.getURI(), response, this))) { - finishUpdate(future, ctx, response.isChunked()); - return; - } else if (!response.isChunked()) { - if (response.getContent().readableBytes() != 0) { - updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), response, this, true)); - } - finishUpdate(future, ctx, false); - return; - } - - if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) { - updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), response, this, true)); - markAsDone(future, ctx); - drainChannel(ctx, future, future.getKeepAlive(), future.getURI()); - } - - } else if (e.getMessage() instanceof HttpChunk) { - HttpChunk chunk = (HttpChunk) e.getMessage(); - - if (handler != null) { - if (chunk.isLast() || updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), null, this, chunk, chunk.isLast()))) { - if (chunk instanceof DefaultHttpChunkTrailer) { - updateHeadersAndInterrupt(handler, new ResponseHeaders(future.getURI(), - future.getHttpResponse(), this, (HttpChunkTrailer) chunk)); - } - finishUpdate(future, ctx, !chunk.isLast()); - } - } - } - } catch (Exception t) { - if (IOException.class.isAssignableFrom(t.getClass()) && config.getIOExceptionFilters().size() > 0) { - FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler()) - .request(future.getRequest()).ioException(IOException.class.cast(t)).build(); - fc = handleIoException(fc, future); - - if (fc.replayRequest()) { - replayRequest(future, fc, response, ctx); - return; - } - } - - try { - abort(future, t); - } finally { - finishUpdate(future, ctx, false); - throw t; - } - } + Protocol p = (ctx.getPipeline().get(HttpClientCodec.class) != null ? httpProtocol : webSocketProtocol); + p.handle(ctx, e); } private Realm kerberosChallenge(List proxyAuth, @@ -1754,6 +1504,9 @@ public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) } closeChannel(ctx); ctx.sendUpstream(e); + + Protocol p = (ctx.getPipeline().get(HttpClientCodec.class) != null ? httpProtocol : webSocketProtocol); + p.onError(ctx, e); } protected static boolean abortOnConnectCloseException(Throwable cause) { @@ -2164,5 +1917,405 @@ public boolean canCacheConnection() { public void destroy() { } } + + private static final boolean validateWebSocketRequest(Request request, AsyncHandler asyncHandler) { + if (request.getMethod() != "GET" || !WebSocketUpgradeHandler.class.isAssignableFrom(asyncHandler.getClass())) { + return false; + } + return true; + } + + private final class HttpProtocol implements Protocol { + @Override + public void handle(final ChannelHandlerContext ctx, final MessageEvent e) throws Exception { + final NettyResponseFuture future = (NettyResponseFuture) ctx.getAttachment(); + future.touch(); + + // The connect timeout occured. + if (future.isCancelled() || future.isDone()) { + finishChannel(ctx); + return; + } + + HttpRequest nettyRequest = future.getNettyRequest(); + AsyncHandler handler = future.getAsyncHandler(); + Request request = future.getRequest(); + HttpResponse response = null; + try { + if (e.getMessage() instanceof HttpResponse) { + response = (HttpResponse) e.getMessage(); + + log.debug("\n\nRequest {}\n\nResponse {}\n", nettyRequest, response); + + // Required if there is some trailing headers. + future.setHttpResponse(response); + + int statusCode = response.getStatus().getCode(); + + String ka = response.getHeader(HttpHeaders.Names.CONNECTION); + future.setKeepAlive(ka == null || ka.toLowerCase().equals("keep-alive")); + + List wwwAuth = getAuthorizationToken(response.getHeaders(), HttpHeaders.Names.WWW_AUTHENTICATE); + Realm realm = request.getRealm() != null ? request.getRealm() : config.getRealm(); + + HttpResponseStatus status = new ResponseStatus(future.getURI(), response, NettyAsyncHttpProvider.this); + FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(handler).request(request).responseStatus(status).build(); + for (ResponseFilter asyncFilter : config.getResponseFilters()) { + try { + fc = asyncFilter.filter(fc); + if (fc == null) { + throw new NullPointerException("FilterContext is null"); + } + } catch (FilterException efe) { + abort(future, efe); + } + } + + // The request has changed + if (fc.replayRequest()) { + replayRequest(future, fc, response, ctx); + return; + } + + Realm newRealm = null; + ProxyServer proxyServer = request.getProxyServer() != null ? request.getProxyServer() : config.getProxyServer(); + final FluentCaseInsensitiveStringsMap headers = request.getHeaders(); + final RequestBuilder builder = new RequestBuilder(future.getRequest()); + + if (realm != null && !future.getURI().getPath().equalsIgnoreCase(realm.getUri())) { + builder.setUrl(future.getURI().toString()); + } + + if (statusCode == 401 + && wwwAuth.size() > 0 + && !future.getAndSetAuth(true)) { + + future.setState(NettyResponseFuture.STATE.NEW); + // NTLM + if (!wwwAuth.contains("Kerberos") && (wwwAuth.contains("NTLM") || (wwwAuth.contains("Negotiate")))) { + newRealm = ntlmChallenge(wwwAuth, request, proxyServer, headers, realm, future); + // SPNEGO KERBEROS + } else if (wwwAuth.contains("Negotiate")) { + newRealm = kerberosChallenge(wwwAuth, request, proxyServer, headers, realm, future); + if (newRealm == null) return; + } else { + Realm.RealmBuilder realmBuilder; + if (realm != null) { + realmBuilder = new Realm.RealmBuilder().clone(realm).setScheme(realm.getAuthScheme()) + ; + } else { + realmBuilder = new Realm.RealmBuilder(); + } + newRealm = realmBuilder + .setUri(URI.create(request.getUrl()).getPath()) + .setMethodName(request.getMethod()) + .setUsePreemptiveAuth(true) + .parseWWWAuthenticateHeader(wwwAuth.get(0)) + .build(); + } + + final Realm nr = newRealm; + + log.debug("Sending authentication to {}", request.getUrl()); + AsyncCallable ac = new AsyncCallable(future) { + public Object call() throws Exception { + drainChannel(ctx, future, future.getKeepAlive(), future.getURI()); + nextRequest(builder.setHeaders(headers).setRealm(nr).build(), future); + return null; + } + }; + + if (future.getKeepAlive() && response.isChunked()) { + // We must make sure there is no bytes left before executing the next request. + ctx.setAttachment(ac); + } else { + ac.call(); + } + return; + } + + if (statusCode == 100) { + future.getAndSetWriteHeaders(false); + future.getAndSetWriteBody(true); + writeRequest(ctx.getChannel(), config, future, nettyRequest); + return; + } + + List proxyAuth = getAuthorizationToken(response.getHeaders(), HttpHeaders.Names.PROXY_AUTHENTICATE); + if (statusCode == 407 + && proxyAuth.size() > 0 + && !future.getAndSetAuth(true)) { + + log.debug("Sending proxy authentication to {}", request.getUrl()); + + future.setState(NettyResponseFuture.STATE.NEW); + + if (!proxyAuth.contains("Kerberos") && (proxyAuth.get(0).contains("NTLM") || (proxyAuth.contains("Negotiate")))) { + newRealm = ntlmProxyChallenge(proxyAuth, request, proxyServer, headers, realm, future); + // SPNEGO KERBEROS + } else if (proxyAuth.contains("Negotiate")) { + newRealm = kerberosChallenge(proxyAuth, request, proxyServer, headers, realm, future); + if (newRealm == null) return; + } else { + newRealm = future.getRequest().getRealm(); + } + + Request req = builder.setHeaders(headers).setRealm(newRealm).build(); + future.setReuseChannel(true); + future.setConnectAllowed(true); + nextRequest(req, future); + return; + } + + if (future.getNettyRequest().getMethod().equals(HttpMethod.CONNECT) + && statusCode == 200) { + + log.debug("Connected to {}:{}", proxyServer.getHost(), proxyServer.getPort()); + + if (future.getKeepAlive()) { + future.attachChannel(ctx.getChannel(), true); + } + + try { + log.debug("Connecting to proxy {} for scheme {}", proxyServer, request.getUrl()); + upgradeProtocol(ctx.getChannel().getPipeline(), request.getUrl()); + } catch (Throwable ex) { + abort(future, ex); + } + Request req = builder.build(); + future.setReuseChannel(true); + future.setConnectAllowed(false); + nextRequest(req, future); + return; + } + + boolean redirectEnabled = request.isRedirectEnabled() ? true : config.isRedirectEnabled(); + if (redirectEnabled && (statusCode == 302 || statusCode == 301 || statusCode == 307)) { + + if (future.incrementAndGetCurrentRedirectCount() < config.getMaxRedirects()) { + // We must allow 401 handling again. + future.getAndSetAuth(false); + + String location = response.getHeader(HttpHeaders.Names.LOCATION); + URI uri = AsyncHttpProviderUtils.getRedirectUri(future.getURI(), location); + boolean stripQueryString = config.isRemoveQueryParamOnRedirect(); + + if (!uri.toString().equalsIgnoreCase(future.getURI().toString())) { + final RequestBuilder nBuilder = stripQueryString ? + new RequestBuilder(future.getRequest()).setQueryParameters(null) + : new RequestBuilder(future.getRequest()); + + final URI initialConnectionUri = future.getURI(); + final boolean initialConnectionKeepAlive = future.getKeepAlive(); + future.setURI(uri); + final String newUrl = uri.toString(); + + log.debug("Redirecting to {}", newUrl); + for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE)) { + Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); + nBuilder.addOrReplaceCookie(c); + } + + for (String cookieStr : future.getHttpResponse().getHeaders(HttpHeaders.Names.SET_COOKIE2)) { + Cookie c = AsyncHttpProviderUtils.parseCookie(cookieStr); + nBuilder.addOrReplaceCookie(c); + } + + AsyncCallable ac = new AsyncCallable(future) { + public Object call() throws Exception { + if (initialConnectionKeepAlive && ctx.getChannel().isReadable() && + connectionsPool.offer(AsyncHttpProviderUtils.getBaseUrl(initialConnectionUri), ctx.getChannel())) { + return null; + } + finishChannel(ctx); + return null; + } + }; + + if (response.isChunked()) { + // We must make sure there is no bytes left before executing the next request. + ctx.setAttachment(ac); + } else { + ac.call(); + } + nextRequest(nBuilder.setUrl(newUrl).build(), future); + return; + } + } else { + throw new MaxRedirectException("Maximum redirect reached: " + config.getMaxRedirects()); + } + } + + if (!future.getAndSetStatusReceived(true) && updateStatusAndInterrupt(handler, status)) { + finishUpdate(future, ctx, response.isChunked()); + return; + } else if (updateHeadersAndInterrupt(handler, new ResponseHeaders(future.getURI(), response, NettyAsyncHttpProvider.this))) { + finishUpdate(future, ctx, response.isChunked()); + return; + } else if (!response.isChunked()) { + if (response.getContent().readableBytes() != 0) { + updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), response, NettyAsyncHttpProvider.this, true)); + } + finishUpdate(future, ctx, false); + return; + } + + if (nettyRequest.getMethod().equals(HttpMethod.HEAD)) { + updateBodyAndInterrupt(future, handler, new ResponseBodyPart(future.getURI(), response, NettyAsyncHttpProvider.this, true)); + markAsDone(future, ctx); + drainChannel(ctx, future, future.getKeepAlive(), future.getURI()); + } + + } else if (e.getMessage() instanceof HttpChunk) { + HttpChunk chunk = (HttpChunk) e.getMessage(); + + if (handler != null) { + if (chunk.isLast() || updateBodyAndInterrupt(future, handler, + new ResponseBodyPart(future.getURI(), null, NettyAsyncHttpProvider.this, chunk, chunk.isLast()))) { + if (chunk instanceof DefaultHttpChunkTrailer) { + updateHeadersAndInterrupt(handler, new ResponseHeaders(future.getURI(), + future.getHttpResponse(), NettyAsyncHttpProvider.this, (HttpChunkTrailer) chunk)); + } + finishUpdate(future, ctx, !chunk.isLast()); + } + } + } + } catch (Exception t) { + if (IOException.class.isAssignableFrom(t.getClass()) && config.getIOExceptionFilters().size() > 0) { + FilterContext fc = new FilterContext.FilterContextBuilder().asyncHandler(future.getAsyncHandler()) + .request(future.getRequest()).ioException(IOException.class.cast(t)).build(); + fc = handleIoException(fc, future); + + if (fc.replayRequest()) { + replayRequest(future, fc, response, ctx); + return; + } + } + + try { + abort(future, t); + } finally { + finishUpdate(future, ctx, false); + throw t; + } + } + } + + @Override + public void onError(ChannelHandlerContext ctx, ExceptionEvent e) { + } + + @Override + public void onClose(ChannelHandlerContext ctx, ChannelStateEvent e) { + } + } + + private final class WebSocketProtocol implements Protocol { + + @Override + public void handle(ChannelHandlerContext ctx, MessageEvent e) throws Exception { + final NettyResponseFuture future = (NettyResponseFuture) ctx.getAttachment(); + NettyResponseFuture nettyResponse = NettyResponseFuture.class.cast(ctx.getAttachment()); + WebSocketUpgradeHandler h = WebSocketUpgradeHandler.class.cast(nettyResponse.getAsyncHandler()); + + if (e.getMessage() instanceof HttpResponse) { + HttpResponse response = (HttpResponse) e.getMessage(); + final org.jboss.netty.handler.codec.http.HttpResponseStatus status = + new org.jboss.netty.handler.codec.http.HttpResponseStatus(101, "Web Socket Protocol Handshake"); + + final boolean validStatus = response.getStatus().equals(status); + final boolean validUpgrade = response.getHeader(HttpHeaders.Names.UPGRADE) != null; + final boolean validConnection = response.getHeader(HttpHeaders.Names.CONNECTION).equals(HttpHeaders.Values.UPGRADE); + + HttpResponseStatus s = new ResponseStatus(future.getURI(), response, NettyAsyncHttpProvider.this); + final boolean statusReceived = h.onStatusReceived(s) == STATE.UPGRADE; + + if (!validStatus || !validUpgrade || !validConnection || !statusReceived) { + throw new IOException("Invalid handshake response"); + } + + String accept = response.getHeader("Sec-WebSocket-Accept"); + String key = WebSocketUtil.getAcceptKey(future.getNettyRequest().getHeader(WEBSOCKET_KEY)); + if (accept == null || !accept.equals(key)) { + throw new IOException(String.format("Invalid challenge. Actual: %s. Expected: %s", accept, key)); + } + + ctx.getPipeline().replace("ws-decoder", "ws-decoder", new WebSocket08FrameDecoder(false, false)); + ctx.getPipeline().replace("ws-encoder", "ws-encoder", new WebSocket08FrameEncoder(true)); + if (h.onHeadersReceived(new ResponseHeaders(future.getURI(), response, NettyAsyncHttpProvider.this)) == STATE.CONTINUE) { + h.onSuccess(new NettyWebSocket(ctx.getChannel())); + } + future.done(null); + } else if (e.getMessage() instanceof WebSocketFrame) { + final WebSocketFrame frame = (WebSocketFrame) e.getMessage(); + + HttpChunk webSocketChunk = new HttpChunk() { + private ChannelBuffer content; + + @Override + public boolean isLast() { + return false; + } + + @Override + public ChannelBuffer getContent() { + return content; + } + + @Override + public void setContent(ChannelBuffer content) { + this.content = content; + } + }; + + webSocketChunk.setContent(ChannelBuffers.wrappedBuffer(frame.getBinaryData())); + ResponseBodyPart rp = new ResponseBodyPart(future.getURI(), null, NettyAsyncHttpProvider.this, webSocketChunk, true); + h.onBodyPartReceived(rp); + + NettyWebSocket webSocket = NettyWebSocket.class.cast(h.onCompleted()); + webSocket.onMessage(rp.getBodyPartBytes()); + webSocket.onTextMessage(frame.getBinaryData().toString("UTF-8")); + } else { + log.error("Invalid attachment {}", ctx.getAttachment()); + } + } + + @Override + public void onError(ChannelHandlerContext ctx, ExceptionEvent e) { + try { + log.trace("onError {}", e); + if (!NettyResponseFuture.class.isAssignableFrom(ctx.getAttachment().getClass())) { + return; + } + + NettyResponseFuture nettyResponse = NettyResponseFuture.class.cast(ctx.getAttachment()); + WebSocketUpgradeHandler h = WebSocketUpgradeHandler.class.cast(nettyResponse.getAsyncHandler()); + + NettyWebSocket webSocket = NettyWebSocket.class.cast(h.onCompleted()); + webSocket.onError(e.getCause()); + webSocket.close(); + } catch (Throwable t) { + log.error("onError", t); + } + } + + @Override + public void onClose(ChannelHandlerContext ctx, ChannelStateEvent e) { + log.trace("onClose {}", e); + if (!NettyResponseFuture.class.isAssignableFrom(ctx.getAttachment().getClass())) { + return; + } + + try { + NettyResponseFuture nettyResponse = NettyResponseFuture.class.cast(ctx.getAttachment()); + WebSocketUpgradeHandler h = WebSocketUpgradeHandler.class.cast(nettyResponse.getAsyncHandler()); + NettyWebSocket webSocket = NettyWebSocket.class.cast(h.onCompleted()); + + webSocket.close(); + } catch (Throwable t) { + log.error("onError", t); + } + } + } } diff --git a/src/main/java/com/ning/http/client/providers/netty/NettyWebSocket.java b/src/main/java/com/ning/http/client/providers/netty/NettyWebSocket.java new file mode 100644 index 0000000000..8957776f0d --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/NettyWebSocket.java @@ -0,0 +1,96 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.providers.netty; + +import com.ning.http.client.providers.netty.netty4.BinaryWebSocketFrame; +import com.ning.http.client.providers.netty.netty4.TextWebSocketFrame; +import com.ning.http.client.websocket.WebSocket; +import com.ning.http.client.websocket.WebSocketByteListener; +import com.ning.http.client.websocket.WebSocketListener; +import com.ning.http.client.websocket.WebSocketTextListener; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; + +import java.io.UnsupportedEncodingException; +import java.util.concurrent.ConcurrentLinkedQueue; + +import static org.jboss.netty.buffer.ChannelBuffers.wrappedBuffer; + +public class NettyWebSocket implements WebSocket { + + private final Channel channel; + private final ConcurrentLinkedQueue listeners = new ConcurrentLinkedQueue(); + + public NettyWebSocket(Channel channel) { + this.channel = channel; + } + + @Override + public WebSocket sendMessage(byte[] message) { + channel.write(new BinaryWebSocketFrame(wrappedBuffer(message))); + return this; + } + + @Override + public WebSocket sendTextMessage(String message) { + channel.write(new TextWebSocketFrame(message)); + return this; + } + + @Override + public WebSocket addMessageListener(WebSocketListener l) { + listeners.add(l); + return this; + } + + @Override + public void close() { + onClose(); + channel.close(); + } + + protected void onMessage(byte[] message) { + for (WebSocketListener l : listeners) { + if (WebSocketByteListener.class.isAssignableFrom(l.getClass())) { + WebSocketByteListener.class.cast(l).onMessage(message); + } + } + } + + protected void onTextMessage(String message) { + for (WebSocketListener l : listeners) { + if (WebSocketTextListener.class.isAssignableFrom(l.getClass())) { + WebSocketTextListener.class.cast(l).onMessage(message); + } + } + } + + protected void onError(Throwable t) { + for (WebSocketListener l : listeners) { + l.onError(t); + } + } + + protected void onClose() { + for (WebSocketListener l : listeners) { + l.onClose(this); + } + } + + @Override + public String toString() { + return "NettyWebSocket{" + + "channel=" + channel + + '}'; + } +} diff --git a/src/main/java/com/ning/http/client/providers/netty/Protocol.java b/src/main/java/com/ning/http/client/providers/netty/Protocol.java new file mode 100644 index 0000000000..718cd20305 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/Protocol.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.providers.netty; + +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.channel.ChannelStateEvent; +import org.jboss.netty.channel.ExceptionEvent; +import org.jboss.netty.channel.MessageEvent; + +public interface Protocol { + + void handle(ChannelHandlerContext ctx, MessageEvent e) throws Exception; + + void onError(ChannelHandlerContext ctx, ExceptionEvent e); + + void onClose(ChannelHandlerContext ctx, ChannelStateEvent e); +} diff --git a/src/main/java/com/ning/http/client/providers/netty/WebSocketUtil.java b/src/main/java/com/ning/http/client/providers/netty/WebSocketUtil.java new file mode 100644 index 0000000000..0e9cf3501f --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/WebSocketUtil.java @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.providers.netty; + +import com.ning.http.util.Base64; +import org.jboss.netty.util.CharsetUtil; + +import java.io.UnsupportedEncodingException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public final class WebSocketUtil { + public static final String MAGIC_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + + public static String getKey() { + byte[] nonce = createRandomBytes(16); + return base64Encode(nonce); + } + + public static String getAcceptKey(String key) throws UnsupportedEncodingException { + String acceptSeed = key + MAGIC_GUID; + byte[] sha1 = sha1(acceptSeed.getBytes("US-ASCII")); + return base64Encode(sha1); + } + + public static byte[] md5(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("MD5"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("MD5 not supported on this platform"); + } + } + + public static byte[] sha1(byte[] bytes) { + try { + MessageDigest md = MessageDigest.getInstance("SHA1"); + return md.digest(bytes); + } catch (NoSuchAlgorithmException e) { + throw new InternalError("SHA-1 not supported on this platform"); + } + } + + public static String base64Encode(byte[] bytes) { + return Base64.encode(bytes); + } + + public static byte[] createRandomBytes(int size) { + byte[] bytes = new byte[size]; + + for (int i = 0; i < size; i++) { + bytes[i] = (byte) createRandomNumber(0, 255); + } + + return bytes; + } + + public static int createRandomNumber(int min, int max) { + return (int) (Math.random() * max + min); + } + +} + diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/BinaryWebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/BinaryWebSocketFrame.java new file mode 100644 index 0000000000..8a5f2bf533 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/BinaryWebSocketFrame.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class BinaryWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty binary frame. + */ + public BinaryWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new binary frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param binaryData + * the content of the frame. + */ + public BinaryWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new binary frame with the specified binary data and the final + * fragment flag. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public BinaryWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/CloseWebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/CloseWebSocketFrame.java new file mode 100644 index 0000000000..de7a180116 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/CloseWebSocketFrame.java @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket Frame for closing the connection + * + * @author Vibul Imtarnasan + */ +public class CloseWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty close frame. + */ + public CloseWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new close frame + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + */ + public CloseWebSocketFrame(boolean finalFragment, int rsv) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + } + + @Override + public String toString() { + return getClass().getSimpleName(); + } +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/ContinuationWebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/ContinuationWebSocketFrame.java new file mode 100644 index 0000000000..041cfac01b --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/ContinuationWebSocketFrame.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + +/** + * Web Socket continuation frame containing continuation text or binary data. + * This is used for fragmented messages where the contents of a messages is + * contained more than 1 frame. + * + * @author Vibul Imtarnasan + */ +public class ContinuationWebSocketFrame extends WebSocketFrame { + + private String aggregatedText = null; + + /** + * Creates a new empty continuation frame. + */ + public ContinuationWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new continuation frame with the specified binary data. The + * final fragment flag is set to true. + * + * @param binaryData + * the content of the frame. + */ + public ContinuationWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new continuation frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + /** + * Creates a new continuation frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + * @param aggregatedText + * Aggregated text set by decoder on the final continuation frame + * of a fragmented text message + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData, String aggregatedText) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + this.aggregatedText = aggregatedText; + } + + /** + * Creates a new continuation frame with the specified text data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param text + * text content of the frame. + */ + public ContinuationWebSocketFrame(boolean finalFragment, int rsv, String text) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setText(text); + } + + /** + * Returns the text data in this frame + */ + public String getText() { + if (this.getBinaryData() == null) { + return null; + } + return this.getBinaryData().toString(CharsetUtil.UTF_8); + } + + /** + * Sets the string for this frame + * + * @param text + * text to store + */ + public void setText(String text) { + if (text == null || text.equalsIgnoreCase("")) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + + /** + * Aggregated text returned by decoder on the final continuation frame of a + * fragmented text message + */ + public String getAggregatedText() { + return aggregatedText; + } + + public void setAggregatedText(String aggregatedText) { + this.aggregatedText = aggregatedText; + } + +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/PingWebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/PingWebSocketFrame.java new file mode 100644 index 0000000000..04acbe8e9b --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/PingWebSocketFrame.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class PingWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty ping frame. + */ + public PingWebSocketFrame() { + this.setFinalFragment(true); + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new ping frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PingWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new ping frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public PingWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/PongWebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/PongWebSocketFrame.java new file mode 100644 index 0000000000..f754a2c88a --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/PongWebSocketFrame.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; + +/** + * Web Socket frame containing binary data + * + * @author Vibul Imtarnasan + */ +public class PongWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty pong frame. + */ + public PongWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new pong frame with the specified binary data. + * + * @param binaryData + * the content of the frame. + */ + public PongWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new pong frame with the specified binary data + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. + */ + public PongWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(data: " + getBinaryData() + ')'; + } + +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/TextWebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/TextWebSocketFrame.java new file mode 100644 index 0000000000..9af42897c6 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/TextWebSocketFrame.java @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.util.CharsetUtil; + +/** + * Web Socket text frame with assumed UTF-8 encoding + * + * @author Vibul Imtarnasan + * + */ +public class TextWebSocketFrame extends WebSocketFrame { + + /** + * Creates a new empty text frame. + */ + public TextWebSocketFrame() { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } + + /** + * Creates a new text frame with the specified text string. The final + * fragment flag is set to true. + * + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(String text) { + if (text == null || text.equalsIgnoreCase("")) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + /** + * Creates a new text frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param binaryData + * the content of the frame. Must be UTF-8 encoded + */ + public TextWebSocketFrame(ChannelBuffer binaryData) { + this.setBinaryData(binaryData); + } + + /** + * Creates a new text frame with the specified text string. The final + * fragment flag is set to true. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param text + * String to put in the frame + */ + public TextWebSocketFrame(boolean finalFragment, int rsv, String text) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + if (text == null || text.equalsIgnoreCase("")) { + this.setBinaryData(ChannelBuffers.EMPTY_BUFFER); + } else { + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + } + + /** + * Creates a new text frame with the specified binary data. The final + * fragment flag is set to true. + * + * @param finalFragment + * flag indicating if this frame is the final fragment + * @param rsv + * reserved bits used for protocol extensions + * @param binaryData + * the content of the frame. Must be UTF-8 encoded + */ + public TextWebSocketFrame(boolean finalFragment, int rsv, ChannelBuffer binaryData) { + this.setFinalFragment(finalFragment); + this.setRsv(rsv); + this.setBinaryData(binaryData); + } + + /** + * Returns the text data in this frame + */ + public String getText() { + if (this.getBinaryData() == null) { + return null; + } + return this.getBinaryData().toString(CharsetUtil.UTF_8); + } + + /** + * Sets the string for this frame + * + * @param text + * text to store + */ + public void setText(String text) { + if (text == null) { + throw new NullPointerException("text"); + } + this.setBinaryData(ChannelBuffers.copiedBuffer(text, CharsetUtil.UTF_8)); + } + + @Override + public String toString() { + return getClass().getSimpleName() + "(text: " + getText() + ')'; + } +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/UTF8Exception.java b/src/main/java/com/ning/http/client/providers/netty/netty4/UTF8Exception.java new file mode 100644 index 0000000000..eabd3cb49e --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/UTF8Exception.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package com.ning.http.client.providers.netty.netty4; + +/** + * Invalid UTF8 bytes encountered + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Exception extends RuntimeException { + private static final long serialVersionUID = 1L; + + public UTF8Exception(String reason) { + super(reason); + } +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/UTF8Output.java b/src/main/java/com/ning/http/client/providers/netty/netty4/UTF8Output.java new file mode 100644 index 0000000000..c2c5971040 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/UTF8Output.java @@ -0,0 +1,89 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Adaptation of http://bjoern.hoehrmann.de/utf-8/decoder/dfa/ + * + * Copyright (c) 2008-2009 Bjoern Hoehrmann + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated + * documentation files (the "Software"), to deal in the Software without restriction, including without limitation + * the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and + * to permit persons to whom the Software is furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all copies or substantial portions + * of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO + * THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF + * CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + */ +package com.ning.http.client.providers.netty.netty4; + +/** + * Checks UTF8 bytes for validity before converting it into a string + * + * @author Bjoern Hoehrmann + * @author https://github.com/joewalnes/webbit + * @author Vibul Imtarnasan + */ +public class UTF8Output { + 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 int codep = 0; + + private final StringBuilder stringBuilder; + + public UTF8Output(byte[] bytes) { + stringBuilder = new StringBuilder(bytes.length); + write(bytes); + } + + public void write(byte[] bytes) { + for (byte b : bytes) { + write(b); + } + } + + public void write(int 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) { + stringBuilder.append((char) codep); + } else if (state == UTF8_REJECT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + } + + @Override + public String toString() { + if (state != UTF8_ACCEPT) { + throw new UTF8Exception("bytes are not UTF-8"); + } + return stringBuilder.toString(); + } +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocket08FrameDecoder.java b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocket08FrameDecoder.java new file mode 100644 index 0000000000..fa59f866af --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocket08FrameDecoder.java @@ -0,0 +1,384 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +// (BSD License: http://www.opensource.org/licenses/bsd-license) +// +// Copyright (c) 2011, Joe Walnes and contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the +// following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of the Webbit nor the names of +// its contributors may be used to endorse or promote products +// derived from this software without specific prior written +// permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelFutureListener; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.CorruptedFrameException; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.replay.ReplayingDecoder; +import org.jboss.netty.logging.InternalLogger; +import org.jboss.netty.logging.InternalLoggerFactory; + +/** + * Decodes a web socket frame from wire protocol version 8 format. This code was + * forked from webbit and + * modified. + * + * @author Aslak Hellesøy + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameDecoder extends ReplayingDecoder { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(WebSocket08FrameDecoder.class); + + private static final byte OPCODE_CONT = 0x0; + private static final byte OPCODE_TEXT = 0x1; + private static final byte OPCODE_BINARY = 0x2; + private static final byte OPCODE_CLOSE = 0x8; + private static final byte OPCODE_PING = 0x9; + private static final byte OPCODE_PONG = 0xA; + + private UTF8Output fragmentedFramesText = null; + private int fragmentedFramesCount = 0; + + private boolean frameFinalFlag; + private int frameRsv; + private int frameOpcode; + private long framePayloadLength; + private ChannelBuffer framePayload = null; + private int framePayloadBytesRead = 0; + private ChannelBuffer maskingKey; + + private boolean allowExtensions = false; + private boolean maskedPayload = false; + private boolean receivedClosingHandshake = false; + + public enum State { + FRAME_START, MASKING_KEY, PAYLOAD, CORRUPT + } + + /** + * Constructor + * + * @param maskedPayload + * Web socket servers must set this to true processed incoming + * masked payload. Client implementations must set this to false. + * @param allowExtensions + * Flag to allow reserved extension bits to be used or not + */ + public WebSocket08FrameDecoder(boolean maskedPayload, boolean allowExtensions) { + super(State.FRAME_START); + this.maskedPayload = maskedPayload; + this.allowExtensions = allowExtensions; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, Channel channel, ChannelBuffer buffer, State state) throws Exception { + + // Discard all data received if closing handshake was received before. + if (receivedClosingHandshake) { + buffer.skipBytes(actualReadableBytes()); + return null; + } + + switch (state) { + case FRAME_START: + framePayloadBytesRead = 0; + framePayloadLength = -1; + framePayload = null; + + // FIN, RSV, OPCODE + byte b = buffer.readByte(); + frameFinalFlag = (b & 0x80) != 0; + frameRsv = (b & 0x70) >> 4; + frameOpcode = (b & 0x0F); + + logger.debug("Decoding WebSocket Frame opCode=" + frameOpcode); + + // MASK, PAYLOAD LEN 1 + b = buffer.readByte(); + boolean frameMasked = (b & 0x80) != 0; + int framePayloadLen1 = (b & 0x7F); + + if (frameRsv != 0 && !this.allowExtensions) { + protocolViolation(channel, "RSV != 0 and no extension negotiated, RSV:" + frameRsv); + return null; + } + + if (this.maskedPayload && !frameMasked) { + protocolViolation(channel, "unmasked client to server frame"); + return null; + } + if (frameOpcode > 7) { // control frame (have MSB in opcode set) + + // control frames MUST NOT be fragmented + if (!frameFinalFlag) { + protocolViolation(channel, "fragmented control frame"); + return null; + } + + // control frames MUST have payload 125 octets or less + if (framePayloadLen1 > 125) { + protocolViolation(channel, "control frame with payload length > 125 octets"); + return null; + } + + // check for reserved control frame opcodes + if (!(frameOpcode == OPCODE_CLOSE || frameOpcode == OPCODE_PING || frameOpcode == OPCODE_PONG)) { + protocolViolation(channel, "control frame using reserved opcode " + frameOpcode); + return null; + } + + // 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) { + protocolViolation(channel, "received close control frame with payload len 1"); + return null; + } + } else { // data frame + // check for reserved data frame opcodes + if (!(frameOpcode == OPCODE_CONT || frameOpcode == OPCODE_TEXT || frameOpcode == OPCODE_BINARY)) { + protocolViolation(channel, "data frame using reserved opcode " + frameOpcode); + return null; + } + + // check opcode vs message fragmentation state 1/2 + if (fragmentedFramesCount == 0 && frameOpcode == OPCODE_CONT) { + protocolViolation(channel, "received continuation data frame outside fragmented message"); + return null; + } + + // check opcode vs message fragmentation state 2/2 + if (fragmentedFramesCount != 0 && frameOpcode != OPCODE_CONT && frameOpcode != OPCODE_PING) { + protocolViolation(channel, "received non-continuation data frame while inside fragmented message"); + return null; + } + } + + if (framePayloadLen1 == 126) { + framePayloadLength = buffer.readUnsignedShort(); + if (framePayloadLength < 126) { + protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); + return null; + } + } else if (framePayloadLen1 == 127) { + framePayloadLength = buffer.readLong(); + // TODO: check if it's bigger than 0x7FFFFFFFFFFFFFFF, Maybe + // just check if it's negative? + + if (framePayloadLength < 65536) { + protocolViolation(channel, "invalid data frame length (not using minimal length encoding)"); + return null; + } + } else { + framePayloadLength = framePayloadLen1; + } + + // logger.debug("Frame length=" + framePayloadLength); + checkpoint(State.MASKING_KEY); + case MASKING_KEY: + if (this.maskedPayload) { + maskingKey = buffer.readBytes(4); + } + checkpoint(State.PAYLOAD); + case PAYLOAD: + // Some times, the payload may not be delivered in 1 nice packet + // We need to accumulate the data until we have it all + int rbytes = actualReadableBytes(); + ChannelBuffer payloadBuffer = null; + + int willHaveReadByteCount = framePayloadBytesRead + rbytes; + // logger.debug("Frame rbytes=" + rbytes + " willHaveReadByteCount=" + // + willHaveReadByteCount + " framePayloadLength=" + + // framePayloadLength); + if (willHaveReadByteCount == framePayloadLength) { + // We have all our content so proceed to process + payloadBuffer = buffer.readBytes(rbytes); + } else if (willHaveReadByteCount < framePayloadLength) { + // We don't have all our content so accumulate payload. + // Returning null means we will get called back + payloadBuffer = buffer.readBytes(rbytes); + if (framePayload == null) { + framePayload = channel.getConfig().getBufferFactory().getBuffer(toFrameLength(framePayloadLength)); + } + framePayload.writeBytes(payloadBuffer); + framePayloadBytesRead = framePayloadBytesRead + rbytes; + + // Return null to wait for more bytes to arrive + return null; + } else if (willHaveReadByteCount > framePayloadLength) { + // We have more than what we need so read up to the end of frame + // Leave the remainder in the buffer for next frame + payloadBuffer = buffer.readBytes(toFrameLength(framePayloadLength - framePayloadBytesRead)); + } + + // Now we have all the data, the next checkpoint must be the next + // frame + checkpoint(State.FRAME_START); + + // Take the data that we have in this packet + if (framePayload == null) { + framePayload = payloadBuffer; + } else { + framePayload.writeBytes(payloadBuffer); + } + + // Unmask data if needed + if (this.maskedPayload) { + unmask(framePayload); + } + + // Processing for fragmented messages + String aggregatedText = null; + if (frameFinalFlag) { + // Final frame of the sequence. Apparently ping frames are + // allowed in the middle of a fragmented message + if (frameOpcode != OPCODE_PING) { + fragmentedFramesCount = 0; + + // Check text for UTF8 correctness + if (frameOpcode == OPCODE_TEXT || fragmentedFramesText != null) { + // Check UTF-8 correctness for this payload + checkUTF8String(channel, framePayload.array()); + + // This does a second check to make sure UTF-8 + // correctness for entire text message + aggregatedText = fragmentedFramesText.toString(); + + fragmentedFramesText = null; + } + } + } else { + // Not final frame so we can expect more frames in the + // fragmented sequence + if (fragmentedFramesCount == 0) { + // First text or binary frame for a fragmented set + fragmentedFramesText = null; + if (frameOpcode == OPCODE_TEXT) { + checkUTF8String(channel, framePayload.array()); + } + } else { + // Subsequent frames - only check if init frame is text + if (fragmentedFramesText != null) { + checkUTF8String(channel, framePayload.array()); + } + } + + // Increment counter + fragmentedFramesCount++; + } + + // Return the frame + if (frameOpcode == OPCODE_TEXT) { + return new TextWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_BINARY) { + return new BinaryWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_PING) { + return new PingWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_PONG) { + return new PongWebSocketFrame(frameFinalFlag, frameRsv, framePayload); + } else if (frameOpcode == OPCODE_CONT) { + return new ContinuationWebSocketFrame(frameFinalFlag, frameRsv, framePayload, aggregatedText); + } else if (frameOpcode == OPCODE_CLOSE) { + this.receivedClosingHandshake = true; + return new CloseWebSocketFrame(frameFinalFlag, frameRsv); + } else { + throw new UnsupportedOperationException("Cannot decode web socket frame with opcode: " + frameOpcode); + } + case CORRUPT: + // If we don't keep reading Netty will throw an exception saying + // we can't return null if no bytes read and state not changed. + buffer.readByte(); + return null; + default: + throw new Error("Shouldn't reach here."); + } + } + + private void unmask(ChannelBuffer frame) { + byte[] bytes = frame.array(); + for (int i = 0; i < bytes.length; i++) { + frame.setByte(i, frame.getByte(i) ^ maskingKey.getByte(i % 4)); + } + } + + private void protocolViolation(Channel channel, String reason) throws CorruptedFrameException { + checkpoint(State.CORRUPT); + if (channel.isConnected()) { + channel.write(ChannelBuffers.EMPTY_BUFFER).addListener(ChannelFutureListener.CLOSE); + channel.close().awaitUninterruptibly(); + } + throw new CorruptedFrameException(reason); + } + + private int toFrameLength(long l) throws TooLongFrameException { + if (l > Integer.MAX_VALUE) { + throw new TooLongFrameException("Length:" + l); + } else { + return (int) l; + } + } + + private void checkUTF8String(Channel channel, byte[] bytes) throws CorruptedFrameException { + try { + // StringBuilder sb = new StringBuilder("UTF8 " + bytes.length + + // " bytes: "); + // for (byte b : bytes) { + // sb.append(Integer.toHexString(b)).append(" "); + // } + // logger.debug(sb.toString()); + + if (fragmentedFramesText == null) { + fragmentedFramesText = new UTF8Output(bytes); + } else { + fragmentedFramesText.write(bytes); + } + } catch (UTF8Exception ex) { + protocolViolation(channel, "invalid UTF-8 bytes"); + } + } +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocket08FrameEncoder.java b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocket08FrameEncoder.java new file mode 100644 index 0000000000..4b95354af6 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocket08FrameEncoder.java @@ -0,0 +1,186 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +// (BSD License: http://www.opensource.org/licenses/bsd-license) +// +// Copyright (c) 2011, Joe Walnes and contributors +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or +// without modification, are permitted provided that the +// following conditions are met: +// +// * Redistributions of source code must retain the above +// copyright notice, this list of conditions and the +// following disclaimer. +// +// * Redistributions in binary form must reproduce the above +// copyright notice, this list of conditions and the +// following disclaimer in the documentation and/or other +// materials provided with the distribution. +// +// * Neither the name of the Webbit nor the names of +// its contributors may be used to endorse or promote products +// derived from this software without specific prior written +// permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +// CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +// INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +// CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +// INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE +// GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR +// BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +// LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT +// OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +// POSSIBILITY OF SUCH DAMAGE. + +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; +import org.jboss.netty.buffer.ChannelBuffers; +import org.jboss.netty.channel.Channel; +import org.jboss.netty.channel.ChannelHandlerContext; +import org.jboss.netty.handler.codec.frame.TooLongFrameException; +import org.jboss.netty.handler.codec.oneone.OneToOneEncoder; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.nio.ByteBuffer; + +/** + *

+ * Encodes a web socket frame into wire protocol version 8 format. This code was + * forked from webbit and + * modified. + *

+ * + * @author Aslak Hellesøy + * @author Vibul Imtarnasan + */ +public class WebSocket08FrameEncoder extends OneToOneEncoder { + + private static final Logger logger = LoggerFactory.getLogger(WebSocket08FrameEncoder.class); + + private static final byte OPCODE_CONT = 0x0; + private static final byte OPCODE_TEXT = 0x1; + private static final byte OPCODE_BINARY = 0x2; + private static final byte OPCODE_CLOSE = 0x8; + private static final byte OPCODE_PING = 0x9; + private static final byte OPCODE_PONG = 0xA; + + private boolean maskPayload = false; + + /** + * Constructor + * + * @param maskPayload + * Web socket clients must set this to true to mask payload. + * Server implementations must set this to false. + */ + public WebSocket08FrameEncoder(boolean maskPayload) { + this.maskPayload = maskPayload; + } + + @Override + protected Object encode(ChannelHandlerContext ctx, Channel channel, Object msg) throws Exception { + + byte[] mask = null; + + if (msg instanceof WebSocketFrame) { + WebSocketFrame frame = (WebSocketFrame) msg; + ChannelBuffer data = frame.getBinaryData(); + if (data == null) { + data = ChannelBuffers.EMPTY_BUFFER; + } + + byte opcode; + if (frame instanceof TextWebSocketFrame) { + opcode = OPCODE_TEXT; + } else if (frame instanceof PingWebSocketFrame) { + opcode = OPCODE_PING; + } else if (frame instanceof PongWebSocketFrame) { + opcode = OPCODE_PONG; + } else if (frame instanceof CloseWebSocketFrame) { + opcode = OPCODE_CLOSE; + } else if (frame instanceof BinaryWebSocketFrame) { + opcode = OPCODE_BINARY; + } else if (frame instanceof ContinuationWebSocketFrame) { + opcode = OPCODE_CONT; + } else { + throw new UnsupportedOperationException("Cannot encode frame of type: " + frame.getClass().getName()); + } + + int length = data.readableBytes(); + + logger.debug("Encoding WebSocket Frame opCode=" + opcode + " length=" + length); + + int b0 = 0; + if (frame.isFinalFragment()) { + b0 |= (1 << 7); + } + b0 |= (frame.getRsv() % 8) << 4; + b0 |= opcode % 128; + + ChannelBuffer header; + ChannelBuffer body; + + if (opcode == OPCODE_PING && length > 125) { + throw new TooLongFrameException("invalid payload for PING (payload length must be <= 125, was " + length); + } + + int maskLength = this.maskPayload ? 4 : 0; + if (length <= 125) { + header = ChannelBuffers.buffer(2 + maskLength); + header.writeByte(b0); + byte b = (byte) (this.maskPayload ? (0x80 | (byte) length) : (byte) length); + header.writeByte(b); + } else if (length <= 0xFFFF) { + header = ChannelBuffers.buffer(4 + maskLength); + header.writeByte(b0); + header.writeByte(this.maskPayload ? (0xFE) : 126); + header.writeByte((length >>> 8) & 0xFF); + header.writeByte((length) & 0xFF); + } else { + header = ChannelBuffers.buffer(10 + maskLength); + header.writeByte(b0); + header.writeByte(this.maskPayload ? (0xFF) : 127); + header.writeLong(length); + } + + // Write payload + if (this.maskPayload) { + Integer random = (int) (Math.random() * Integer.MAX_VALUE); + mask = ByteBuffer.allocate(4).putInt(random).array(); + header.writeBytes(mask); + + body = ChannelBuffers.buffer(length); + int counter = 0; + while (data.readableBytes() > 0) { + byte byteData = data.readByte(); + body.writeByte(byteData ^ mask[+counter++ % 4]); + } + } else { + body = data; + } + return ChannelBuffers.wrappedBuffer(header, body); + } + + // If not websocket, then just return the message + return msg; + } + +} \ No newline at end of file diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocketFrame.java b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocketFrame.java new file mode 100644 index 0000000000..a6c3f90f49 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocketFrame.java @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +import org.jboss.netty.buffer.ChannelBuffer; + +/** + * Base class for web socket frames + * + * @author The Netty Project + */ +public abstract class WebSocketFrame { + + /** + * Flag to indicate if this frame is the final fragment in a message. The + * first fragment (frame) may also be the final fragment. + */ + private boolean finalFragment = true; + + /** + * RSV1, RSV2, RSV3 used for extensions + */ + private int rsv = 0; + + /** + * Contents of this frame + */ + private ChannelBuffer binaryData; + + /** + * Returns binary data + */ + public ChannelBuffer getBinaryData() { + return binaryData; + } + + /** + * Sets the binary data for this frame + */ + public void setBinaryData(ChannelBuffer binaryData) { + this.binaryData = binaryData; + } + + /** + * 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; + } + + public void setFinalFragment(boolean finalFragment) { + this.finalFragment = finalFragment; + } + + /** + * Bits used for extensions to the standard. + */ + public int getRsv() { + return rsv; + } + + public void setRsv(int rsv) { + this.rsv = rsv; + } + +} diff --git a/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocketFrameType.java b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocketFrameType.java new file mode 100644 index 0000000000..449497aec6 --- /dev/null +++ b/src/main/java/com/ning/http/client/providers/netty/netty4/WebSocketFrameType.java @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +/* + * Copyright 2010 Red Hat, Inc. + * + * Red Hat licenses this file to you under the Apache License, version 2.0 + * (the "License"); you may not use this file except in compliance with the + * License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package com.ning.http.client.providers.netty.netty4; + +/** + * Type of web socket frames + * + * @author The Netty Project + */ +public enum WebSocketFrameType { + TEXT, BINARY, PING, PONG, CLOSE, CONTINUATION +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocket.java b/src/main/java/com/ning/http/client/websocket/WebSocket.java new file mode 100644 index 0000000000..ee5ad42a29 --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocket.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +/** + * A Websocket client + */ +public interface WebSocket { + + /** + * Sen a byte message. + * @param message a byte message + * @return this + */ + WebSocket sendMessage(byte[] message); + + /** + * Send a text message + * @param message a text message + * @return this. + */ + WebSocket sendTextMessage(String message); + + /** + * Add a {@link WebSocketListener} + * @param l a {@link WebSocketListener} + * @return this + */ + WebSocket addMessageListener(WebSocketListener l); + + /** + * Close the WebSocket. + */ + void close(); +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocketByteListener.java b/src/main/java/com/ning/http/client/websocket/WebSocketByteListener.java new file mode 100644 index 0000000000..cafee7b0ea --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocketByteListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +/** + * A {@link WebSocketListener} for bytes + */ +public interface WebSocketByteListener extends WebSocketListener { + + /** + * Invoked when bytes are available. + * @param message a byte array. + */ + void onMessage(byte[] message); + +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocketListener.java b/src/main/java/com/ning/http/client/websocket/WebSocketListener.java new file mode 100644 index 0000000000..6149aeffca --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocketListener.java @@ -0,0 +1,38 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +/** + * A generic {@link WebSocketListener} for WebSocket events. Use the appropriate listener for receiving message bytes. + */ +public interface WebSocketListener { + + /** + * Invoked when the {@link WebSocket} is open. + * @param websocket + */ + void onOpen(WebSocket websocket); + + /** + * Invoked when the {@link WebSocket} is close. + * @param websocket + */ + void onClose(WebSocket websocket); + + /** + * Invoked when the {@link WebSocket} is open. + * @param t a {@link Throwable} + */ + void onError(Throwable t); + +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocketPingListener.java b/src/main/java/com/ning/http/client/websocket/WebSocketPingListener.java new file mode 100644 index 0000000000..adc3f82833 --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocketPingListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +/** + * A WebSocket's Ping Listener + */ +public interface WebSocketPingListener extends WebSocketListener { + + /** + * Invoked when a ping message is received + * @param message a byte array + */ + void onPing(byte[] message); + +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocketPongListener.java b/src/main/java/com/ning/http/client/websocket/WebSocketPongListener.java new file mode 100644 index 0000000000..6f398ce0ff --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocketPongListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +/** + * A WebSocket's Pong Listener + */ +public interface WebSocketPongListener extends WebSocketListener { + + /** + * Invoked when a pong message is received + * @param message a byte array + */ + void onPong(byte[] message); + +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocketTextListener.java b/src/main/java/com/ning/http/client/websocket/WebSocketTextListener.java new file mode 100644 index 0000000000..544c0027cd --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocketTextListener.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +/** + * A {@link WebSocketListener} for text message + */ +public interface WebSocketTextListener extends WebSocketListener { + + /** + * Invoked when WebSocket text message are received. + * @param message a {@link String} message + */ + void onMessage(String message); + +} diff --git a/src/main/java/com/ning/http/client/websocket/WebSocketUpgradeHandler.java b/src/main/java/com/ning/http/client/websocket/WebSocketUpgradeHandler.java new file mode 100644 index 0000000000..b5a0e4ef64 --- /dev/null +++ b/src/main/java/com/ning/http/client/websocket/WebSocketUpgradeHandler.java @@ -0,0 +1,188 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +import com.ning.http.client.AsyncHandler; +import com.ning.http.client.HttpResponseBodyPart; +import com.ning.http.client.HttpResponseHeaders; +import com.ning.http.client.HttpResponseStatus; +import com.ning.http.client.UpgradeHandler; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * An {@link AsyncHandler} which is able to execute WebSocket upgrade. Use the Builder for configuring WebSocket options. + */ +public class WebSocketUpgradeHandler implements UpgradeHandler, AsyncHandler { + + private WebSocket webSocket; + private final ConcurrentLinkedQueue l; + private final String protocol; + private final long maxByteSize; + private final long maxTextSize; + private final AtomicBoolean ok = new AtomicBoolean(false); + + private WebSocketUpgradeHandler(Builder b) { + l = b.l; + protocol = b.protocol; + maxByteSize = b.maxByteSize; + maxTextSize = b.maxTextSize; + } + + /** + * {@inheritDoc} + */ + @Override + public final void onThrowable(Throwable t) { + onFailure(t); + } + + /** + * {@inheritDoc} + */ + @Override + public final STATE onBodyPartReceived(HttpResponseBodyPart bodyPart) throws Exception { + return STATE.CONTINUE; + } + + /** + * {@inheritDoc} + */ + @Override + public final STATE onStatusReceived(HttpResponseStatus responseStatus) throws Exception { + if (responseStatus.getStatusCode() == 101) { + return STATE.UPGRADE; + } else { + throw new IllegalStateException("Invalid Upgrade protocol"); + } + } + + /** + * {@inheritDoc} + */ + @Override + public final STATE onHeadersReceived(HttpResponseHeaders headers) throws Exception { + return STATE.CONTINUE; + } + + /** + * {@inheritDoc} + */ + @Override + public final WebSocket onCompleted() throws Exception { + if (webSocket == null) { + throw new IllegalStateException("WebSocket is null"); + } + return webSocket; + } + + /** + * {@inheritDoc} + */ + @Override + public final void onSuccess(WebSocket webSocket) { + this.webSocket = webSocket; + for (WebSocketListener w : l) { + webSocket.addMessageListener(w); + w.onOpen(webSocket); + } + ok.set(true); + } + + /** + * {@inheritDoc} + */ + @Override + public final void onFailure(Throwable t) { + for (WebSocketListener w : l) { + if (!ok.get()) { + webSocket.addMessageListener(w); + } + w.onError(t); + } + } + + /** + * Build a {@link WebSocketUpgradeHandler} + */ + public final static class Builder { + private ConcurrentLinkedQueue l = new ConcurrentLinkedQueue(); + private String protocol = ""; + private long maxByteSize = 8192; + private long maxTextSize = 8192; + + /** + * Add a {@link WebSocketListener} that will be added to the {@link WebSocket} + * + * @param listener a {@link WebSocketListener} + * @return this + */ + public Builder addWebSocketListener(WebSocketListener listener) { + l.add(listener); + return this; + } + + /** + * Remove a {@link WebSocketListener} + * + * @param listener a {@link WebSocketListener} + * @return this + */ + public Builder removeWebSocketListener(WebSocketListener listener) { + l.remove(listener); + return this; + } + + /** + * Set the WebSocket protocol. + * + * @param protocol the WebSocket protocol. + * @return this + */ + public Builder setProtocol(String protocol) { + this.protocol = protocol; + return this; + } + + /** + * Set the max size of the WebSocket byte message that will be sent. + * + * @param maxByteSize max size of the WebSocket byte message + * @return this + */ + public Builder setMaxByteSize(long maxByteSize) { + this.maxByteSize = maxByteSize; + return this; + } + + /** + * Set the max size of the WebSocket text message that will be sent. + * + * @param maxTextSize max size of the WebSocket byte message + * @return this + */ + public Builder setMaxTextSize(long maxTextSize) { + this.maxTextSize = maxTextSize; + return this; + } + + /** + * Build a {@link WebSocketUpgradeHandler} + * @return a {@link WebSocketUpgradeHandler} + */ + public WebSocketUpgradeHandler build() { + return new WebSocketUpgradeHandler(this); + } + } +} diff --git a/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java b/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java index 867a60b01f..13d4484e2b 100644 --- a/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java +++ b/src/main/java/com/ning/http/util/AsyncHttpProviderUtils.java @@ -137,9 +137,10 @@ public final static SimpleDateFormat[] get() { public final static URI createUri(String u) { URI uri = URI.create(u); final String scheme = uri.getScheme(); - if (scheme == null || !scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https")) { + if (scheme == null || !scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https") && !scheme.equalsIgnoreCase("ws") + && !scheme.equalsIgnoreCase("wss")) { throw new IllegalArgumentException("The URI scheme, of the URI " + u - + ", must be equal (ignoring case) to 'http' or 'https'"); + + ", must be equal (ignoring case) to 'http', 'https', 'ws', or 'wss'"); } String path = uri.getPath(); diff --git a/src/test/java/com/ning/http/client/async/AuthTimeoutTest.java b/src/test/java/com/ning/http/client/async/AuthTimeoutTest.java index 6f5fb4eb2a..eda2991303 100644 --- a/src/test/java/com/ning/http/client/async/AuthTimeoutTest.java +++ b/src/test/java/com/ning/http/client/async/AuthTimeoutTest.java @@ -20,7 +20,6 @@ import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; -import org.eclipse.jetty.http.security.Constraint; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.HashLoginService; @@ -31,6 +30,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.security.Constraint; import org.testng.annotations.Test; import javax.servlet.ServletException; @@ -38,8 +38,10 @@ import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; @@ -89,8 +91,11 @@ public void setUpServer(String auth) knownRoles.add(admin); ConstraintSecurityHandler security = new ConstraintSecurityHandler(); - - security.setConstraintMappings(new ConstraintMapping[]{mapping}, knownRoles); + + List cm = new ArrayList(); + cm.add(mapping); + + security.setConstraintMappings(cm, knownRoles); security.setAuthenticator(new BasicAuthenticator()); security.setLoginService(loginService); security.setStrict(false); diff --git a/src/test/java/com/ning/http/client/async/BasicAuthTest.java b/src/test/java/com/ning/http/client/async/BasicAuthTest.java index 791e8ab9ed..bbdefbd3e7 100644 --- a/src/test/java/com/ning/http/client/async/BasicAuthTest.java +++ b/src/test/java/com/ning/http/client/async/BasicAuthTest.java @@ -30,7 +30,6 @@ import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; -import org.eclipse.jetty.http.security.Constraint; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.HashLoginService; @@ -42,6 +41,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.security.Constraint; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -53,8 +53,10 @@ import java.io.FileInputStream; import java.io.IOException; import java.net.URL; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -102,12 +104,15 @@ public void setUpGlobal() throws Exception { mapping.setConstraint(constraint); mapping.setPathSpec("/*"); + List cm = new ArrayList(); + cm.add(mapping); + Set knownRoles = new HashSet(); knownRoles.add(user); knownRoles.add(admin); ConstraintSecurityHandler security = new ConstraintSecurityHandler(); - security.setConstraintMappings(new ConstraintMapping[]{mapping}, knownRoles); + security.setConstraintMappings(cm, knownRoles); security.setAuthenticator(new BasicAuthenticator()); security.setLoginService(loginService); security.setStrict(false); @@ -139,7 +144,8 @@ private String getFileContent(final File file) { if (in != null) { try { in.close(); - } catch (IOException ignored) {} + } catch (IOException ignored) { + } } } @@ -182,7 +188,11 @@ public void handle(String arg0, Request arg1, HttpServletRequest arg2, HttpServl super.handle(arg0, arg1, arg2, arg3); } }; - security.setConstraintMappings(new ConstraintMapping[]{mapping}, knownRoles); + + List cm = new ArrayList(); + cm.add(mapping); + + security.setConstraintMappings(cm, knownRoles); security.setAuthenticator(new DigestAuthenticator()); security.setLoginService(loginService); security.setStrict(true); diff --git a/src/test/java/com/ning/http/client/async/DigestAuthTest.java b/src/test/java/com/ning/http/client/async/DigestAuthTest.java index 9753e4d922..6471174c1e 100644 --- a/src/test/java/com/ning/http/client/async/DigestAuthTest.java +++ b/src/test/java/com/ning/http/client/async/DigestAuthTest.java @@ -19,7 +19,6 @@ import org.apache.log4j.Level; import org.apache.log4j.Logger; import org.apache.log4j.PatternLayout; -import org.eclipse.jetty.http.security.Constraint; import org.eclipse.jetty.security.ConstraintMapping; import org.eclipse.jetty.security.ConstraintSecurityHandler; import org.eclipse.jetty.security.HashLoginService; @@ -30,6 +29,7 @@ import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.handler.AbstractHandler; import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.util.security.Constraint; import org.testng.annotations.BeforeClass; import org.testng.annotations.Test; @@ -37,8 +37,10 @@ import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; @@ -82,12 +84,15 @@ public void setUpGlobal() throws Exception { mapping.setConstraint(constraint); mapping.setPathSpec("/*"); + List cm = new ArrayList(); + cm.add(mapping); + Set knownRoles = new HashSet(); knownRoles.add(user); knownRoles.add(admin); ConstraintSecurityHandler security = new ConstraintSecurityHandler(); - security.setConstraintMappings(new ConstraintMapping[]{mapping}, knownRoles); + security.setConstraintMappings(cm, knownRoles); security.setAuthenticator(new DigestAuthenticator()); security.setLoginService(loginService); security.setStrict(false); diff --git a/src/test/java/com/ning/http/client/async/ProxyyTunnellingTest.java b/src/test/java/com/ning/http/client/async/ProxyyTunnellingTest.java index 6fb2198843..1459a9c6f3 100644 --- a/src/test/java/com/ning/http/client/async/ProxyyTunnellingTest.java +++ b/src/test/java/com/ning/http/client/async/ProxyyTunnellingTest.java @@ -111,7 +111,7 @@ public Response onCompleted(Response response) throws Exception { }); Response r = responseFuture.get(); assertEquals(r.getStatusCode(), 200); - assertEquals(r.getHeader("server"), "Jetty(7.1.4.v20100610)"); + assertEquals(r.getHeader("server"), "Jetty(8.1.0.RC1)"); asyncHttpClient.close(); } @@ -142,7 +142,7 @@ public Response onCompleted(Response response) throws Exception { }); Response r = responseFuture.get(); assertEquals(r.getStatusCode(), 200); - assertEquals(r.getHeader("server"), "Jetty(7.1.4.v20100610)"); + assertEquals(r.getHeader("server"), "Jetty(8.1.0.RC1)"); asyncHttpClient.close(); } @@ -162,7 +162,7 @@ public void testSimpleAHCConfigProxy() throws IOException, InterruptedException, Response r = client.get().get(); assertEquals(r.getStatusCode(), 200); - assertEquals(r.getHeader("server"), "Jetty(7.1.4.v20100610)"); + assertEquals(r.getHeader("server"), "Jetty(8.1.0.RC1)"); client.close(); } diff --git a/src/test/java/com/ning/http/client/async/grizzly/GrizzlyAsyncProviderBasicTest.java b/src/test/java/com/ning/http/client/async/grizzly/GrizzlyAsyncProviderBasicTest.java index 6c68ff867a..6e6b88d67a 100644 --- a/src/test/java/com/ning/http/client/async/grizzly/GrizzlyAsyncProviderBasicTest.java +++ b/src/test/java/com/ning/http/client/async/grizzly/GrizzlyAsyncProviderBasicTest.java @@ -16,6 +16,8 @@ import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.AsyncHttpClientConfig; import com.ning.http.client.AsyncHttpProviderConfig; +import com.ning.http.client.FluentCaseInsensitiveStringsMap; +import com.ning.http.client.Response; import com.ning.http.client.async.AsyncProvidersBasicTest; import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider; import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProviderConfig; @@ -23,8 +25,14 @@ import org.glassfish.grizzly.filterchain.FilterChainBuilder; import org.glassfish.grizzly.nio.transport.TCPNIOTransport; import org.glassfish.grizzly.strategies.SameThreadIOStrategy; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; import static com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProviderConfig.Property.TRANSPORT_CUSTOMIZER; +import static org.testng.Assert.assertEquals; public class GrizzlyAsyncProviderBasicTest extends AsyncProvidersBasicTest { @@ -49,4 +57,8 @@ public void customize(TCPNIOTransport transport, FilterChainBuilder builder) { }); return config; } + + @Test(groups = {"standalone", "default_provider", "async"}, enabled = false) + public void asyncDoPostBasicGZIPTest() throws Throwable { + } } diff --git a/src/test/java/com/ning/http/client/async/grizzly/GrizzlyBasicHttpsTest.java b/src/test/java/com/ning/http/client/async/grizzly/GrizzlyBasicHttpsTest.java index bfd11ad848..14e56cc88e 100644 --- a/src/test/java/com/ning/http/client/async/grizzly/GrizzlyBasicHttpsTest.java +++ b/src/test/java/com/ning/http/client/async/grizzly/GrizzlyBasicHttpsTest.java @@ -28,4 +28,8 @@ public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) { return new AsyncHttpClient(new GrizzlyAsyncHttpProvider(config), config); } + @Override + public void zeroCopyPostTest() throws Throwable { + super.zeroCopyPostTest(); //To change body of overridden methods use File | Settings | File Templates. + } } diff --git a/src/test/java/com/ning/http/client/websocket/AbstractBasicTest.java b/src/test/java/com/ning/http/client/websocket/AbstractBasicTest.java new file mode 100644 index 0000000000..7959eabe2d --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/AbstractBasicTest.java @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import org.eclipse.jetty.server.Request; +import org.eclipse.jetty.server.Server; +import org.eclipse.jetty.server.handler.HandlerWrapper; +import org.eclipse.jetty.server.nio.SelectChannelConnector; +import org.eclipse.jetty.websocket.WebSocketFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.net.ServerSocket; + +public abstract class AbstractBasicTest extends Server { + + public abstract class WebSocketHandler extends HandlerWrapper implements WebSocketFactory.Acceptor { + private final WebSocketFactory _webSocketFactory = new WebSocketFactory(this, 32 * 1024); + + public WebSocketFactory getWebSocketFactory() { + return _webSocketFactory; + } + + /* ------------------------------------------------------------ */ + @Override + public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { + if (_webSocketFactory.acceptWebSocket(request, response) || response.isCommitted()) + return; + super.handle(target, baseRequest, request, response); + } + + /* ------------------------------------------------------------ */ + public boolean checkOrigin(HttpServletRequest request, String origin) { + return true; + } + + } + + protected final Logger log = LoggerFactory.getLogger(AbstractBasicTest.class); + protected int port1; + SelectChannelConnector _connector; + + @AfterClass(alwaysRun = true) + public void tearDownGlobal() throws Exception { + stop(); + } + + protected int findFreePort() throws IOException { + ServerSocket socket = null; + + try { + socket = new ServerSocket(0); + + return socket.getLocalPort(); + } finally { + if (socket != null) { + socket.close(); + } + } + } + + protected String getTargetUrl() { + return String.format("ws://127.0.0.1:%d/", port1); + } + + @BeforeClass(alwaysRun = true) + public void setUpGlobal() throws Exception { + port1 = 8080; + _connector = new SelectChannelConnector(); + _connector.setPort(port1); + + addConnector(_connector); + WebSocketHandler _wsHandler = getWebSocketHandler(); + + setHandler(_wsHandler); + + start(); + log.info("Local HTTP server started successfully"); + } + + public abstract WebSocketHandler getWebSocketHandler() ; + + public abstract AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config); + +} diff --git a/src/test/java/com/ning/http/client/websocket/ByteMessageTest.java b/src/test/java/com/ning/http/client/websocket/ByteMessageTest.java new file mode 100644 index 0000000000..e391f64093 --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/ByteMessageTest.java @@ -0,0 +1,191 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.providers.netty.NettyWebSocket; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.Arrays; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertEquals; + +public abstract class ByteMessageTest extends AbstractBasicTest { + + private final class EchoByteWebSocket implements org.eclipse.jetty.websocket.WebSocket, org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage { + + private Connection connection; + + @Override + public void onOpen(Connection connection) { + this.connection = connection; + } + + @Override + public void onClose(int i, String s) { + connection.close(); + } + + @Override + public void onMessage(byte[] bytes, int i, int i1) { + try { + connection.sendMessage(bytes, i, i1); + } catch (IOException e) { + try { + connection.sendMessage("FAIL"); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + } + + @Override + public WebSocketHandler getWebSocketHandler() { + return new WebSocketHandler() { + @Override + public org.eclipse.jetty.websocket.WebSocket doWebSocketConnect(HttpServletRequest httpServletRequest, String s) { + return new EchoByteWebSocket(); + } + }; + } + + @Test + public void echoByte() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference text = new AtomicReference(new byte[0]); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketByteListener() { + + @Override + public void onOpen(WebSocket websocket) { + } + + @Override + public void onClose(WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + + @Override + public void onMessage(byte[] message) { + text.set(message); + latch.countDown(); + } + }).build()).get(); + + websocket.sendMessage("ECHO".getBytes()); + + latch.await(); + assertEquals(text.get(), "ECHO".getBytes()); + } + + @Test + public void echoTwoMessagesTest() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(2); + final AtomicReference text = new AtomicReference(null); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketByteListener() { + + @Override + public void onOpen(WebSocket websocket) { + } + + @Override + public void onClose(WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + + @Override + public void onMessage(byte[] message) { + if (text.get() == null) { + text.set(message); + } else { + byte[] n = new byte[text.get().length + message.length]; + System.arraycopy(text.get(), 0, n, 0, text.get().length); + System.arraycopy(message, 0, n, text.get().length, message.length); + text.set(n); + } + latch.countDown(); + } + }).build()).get(); + + websocket.sendMessage("ECHO".getBytes()).sendMessage("ECHO".getBytes()); + + latch.await(); + assertEquals(text.get(), "ECHOECHO".getBytes()); + } + + @Test + public void echoOnOpenMessagesTest() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(2); + final AtomicReference text = new AtomicReference(null); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketByteListener() { + + @Override + public void onOpen(WebSocket websocket) { + websocket.sendMessage("ECHO".getBytes()).sendMessage("ECHO".getBytes()); + } + + @Override + public void onClose(WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + + @Override + public void onMessage(byte[] message) { + if (text.get() == null) { + text.set(message); + } else { + byte[] n = new byte[text.get().length + message.length]; + System.arraycopy(text.get(), 0, n, 0, text.get().length); + System.arraycopy(message, 0, n, text.get().length, message.length); + text.set(n); + } + latch.countDown(); + } + }).build()).get(); + + latch.await(); + assertEquals(text.get(), "ECHOECHO".getBytes()); + } +} diff --git a/src/test/java/com/ning/http/client/websocket/TextMessageTest.java b/src/test/java/com/ning/http/client/websocket/TextMessageTest.java new file mode 100644 index 0000000000..ee4f2e27a6 --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/TextMessageTest.java @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import org.testng.annotations.Test; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static org.testng.Assert.assertEquals; + +public abstract class TextMessageTest extends AbstractBasicTest { + + private final class EchoTextWebSocket implements org.eclipse.jetty.websocket.WebSocket, org.eclipse.jetty.websocket.WebSocket.OnTextMessage { + + private Connection connection; + + @Override + public void onOpen(Connection connection) { + this.connection = connection; + } + + @Override + public void onClose(int i, String s) { + connection.close(); + } + + @Override + public void onMessage(String s) { + try { + connection.sendMessage(s); + } catch (IOException e) { + try { + connection.sendMessage("FAIL"); + } catch (IOException e1) { + e1.printStackTrace(); + } + } + } + } + + @Override + public WebSocketHandler getWebSocketHandler() { + return new WebSocketHandler() { + @Override + public org.eclipse.jetty.websocket.WebSocket doWebSocketConnect(HttpServletRequest httpServletRequest, String s) { + return new EchoTextWebSocket(); + } + }; + } + + @Override + public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) { + return null; //To change body of implemented methods use File | Settings | File Templates. + } + + @Test(timeOut = 60000) + public void onOpen() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference text = new AtomicReference(""); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() { + + @Override + public void onOpen(com.ning.http.client.websocket.WebSocket websocket) { + text.set("OnOpen"); + latch.countDown(); + } + + @Override + public void onClose(com.ning.http.client.websocket.WebSocket websocket) { + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + }).build()).get(); + + + latch.await(); + assertEquals(text.get(), "OnOpen"); + } + + @Test(timeOut = 60000) + public void onClose() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference text = new AtomicReference(""); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() { + + @Override + public void onOpen(com.ning.http.client.websocket.WebSocket websocket) { + } + + @Override + public void onClose(com.ning.http.client.websocket.WebSocket websocket) { + text.set("OnClose"); + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + }).build()).get(); + + websocket.close(); + + latch.await(); + assertEquals(text.get(), "OnClose"); + } + + @Test(timeOut = 60000) + public void echoText() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(1); + final AtomicReference text = new AtomicReference(""); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketTextListener() { + + @Override + public void onMessage(String message) { + text.set(message); + latch.countDown(); + } + + @Override + public void onOpen(com.ning.http.client.websocket.WebSocket websocket) { + } + + @Override + public void onClose(com.ning.http.client.websocket.WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + }).build()).get(); + + websocket.sendTextMessage("ECHO"); + + latch.await(); + assertEquals(text.get(), "ECHO"); + } + + @Test(timeOut = 60000) + public void echoDoubleListenerText() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(2); + final AtomicReference text = new AtomicReference(""); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketTextListener() { + + @Override + public void onMessage(String message) { + text.set(message); + latch.countDown(); + } + + @Override + public void onOpen(com.ning.http.client.websocket.WebSocket websocket) { + } + + @Override + public void onClose(com.ning.http.client.websocket.WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + }).addWebSocketListener(new WebSocketTextListener() { + + @Override + public void onMessage(String message) { + text.set(text.get() + message); + latch.countDown(); + } + + @Override + public void onOpen(com.ning.http.client.websocket.WebSocket websocket) { + } + + @Override + public void onClose(com.ning.http.client.websocket.WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + }).build()).get(); + + websocket.sendTextMessage("ECHO"); + + latch.await(); + assertEquals(text.get(), "ECHOECHO"); + } + + @Test + public void echoTwoMessagesTest() throws Throwable { + AsyncHttpClient c = getAsyncHttpClient(new AsyncHttpClientConfig.Builder().build()); + final CountDownLatch latch = new CountDownLatch(2); + final AtomicReference text = new AtomicReference(""); + + WebSocket websocket = c.prepareGet(getTargetUrl()) + .execute(new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketTextListener() { + + @Override + public void onMessage(String message) { + text.set(text.get() + message); + latch.countDown(); + } + + boolean t = false; + + @Override + public void onOpen(com.ning.http.client.websocket.WebSocket websocket) { + websocket.sendTextMessage("ECHO").sendTextMessage("ECHO"); + } + + @Override + public void onClose(com.ning.http.client.websocket.WebSocket websocket) { + latch.countDown(); + } + + @Override + public void onError(Throwable t) { + t.printStackTrace(); + latch.countDown(); + } + }).build()).get(); + + latch.await(); + assertEquals(text.get(), "ECHOECHO"); + } +} diff --git a/src/test/java/com/ning/http/client/websocket/grizzly/GrizzlyByteMessageText.java b/src/test/java/com/ning/http/client/websocket/grizzly/GrizzlyByteMessageText.java new file mode 100644 index 0000000000..38f597e866 --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/grizzly/GrizzlyByteMessageText.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket.grizzly; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.async.ProviderUtil; +import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider; +import com.ning.http.client.websocket.ByteMessageTest; + +public class GrizzlyByteMessageText extends ByteMessageTest { + @Override + public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) { + if (config == null) { + config = new AsyncHttpClientConfig.Builder().build(); + } + return new AsyncHttpClient(new GrizzlyAsyncHttpProvider(config), config); + } +} diff --git a/src/test/java/com/ning/http/client/websocket/grizzly/GrizzlyTextMessageText.java b/src/test/java/com/ning/http/client/websocket/grizzly/GrizzlyTextMessageText.java new file mode 100644 index 0000000000..ccce7a2f4d --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/grizzly/GrizzlyTextMessageText.java @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket.grizzly; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.async.ProviderUtil; +import com.ning.http.client.providers.grizzly.GrizzlyAsyncHttpProvider; +import com.ning.http.client.websocket.ByteMessageTest; + +public class GrizzlyTextMessageText extends ByteMessageTest { + @Override + public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) { + if (config == null) { + config = new AsyncHttpClientConfig.Builder().build(); + } + return new AsyncHttpClient(new GrizzlyAsyncHttpProvider(config), config); + } +} diff --git a/src/test/java/com/ning/http/client/websocket/netty/NettyByteMessageText.java b/src/test/java/com/ning/http/client/websocket/netty/NettyByteMessageText.java new file mode 100644 index 0000000000..f81b70614c --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/netty/NettyByteMessageText.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket.netty; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.async.ProviderUtil; +import com.ning.http.client.async.ZeroCopyFileTest; +import com.ning.http.client.websocket.ByteMessageTest; + +public class NettyByteMessageText extends ByteMessageTest { + @Override + public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) { + return ProviderUtil.nettyProvider(config); + } +} diff --git a/src/test/java/com/ning/http/client/websocket/netty/NettyTextMessageText.java b/src/test/java/com/ning/http/client/websocket/netty/NettyTextMessageText.java new file mode 100644 index 0000000000..564c681fa6 --- /dev/null +++ b/src/test/java/com/ning/http/client/websocket/netty/NettyTextMessageText.java @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2010-2011 Sonatype, Inc. All rights reserved. + * + * This program is licensed to you under the Apache License Version 2.0, + * and you may not use this file except in compliance with the Apache License Version 2.0. + * You may obtain a copy of the Apache License Version 2.0 at http://www.apache.org/licenses/LICENSE-2.0. + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the Apache License Version 2.0 is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the Apache License Version 2.0 for the specific language governing permissions and limitations there under. + */ +package com.ning.http.client.websocket.netty; + +import com.ning.http.client.AsyncHttpClient; +import com.ning.http.client.AsyncHttpClientConfig; +import com.ning.http.client.async.ProviderUtil; +import com.ning.http.client.websocket.ByteMessageTest; + +public class NettyTextMessageText extends ByteMessageTest { + @Override + public AsyncHttpClient getAsyncHttpClient(AsyncHttpClientConfig config) { + return ProviderUtil.nettyProvider(config); + } +}