From 0d32b61e5e4285211acf24129b06ae24f42e29e5 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 6 Jul 2021 13:39:54 -0300 Subject: [PATCH 1/6] android and ios RNEventSource --- android/build.gradle | 20 + android/src/main/AndroidManifest.xml | 4 + .../RNEventSource/RNEventSourceModule.java | 147 ++ .../RNEventSource/RNEventSourcePackage.java | 36 + .../gateway/bridge/ValidateOrigin.java | 26 + .../kaazing/gateway/client/impl/Channel.java | 64 + .../gateway/client/impl/CommandMessage.java | 26 + .../gateway/client/impl/DecoderInput.java | 31 + .../gateway/client/impl/DecoderListener.java | 32 + .../gateway/client/impl/EncoderOutput.java | 31 + .../kaazing/gateway/client/impl/Handler.java | 29 + .../gateway/client/impl/WebSocketChannel.java | 107 ++ .../gateway/client/impl/WebSocketHandler.java | 37 + .../client/impl/WebSocketHandlerAdapter.java | 78 ++ .../client/impl/WebSocketHandlerFactory.java | 28 + .../client/impl/WebSocketHandlerListener.java | 88 ++ .../client/impl/auth/AuthenticationUtil.java | 69 + .../client/impl/bridge/BridgeUtil.java | 191 +++ .../impl/bridge/ClassLoaderFactory.java | 67 + .../impl/bridge/HttpRequestBridgeHandler.java | 273 ++++ .../gateway/client/impl/bridge/Proxy.java | 79 ++ .../client/impl/bridge/ProxyListener.java | 30 + .../bridge/WebSocketNativeBridgeHandler.java | 201 +++ .../gateway/client/impl/bridge/XoaEvent.java | 118 ++ .../gateway/client/impl/http/HttpRequest.java | 183 +++ .../HttpRequestAuthenticationHandler.java | 340 +++++ .../impl/http/HttpRequestDelegateHandler.java | 209 +++ .../client/impl/http/HttpRequestEvent.java | 68 + .../client/impl/http/HttpRequestFactory.java | 31 + .../client/impl/http/HttpRequestHandler.java | 34 + .../impl/http/HttpRequestHandlerAdapter.java | 80 ++ .../impl/http/HttpRequestHandlerFactory.java | 29 + .../client/impl/http/HttpRequestListener.java | 49 + .../impl/http/HttpRequestLoggingHandler.java | 100 ++ .../impl/http/HttpRequestRedirectHandler.java | 154 ++ .../http/HttpRequestTransportHandler.java | 122 ++ .../client/impl/http/HttpRequestUtil.java | 66 + .../client/impl/http/HttpResponse.java | 84 ++ .../client/impl/util/WSCompositeURI.java | 113 ++ .../gateway/client/impl/util/WSURI.java | 70 + .../client/impl/util/WebSocketUtil.java | 113 ++ .../client/impl/ws/CloseCommandMessage.java | 57 + .../gateway/client/impl/ws/ReadyState.java | 29 + .../impl/ws/WebSocketCompositeChannel.java | 108 ++ .../impl/ws/WebSocketCompositeHandler.java | 432 ++++++ .../impl/ws/WebSocketHandshakeObject.java | 81 ++ .../impl/ws/WebSocketLoggingHandler.java | 148 ++ .../ws/WebSocketReAuthenticateHandler.java | 105 ++ .../impl/ws/WebSocketSelectedChannel.java | 49 + .../impl/ws/WebSocketSelectedHandler.java | 30 + .../impl/ws/WebSocketSelectedHandlerImpl.java | 217 +++ .../impl/ws/WebSocketTransportHandler.java | 156 +++ .../client/impl/wseb/CreateChannel.java | 56 + .../client/impl/wseb/CreateHandler.java | 34 + .../impl/wseb/CreateHandlerFactory.java | 26 + .../client/impl/wseb/CreateHandlerImpl.java | 217 +++ .../impl/wseb/CreateHandlerListener.java | 31 + .../client/impl/wseb/DownstreamChannel.java | 72 + .../client/impl/wseb/DownstreamHandler.java | 32 + .../impl/wseb/DownstreamHandlerFactory.java | 26 + .../impl/wseb/DownstreamHandlerImpl.java | 392 ++++++ .../impl/wseb/DownstreamHandlerListener.java | 36 + .../client/impl/wseb/UpstreamChannel.java | 51 + .../client/impl/wseb/UpstreamHandler.java | 36 + .../impl/wseb/UpstreamHandlerFactory.java | 26 + .../client/impl/wseb/UpstreamHandlerImpl.java | 225 +++ .../impl/wseb/UpstreamHandlerListener.java | 29 + .../impl/wseb/WebSocketEmulatedChannel.java | 46 + .../impl/wseb/WebSocketEmulatedDecoder.java | 30 + .../wseb/WebSocketEmulatedDecoderImpl.java | 187 +++ .../WebSocketEmulatedDecoderListener.java | 33 + .../impl/wseb/WebSocketEmulatedEncoder.java | 32 + .../wseb/WebSocketEmulatedEncoderImpl.java | 61 + .../impl/wseb/WebSocketEmulatedHandler.java | 341 +++++ .../WebSocketNativeAuthenticationHandler.java | 182 +++ .../wsn/WebSocketNativeBalancingHandler.java | 307 ++++ .../impl/wsn/WebSocketNativeChannel.java | 63 + .../client/impl/wsn/WebSocketNativeCodec.java | 114 ++ .../wsn/WebSocketNativeDelegateHandler.java | 206 +++ .../impl/wsn/WebSocketNativeEncoder.java | 33 + .../impl/wsn/WebSocketNativeEncoderImpl.java | 200 +++ .../impl/wsn/WebSocketNativeHandler.java | 149 ++ .../wsn/WebSocketNativeHandshakeHandler.java | 388 +++++ .../client/transport/AuthenticateEvent.java | 55 + .../client/transport/BridgeDelegate.java | 26 + .../gateway/client/transport/CloseEvent.java | 58 + .../gateway/client/transport/ErrorEvent.java | 43 + .../gateway/client/transport/Event.java | 78 ++ .../client/transport/IoBufferUtil.java | 52 + .../gateway/client/transport/LoadEvent.java | 50 + .../client/transport/MessageEvent.java | 84 ++ .../gateway/client/transport/OpenEvent.java | 65 + .../client/transport/ProgressEvent.java | 66 + .../transport/ReadyStateChangedEvent.java | 29 + .../client/transport/RedirectEvent.java | 55 + .../transport/http/HttpRequestDelegate.java | 43 + .../http/HttpRequestDelegateFactory.java | 28 + .../http/HttpRequestDelegateImpl.java | 364 +++++ .../http/HttpRequestDelegateListener.java | 40 + .../transport/http/HttpRequestUtil.java | 70 + .../client/transport/ws/Base64Util.java | 179 +++ .../client/transport/ws/BridgeSocket.java | 40 + .../transport/ws/BridgeSocketFactory.java | 30 + .../client/transport/ws/BridgeSocketImpl.java | 80 ++ .../client/transport/ws/FrameProcessor.java | 209 +++ .../transport/ws/FrameProcessorListener.java | 30 + .../transport/ws/WebSocketDelegate.java | 40 + .../ws/WebSocketDelegateFactory.java | 30 + .../transport/ws/WebSocketDelegateImpl.java | 1066 ++++++++++++++ .../ws/WebSocketDelegateListener.java | 40 + .../transport/ws/WsFrameEncodingSupport.java | 234 ++++ .../client/transport/ws/WsMessage.java | 58 + .../gateway/client/util/Base64Util.java | 179 +++ .../gateway/client/util/GenericURI.java | 95 ++ .../kaazing/gateway/client/util/HexUtil.java | 95 ++ .../kaazing/gateway/client/util/HttpURI.java | 66 + .../gateway/client/util/StringUtils.java | 122 ++ .../kaazing/gateway/client/util/URIUtils.java | 62 + .../client/util/WrappedByteBuffer.java | 1247 +++++++++++++++++ .../util/auth/LoginHandlerProvider.java | 40 + .../main/java/org/kaazing/net/URLFactory.java | 206 +++ .../net/URLStreamHandlerFactorySpi.java | 54 + .../net/auth/BasicChallengeHandler.java | 108 ++ .../kaazing/net/auth/ChallengeHandler.java | 118 ++ .../kaazing/net/auth/ChallengeRequest.java | 118 ++ .../kaazing/net/auth/ChallengeResponse.java | 95 ++ .../net/auth/DispatchChallengeHandler.java | 116 ++ .../org/kaazing/net/auth/LoginHandler.java | 63 + .../net/auth/NegotiableChallengeHandler.java | 98 ++ .../net/auth/NegotiateChallengeHandler.java | 96 ++ .../kaazing/net/http/HttpRedirectPolicy.java | 317 +++++ .../auth/BasicChallengeResponseFactory.java | 40 + .../auth/DefaultBasicChallengeHandler.java | 122 ++ .../auth/DefaultDispatchChallengeHandler.java | 778 ++++++++++ .../org/kaazing/net/impl/auth/RealmUtils.java | 65 + .../net/impl/util/BlockingQueueImpl.java | 137 ++ .../kaazing/net/impl/util/ResumableTimer.java | 133 ++ .../org/kaazing/net/sse/SseEventReader.java | 97 ++ .../org/kaazing/net/sse/SseEventSource.java | 96 ++ .../net/sse/SseEventSourceFactory.java | 97 ++ .../org/kaazing/net/sse/SseEventType.java | 26 + .../org/kaazing/net/sse/SseException.java | 37 + .../sse/impl/DefaultEventSourceFactory.java | 85 ++ .../net/sse/impl/SseEventReaderImpl.java | 205 +++ .../net/sse/impl/SseEventSourceImpl.java | 328 +++++ .../kaazing/net/sse/impl/SseEventStream.java | 362 +++++ .../net/sse/impl/SseEventStreamListener.java | 30 + .../org/kaazing/net/sse/impl/SsePayload.java | 53 + .../net/sse/impl/SseURLConnection.java | 100 ++ .../net/sse/impl/SseURLConnectionImpl.java | 281 ++++ .../net/sse/impl/legacy/EventSource.java | 123 ++ .../sse/impl/legacy/EventSourceAdapter.java | 47 + .../net/sse/impl/legacy/EventSourceEvent.java | 82 ++ .../net/sse/impl/legacy/EventSourceImpl.java | 180 +++ .../sse/impl/legacy/EventSourceListener.java | 53 + .../SseURLStreamHandlerFactorySpiImpl.java | 50 + .../sse/impl/url/SseURLStreamHandlerImpl.java | 79 ++ .../java/org/kaazing/net/ws/WebSocket.java | 433 ++++++ .../kaazing/net/ws/WebSocketException.java | 86 ++ .../kaazing/net/ws/WebSocketExtension.java | 315 +++++ .../org/kaazing/net/ws/WebSocketFactory.java | 211 +++ .../net/ws/WebSocketMessageReader.java | 124 ++ .../kaazing/net/ws/WebSocketMessageType.java | 31 + .../net/ws/WebSocketMessageWriter.java | 61 + .../org/kaazing/net/ws/WsURLConnection.java | 425 ++++++ .../net/ws/impl/DefaultWebSocketFactory.java | 219 +++ .../kaazing/net/ws/impl/WebSocketImpl.java | 1188 ++++++++++++++++ .../WsExtensionParameterValuesSpiImpl.java | 68 + .../net/ws/impl/WsURLConnectionImpl.java | 367 +++++ .../net/ws/impl/io/WsInputStreamImpl.java | 172 +++ .../net/ws/impl/io/WsMessagePullParser.java | 88 ++ .../ws/impl/io/WsMessageReaderAdapter.java | 82 ++ .../net/ws/impl/io/WsMessageReaderImpl.java | 187 +++ .../net/ws/impl/io/WsMessageWriterImpl.java | 76 + .../net/ws/impl/io/WsOutputStreamImpl.java | 96 ++ .../kaazing/net/ws/impl/io/WsReaderImpl.java | 149 ++ .../kaazing/net/ws/impl/io/WsWriterImpl.java | 119 ++ .../spi/WebSocketExtensionFactorySpi.java | 64 + .../spi/WebSocketExtensionHandlerSpi.java | 108 ++ .../WebSocketExtensionParameterValuesSpi.java | 60 + .../ws/impl/spi/WebSocketExtensionSpi.java | 52 + .../url/WsURLStreamHandlerFactorySpiImpl.java | 78 ++ .../ws/impl/url/WsURLStreamHandlerImpl.java | 97 ++ .../WssURLStreamHandlerFactorySpiImpl.java | 46 + .../ws/impl/url/WssURLStreamHandlerImpl.java | 39 + ...org.kaazing.net.URLStreamHandlerFactorySpi | 2 + ...org.kaazing.net.auth.BasicChallengeHandler | 1 + ....kaazing.net.auth.DispatchChallengeHandler | 1 + .../org.kaazing.net.sse.SseEventSourceFactory | 1 + .../org.kaazing.net.ws.WebSocketFactory | 1 + ios/RNEventSource.xcodeproj/project.pbxproj | 391 ++++++ ios/RNEventSource/RNEventSource.h | 13 + ios/RNEventSource/RNEventSource.m | 97 ++ .../TRVSEventSource/TRVSEventSource.h | 107 ++ .../TRVSEventSource/TRVSEventSource.m | 294 ++++ .../TRVSEventSource/TRVSEventSourceDelegate.h | 46 + .../TRVSEventSource/TRVSServerSentEvent.h | 35 + .../TRVSEventSource/TRVSServerSentEvent.m | 71 + 198 files changed, 25054 insertions(+) create mode 100644 android/build.gradle create mode 100644 android/src/main/AndroidManifest.xml create mode 100644 android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java create mode 100644 android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourcePackage.java create mode 100755 android/src/main/java/org/kaazing/gateway/bridge/ValidateOrigin.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/Channel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/CommandMessage.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/DecoderInput.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/DecoderListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/EncoderOutput.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/Handler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/WebSocketChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerAdapter.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/auth/AuthenticationUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/BridgeUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/ClassLoaderFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/HttpRequestBridgeHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/Proxy.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/ProxyListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/WebSocketNativeBridgeHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/bridge/XoaEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequest.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestAuthenticationHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestDelegateHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerAdapter.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestLoggingHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestRedirectHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestTransportHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/http/HttpResponse.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/util/WSCompositeURI.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/util/WSURI.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/util/WebSocketUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/CloseCommandMessage.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/ReadyState.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketHandshakeObject.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketLoggingHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketReAuthenticateHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandlerImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketTransportHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoder.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoder.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoderImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeAuthenticationHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeBalancingHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeChannel.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeCodec.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeDelegateHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoder.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoderImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandshakeHandler.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/AuthenticateEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/BridgeDelegate.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/CloseEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ErrorEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/Event.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/IoBufferUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/LoadEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/MessageEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/OpenEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ProgressEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ReadyStateChangedEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/RedirectEvent.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegate.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/Base64Util.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocket.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessor.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessorListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegate.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateFactory.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateImpl.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateListener.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/WsFrameEncodingSupport.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/transport/ws/WsMessage.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/Base64Util.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/GenericURI.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/HexUtil.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/HttpURI.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/StringUtils.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/URIUtils.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/WrappedByteBuffer.java create mode 100755 android/src/main/java/org/kaazing/gateway/client/util/auth/LoginHandlerProvider.java create mode 100755 android/src/main/java/org/kaazing/net/URLFactory.java create mode 100755 android/src/main/java/org/kaazing/net/URLStreamHandlerFactorySpi.java create mode 100755 android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java create mode 100755 android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java create mode 100755 android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/auth/LoginHandler.java create mode 100755 android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java create mode 100755 android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java create mode 100755 android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java create mode 100755 android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java create mode 100755 android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java create mode 100755 android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java create mode 100755 android/src/main/java/org/kaazing/net/sse/SseEventReader.java create mode 100755 android/src/main/java/org/kaazing/net/sse/SseEventSource.java create mode 100755 android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java create mode 100755 android/src/main/java/org/kaazing/net/sse/SseEventType.java create mode 100755 android/src/main/java/org/kaazing/net/sse/SseException.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SseEventSourceImpl.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SseEventStream.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SseEventStreamListener.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SsePayload.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SseURLConnection.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/SseURLConnectionImpl.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSource.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceAdapter.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceEvent.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceImpl.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceListener.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerFactorySpiImpl.java create mode 100755 android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocket.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocketException.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocketExtension.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocketFactory.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocketMessageReader.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocketMessageType.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WebSocketMessageWriter.java create mode 100755 android/src/main/java/org/kaazing/net/ws/WsURLConnection.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/DefaultWebSocketFactory.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/WebSocketImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/WsExtensionParameterValuesSpiImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/WsURLConnectionImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsInputStreamImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsMessagePullParser.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderAdapter.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageWriterImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsOutputStreamImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsReaderImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/io/WsWriterImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionFactorySpi.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionHandlerSpi.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionParameterValuesSpi.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionSpi.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerFactorySpiImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerFactorySpiImpl.java create mode 100755 android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerImpl.java create mode 100755 android/src/main/resources/META-INF/services/org.kaazing.net.URLStreamHandlerFactorySpi create mode 100755 android/src/main/resources/META-INF/services/org.kaazing.net.auth.BasicChallengeHandler create mode 100755 android/src/main/resources/META-INF/services/org.kaazing.net.auth.DispatchChallengeHandler create mode 100755 android/src/main/resources/META-INF/services/org.kaazing.net.sse.SseEventSourceFactory create mode 100755 android/src/main/resources/META-INF/services/org.kaazing.net.ws.WebSocketFactory create mode 100644 ios/RNEventSource.xcodeproj/project.pbxproj create mode 100644 ios/RNEventSource/RNEventSource.h create mode 100644 ios/RNEventSource/RNEventSource.m create mode 100755 ios/RNEventSource/TRVSEventSource/TRVSEventSource.h create mode 100755 ios/RNEventSource/TRVSEventSource/TRVSEventSource.m create mode 100755 ios/RNEventSource/TRVSEventSource/TRVSEventSourceDelegate.h create mode 100755 ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.h create mode 100755 ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.m diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..2f2fcd3 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,20 @@ +def safeExtGet(name, fallback) { + rootProject.ext.has(name) ? rootProject.ext.get(name) : fallback +} + +apply plugin: "com.android.library" + +android { + compileSdkVersion safeExtGet("compileSdkVersion", 28) + + defaultConfig { + minSdkVersion safeExtGet("minSdkVersion", 16) + targetSdkVersion safeExtGet("targetSdkVersion", 28) + versionCode 1 + versionName "1.0" + } +} + +dependencies { + implementation "com.facebook.react:react-native:+" +} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..9585048 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java b/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java new file mode 100644 index 0000000..f5ac4e3 --- /dev/null +++ b/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java @@ -0,0 +1,147 @@ +package com.github.jonnybgod.RNEventSource; + +import java.io.IOException; + +import com.facebook.common.logging.FLog; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.common.ReactConstants; +import com.facebook.react.modules.core.DeviceEventManagerModule; + +import java.net.URI; +import java.net.URL; + +import org.kaazing.net.sse.SseEventReader; +import org.kaazing.net.sse.SseEventSource; +import org.kaazing.net.sse.SseEventSourceFactory; +import org.kaazing.net.sse.SseEventType; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class RNEventSourceModule extends ReactContextBaseJavaModule { + + private Map mEventSourceConnections = new HashMap<>(); + private Map mEventReaderThreads = new HashMap<>(); + + private SseEventSourceFactory factory = SseEventSourceFactory.createEventSourceFactory(); + private ReactContext mReactContext; + + public RNEventSourceModule(ReactApplicationContext context) { + super(context); + mReactContext = context; + } + + private void sendEvent(String eventName, WritableMap params) { + mReactContext + .getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class) + .emit(eventName, params); + } + + @Override + public String getName() { + return "RNEventSource"; + } + + @ReactMethod + public void connect(final String url, final int id) { + try { + + final SseEventSource source = factory.createEventSource(new URI(url)); + + source.connect(); + + mEventSourceConnections.put(id, source); + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + sendEvent("eventsourceOpen", params); + + Thread sseEventReaderThread = new Thread() { + public void run() { + try { + SseEventReader reader = source.getEventReader(); + + SseEventType type = null; + while ((type = reader.next()) != SseEventType.EOS) { + switch (type) { + case DATA: + String name; + String data; + try { + name = reader.getName(); + data = reader.getData().toString(); + } catch (IOException e) { + notifyEventSourceFailed(id, e.getMessage()); + return; + } + + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("type", name != null ? name : "message"); + params.putString("data", data); + sendEvent("eventsourceEvent", params); + break; + case EMPTY: + break; + } + } + + notifyEventSourceFailed(id, "Connection with the event source was closed."); + close(id); + } + catch (Exception e) { + notifyEventSourceFailed(id, e.getMessage()); + + Thread.currentThread().interrupt(); + mEventReaderThreads.remove(id); + return; + } + } + }; + + sseEventReaderThread.start(); + mEventReaderThreads.put(id, sseEventReaderThread); + } + catch (Exception e) { + notifyEventSourceFailed(id, e.getMessage()); + } + } + + @ReactMethod + public void close(int id) { + SseEventSource source = mEventSourceConnections.get(id); + Thread thead = mEventReaderThreads.get(id); + if (source == null) { + // EventSource is already closed + // Don't do anything, mirror the behaviour on web + FLog.w( + ReactConstants.TAG, + "Cannot close EventSource. Unknown EventSource id " + id); + + return; + } + try { + thead.interrupt(); + source.close(); + mEventSourceConnections.remove(id); + mEventReaderThreads.remove(id); + } catch (Exception e) { + FLog.e( + ReactConstants.TAG, + "Could not close EventSource connection for id " + id, + e); + } + } + + private void notifyEventSourceFailed(int id, String message) { + WritableMap params = Arguments.createMap(); + params.putInt("id", id); + params.putString("message", message); + sendEvent("eventsourceFailed", params); + } +} \ No newline at end of file diff --git a/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourcePackage.java b/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourcePackage.java new file mode 100644 index 0000000..67bc65c --- /dev/null +++ b/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourcePackage.java @@ -0,0 +1,36 @@ +package com.github.jonnybgod.RNEventSource; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; +import com.facebook.react.bridge.JavaScriptModule; + +public class RNEventSourcePackage implements ReactPackage { + + public RNEventSourcePackage() {} + + @Override + public List createNativeModules( + ReactApplicationContext reactContext) { + List modules = new ArrayList<>(); + + modules.add(new RNEventSourceModule(reactContext)); + + return modules; + } + + public List> createJSModules() { + return Collections.emptyList(); + } + + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/bridge/ValidateOrigin.java b/android/src/main/java/org/kaazing/gateway/bridge/ValidateOrigin.java new file mode 100755 index 0000000..6dbe9e3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/bridge/ValidateOrigin.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.bridge; + +final class ValidateOrigin { + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/Channel.java b/android/src/main/java/org/kaazing/gateway/client/impl/Channel.java new file mode 100755 index 0000000..8fb92bf --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/Channel.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import java.util.concurrent.atomic.AtomicLong; + +import org.kaazing.net.auth.ChallengeResponse; + +public class Channel { + public static final String HEADER_SEQUENCE = "X-Sequence-No"; + + // TODO: This is an abstration violation - authentication should not be exposed on Channel + /** Authentication data */ + public ChallengeResponse challengeResponse = new ChallengeResponse(null, null); + public boolean authenticationReceived = false; + public boolean preventFallback = false; + + private Channel parent; + private final AtomicLong sequence; + + public Channel() { + this(0); + } + + public Channel(long sequence) { + this.sequence = new AtomicLong(sequence); + } + + public void setParent(Channel parent) { + this.parent = parent; + } + + public Channel getParent() { + return parent; + } + + public long nextSequence() { + return this.sequence.getAndIncrement(); + } + + @Override + public String toString() { + return "[" + this.getClass().getSimpleName() + "]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/CommandMessage.java b/android/src/main/java/org/kaazing/gateway/client/impl/CommandMessage.java new file mode 100755 index 0000000..7598366 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/CommandMessage.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +public interface CommandMessage { + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/DecoderInput.java b/android/src/main/java/org/kaazing/gateway/client/impl/DecoderInput.java new file mode 100755 index 0000000..7c30cb3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/DecoderInput.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + + +public interface DecoderInput { + + WrappedByteBuffer read(C channel); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/DecoderListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/DecoderListener.java new file mode 100755 index 0000000..e270774 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/DecoderListener.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + + +public interface DecoderListener { + + void messageDecoded(C channel, WrappedByteBuffer message); + void messageDecoded(C channel, String message); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/EncoderOutput.java b/android/src/main/java/org/kaazing/gateway/client/impl/EncoderOutput.java new file mode 100755 index 0000000..3350534 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/EncoderOutput.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + + +public interface EncoderOutput { + + void write(C channel, WrappedByteBuffer buf); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/Handler.java b/android/src/main/java/org/kaazing/gateway/client/impl/Handler.java new file mode 100755 index 0000000..3ccb4e9 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/Handler.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +/** + * Handler marker class + */ +public interface Handler { + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketChannel.java new file mode 100755 index 0000000..091c14e --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketChannel.java @@ -0,0 +1,107 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.net.http.HttpRedirectPolicy; + +public class WebSocketChannel extends Channel { + static volatile int nextId = 1; + + public WebSocketHandler transportHandler; + public StringBuilder handshakePayload; + + final int id; + + protected int bufferedAmount = 0; + + private WSURI location; + private String selectedProtocol; + private String negotiatedExtensions; + private String enabledExtensions; + private HttpRedirectPolicy followRedirect; + + public WebSocketChannel(WSURI location) { + this.id = nextId++; + + this.location = location; + this.handshakePayload = new StringBuilder(); + } + + /** + * The number of bytes queued to be sent + */ + public int getBufferedAmount() { + return this.bufferedAmount; + } + + public void setLocation(WSURI location) { + this.location = location; + } + + public WSURI getLocation() { + return location; + } + + public String getEnabledExtensions() { + return enabledExtensions; + } + + public void setEnabledExtensions(String extensions) { + this.enabledExtensions = extensions; + } + + public String getNegotiatedExtensions() { + return negotiatedExtensions; + } + + public void setNegotiatedExtensions(String extensions) { + this.negotiatedExtensions = extensions; + } + + public void setProtocol(String protocol) { + this.selectedProtocol = protocol; + } + + public String getProtocol() { + return selectedProtocol; + } + + public HttpRedirectPolicy getFollowRedirect() { + return followRedirect; + } + + public void setFollowRedirect(HttpRedirectPolicy redirectOption) { + this.followRedirect = redirectOption; + } + + @Override + public String toString() { + String className = getClass().getSimpleName(); + if (className == null) { + className = WebSocketChannel.class.getSimpleName(); + } + return "["+className+" "+id+": "+location + "]"; + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandler.java new file mode 100755 index 0000000..350c291 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandler.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public interface WebSocketHandler extends Handler { + + void processConnect(WebSocketChannel channel, WSURI location, String[] protocols); + void processAuthorize(WebSocketChannel channel, String authorizeToken); + void processTextMessage(WebSocketChannel channel, String text); + void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer buffer); + void processClose(WebSocketChannel channel, int code, String reason); + void setListener(WebSocketHandlerListener listener); + void setIdleTimeout(WebSocketChannel channel, int timeout); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerAdapter.java b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerAdapter.java new file mode 100755 index 0000000..e818da4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerAdapter.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketHandlerAdapter implements WebSocketHandler { + + protected WebSocketHandler nextHandler; + protected WebSocketHandlerListener listener; + + /** + * Process connect request to uri and protocol specified + */ + @Override + public void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + nextHandler.processConnect(channel, location, protocols); + } + + /** + * Send authorize token to the Gateway + */ + @Override + public synchronized void processAuthorize(WebSocketChannel channel, String authorizeToken) { + nextHandler.processAuthorize(channel, authorizeToken); + } + + @Override + public void processTextMessage(WebSocketChannel channel, String text) { + nextHandler.processTextMessage(channel, text); + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer buffer) { + nextHandler.processBinaryMessage(channel, buffer); + } + + /** + * Disconnect the WebSocket + */ + @Override + public synchronized void processClose(WebSocketChannel channel, int code, String reason) { + nextHandler.processClose(channel, code, reason); + } + @Override + public void setListener(WebSocketHandlerListener listener) { + this.listener = listener; + } + + @Override + public synchronized void setIdleTimeout(WebSocketChannel channel, int timeout) { + nextHandler.setIdleTimeout(channel, timeout); + } + + public void setNextHandler(WebSocketHandler handler) { + this.nextHandler = handler; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerFactory.java new file mode 100755 index 0000000..83f9913 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerFactory.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +public interface WebSocketHandlerFactory { + + WebSocketHandler createWebSocketHandler(); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerListener.java new file mode 100755 index 0000000..e50b1e1 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/WebSocketHandlerListener.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + + +/** + * Listener for inbound handler messages (coming from the network) + */ +public interface WebSocketHandlerListener { + + /** + * This method is called when the WebSocket is opened + * @param channel + */ + void connectionOpened(WebSocketChannel channel, String protocol); + + /** + * This method is called when the WebSocket is closed + * @param channel + * @param wasClean TODO + * @param code TODO + * @param reason TODO + */ + void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason); + + void connectionClosed(WebSocketChannel channel, Exception ex); + + /** + * This method is called when a connection fails + * @param channel + */ + void connectionFailed(WebSocketChannel channel, Exception ex); + + /** + * This method is called when a redirect response is + * @param channel + * @param location new location for redirect + */ + void redirected(WebSocketChannel channel, String location); + + /** + * This method is called when authentication is requested + * @param channel + */ + void authenticationRequested(WebSocketChannel channel, String location, String challenge); + + /** + * This method is called when a text message is received on the WebSocket channel + * @param channel + * @param message + */ + void textMessageReceived(WebSocketChannel channel, String message); + + /** + * This method is called when a binary message is received on the WebSocket channel + * @param channel + * @param buf + */ + void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf); + + /** + * This method is called when a command message is received on the WebSocket channel + * @param channel + * @param message + */ + void commandMessageReceived(WebSocketChannel channel, CommandMessage message); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/auth/AuthenticationUtil.java b/android/src/main/java/org/kaazing/gateway/client/impl/auth/AuthenticationUtil.java new file mode 100755 index 0000000..8744856 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/auth/AuthenticationUtil.java @@ -0,0 +1,69 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.auth; + +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.auth.ChallengeRequest; +import org.kaazing.net.auth.ChallengeResponse; + +public final class AuthenticationUtil { + + private static final String CLASS_NAME = AuthenticationUtil.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private AuthenticationUtil() { + + } + + public static ChallengeResponse getChallengeResponse(WebSocketChannel channel, ChallengeRequest challengeRequest, ChallengeResponse challengeResponse) { + LOG.entering(CLASS_NAME, "getChallengeResponse"); + + ChallengeHandler challengeHandler = null; + + if (challengeResponse.getNextChallengeHandler() == null) { + if (((WebSocketCompositeChannel)channel.getParent()) != null) { + challengeHandler = ((WebSocketCompositeChannel)channel.getParent()).getChallengeHandler(); + } + } else { + challengeHandler = challengeResponse.getNextChallengeHandler(); + } + + if (challengeHandler == null) { + throw new IllegalStateException("No challenge handler available for challenge " + challengeRequest); + } + + try { + challengeResponse = challengeHandler.handle(challengeRequest); + } catch (Exception e) { + throw new IllegalStateException("Unexpected error processing challenge: "+challengeRequest, e); + } + + if (challengeResponse == null) { + throw new IllegalStateException("Unsupported challenge " + challengeRequest); + } + return challengeResponse; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/BridgeUtil.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/BridgeUtil.java new file mode 100755 index 0000000..f46fe41 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/BridgeUtil.java @@ -0,0 +1,191 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.bridge; + +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.net.URI; +import java.net.URL; +import java.security.SecureRandom; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.bridge.XoaEvent.XoaEventKind; +import org.kaazing.gateway.client.util.StringUtils; + +public class BridgeUtil { + private static final String CLASS_NAME = BridgeUtil.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final String SOA_MESSAGE = "soaMessage"; + private static final String XOP_MESSAGE = "xopMessage"; + + private static Map schemeAuthorityToXopMap = new ConcurrentHashMap(); + private static Map handlerIdToHtml5ObjectMap = new ConcurrentHashMap(); + + private static AtomicInteger sHtml5ObjectIdCounter = new AtomicInteger(new SecureRandom().nextInt(10000)); + + // package private static used for authenticating self for same origin code + static Object token = null; + + public static Object getIdentifier() { + LOG.exiting(CLASS_NAME, "getIdentifier", token); + return token; + } + + static void processEvent(XoaEvent event) { + LOG.entering(CLASS_NAME, "dispatchEventToXoa", event); + LOG.log(Level.FINEST, "SOA --> XOA: {1}", event); + + Integer handlerId = event.getHandlerId(); + if (handlerId == null) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Null handlerId"); + } + return; + } + + String eventType = event.getKind().toString(); + Object[] params = event.getParams(); + Object[] args = { handlerId, eventType, params }; + PropertyChangeSupport xop = getCrossOriginProxy(handlerId); + if (xop == null) { + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("Null xop for handler " + handlerId); + } + return; + } + + xop.firePropertyChange(SOA_MESSAGE, null, args); + } + + public static void eventReceived(final Integer handlerId, final String eventType, final Object[] params) { + LOG.entering(CLASS_NAME, "eventReceived", new Object[] { handlerId, eventType, params }); + final Proxy obj = handlerIdToHtml5ObjectMap.get(handlerId); + if (obj == null) { + LOG.fine("Object by id: " + handlerId + " could not be located in the system"); + return; + } + + try { + XoaEventKind name = XoaEventKind.getName(eventType); + obj.eventReceived(handlerId, name, params); + } + finally { + if (eventType.equals(XoaEvent.XoaEventKind.CLOSED) || eventType.equals(XoaEvent.XoaEventKind.ERROR)) { + handlerIdToHtml5ObjectMap.remove(handlerId); + } + } + } + + private static String getSchemeAuthority(URI uri) { + return uri.getScheme() + "_" + uri.getAuthority(); + } + + private static PropertyChangeSupport getCrossOriginProxy(Integer handlerId) { + Proxy proxy = handlerIdToHtml5ObjectMap.get(handlerId); + return getCrossOriginProxy(proxy); + } + + private static PropertyChangeSupport getCrossOriginProxy(Proxy proxy) { + return getCrossOriginProxy(proxy.getUri()); + } + + private static PropertyChangeSupport getCrossOriginProxy(URI uri) { + String schemeAuthority = getSchemeAuthority(uri); + return getCrossOriginProxy(schemeAuthority); + } + + private static PropertyChangeSupport getCrossOriginProxy(String schemeAuthority) { + return schemeAuthorityToXopMap.get(schemeAuthority); + } + + private static void initCrossOriginProxy(URI uri) throws Exception { + LOG.entering(CLASS_NAME, "initCrossOriginProxy", new Object[] { uri }); + + PropertyChangeSupport xop = getCrossOriginProxy(uri); + if (xop == null) { + try { + String scheme = uri.getScheme(); + String jarUrl = scheme + "://" + uri.getAuthority(); + + if (scheme.equals("ws")) { + jarUrl = jarUrl.replace("ws:", "http:"); + } else if (scheme.equals("wss")) { + jarUrl = jarUrl.replace("wss:", "https:"); + } + + ClassLoaderFactory classLoaderFactory = ClassLoaderFactory.getInstance(); + jarUrl += classLoaderFactory.getQueryParameters(); + + final String jarFileUrl = jarUrl; + LOG.finest("jarFileUrl = " + StringUtils.stripControlCharacters(jarFileUrl)); + + ClassLoader loader = classLoaderFactory.createClassLoader(new URL(jarFileUrl), BridgeUtil.class.getClassLoader()); + + LOG.finest("Created remote proxy class loader: " + loader); + + Class remoteProxyClass = loader.loadClass(classLoaderFactory.getCrossOriginProxyClass()); + + xop = (PropertyChangeSupport) remoteProxyClass.newInstance(); + xop.addPropertyChangeListener(XOP_MESSAGE, new PropertyChangeListener() { + @Override + public void propertyChange(PropertyChangeEvent evt) { + Object[] args = (Object[]) evt.getNewValue(); + Integer proxyId = (Integer) args[0]; + String eventType = (String) args[1]; + Object[] params = (Object[]) args[2]; + eventReceived(proxyId, eventType, params); + } + }); + + schemeAuthorityToXopMap.put(getSchemeAuthority(uri), xop); + } catch (Exception e) { + String reason = "Unable to connect: the Gateway may not be running, a network route may be unavailable, or the Gateway may not be configured properly"; + LOG.log(Level.WARNING, reason); + LOG.log(Level.FINEST, reason, e); + throw new Exception(reason); + } + } + + LOG.exiting(CLASS_NAME, "initCrossOriginProxy", xop); + } + + static Proxy createProxy(URI uri, ProxyListener listener) throws Exception { + LOG.entering(CLASS_NAME, "registerProxy", new Object[] { }); + + BridgeUtil.initCrossOriginProxy(uri); + Integer handlerId = sHtml5ObjectIdCounter.getAndIncrement(); + + Proxy proxy = new Proxy(); + proxy.setHandlerId(handlerId); + proxy.setUri(uri); + proxy.setListener(listener); + + handlerIdToHtml5ObjectMap.put(handlerId, proxy); + return proxy; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/ClassLoaderFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/ClassLoaderFactory.java new file mode 100755 index 0000000..ab955c5 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/ClassLoaderFactory.java @@ -0,0 +1,67 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.bridge; + +import java.net.URL; +import java.net.URLClassLoader; + +public abstract class ClassLoaderFactory { + + private static ClassLoaderFactory sharedInstance; + + static { + sharedInstance = new DefaultClassLoaderFactory(); + } + + public abstract ClassLoader createClassLoader(URL url, ClassLoader parent) throws Exception; + + public abstract String getQueryParameters(); + + public abstract String getCrossOriginProxyClass(); + + public static final void setInstance(ClassLoaderFactory factory) { + sharedInstance = factory; + } + + public static ClassLoaderFactory getInstance() { + return sharedInstance; + } + + private static class DefaultClassLoaderFactory extends ClassLoaderFactory{ + + @Override + public ClassLoader createClassLoader(URL url, ClassLoader parent) throws Exception { + URL[] urls = { url }; + return URLClassLoader.newInstance(urls, parent); + } + + @Override + public String getQueryParameters() { + return "?.kr=xsj"; //"?.kv=10.05&.kr=xsj"; + } + + @Override + public String getCrossOriginProxyClass() { + return "org.kaazing.gateway.bridge.CrossOriginProxy"; + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/HttpRequestBridgeHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/HttpRequestBridgeHandler.java new file mode 100755 index 0000000..0831bc4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/HttpRequestBridgeHandler.java @@ -0,0 +1,273 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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. + */ + +/** + * Copyright (c) 2007-2011, Kaazing Corporation. All rights reserved. + */ + +package org.kaazing.gateway.client.impl.bridge; + +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.bridge.XoaEvent.XoaEventKind; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestListener; +import org.kaazing.gateway.client.impl.http.HttpRequestUtil; +import org.kaazing.gateway.client.impl.http.HttpResponse; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +/* + * WebSocket Emulated Handler Chain + * EmulateHandler + * |- CreateHandler - HttpRequestAuthenticationHandler - HttpRequestRedirectHandler - {HttpRequestBridgeHandler} + * |- UpstreamHandler - {HttpRequestBridgeHandler} + * |- DownstreamHandler - {HttpRequestBridgeHandler} + * Responsibilities: + * a). pass client actions over bridge as events + * b). fire corresponding event to client when receives events from bridge + * + * TODO: + * a). shall we check http response status code? now this code is checked in bridge side + */ +public class HttpRequestBridgeHandler implements HttpRequestHandler, ProxyListener { + private static final String CLASS_NAME = HttpRequestBridgeHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private HttpRequestListener listener; + + public HttpRequestBridgeHandler() { + LOG.entering(CLASS_NAME, ""); + } + + @Override + public synchronized void processOpen(HttpRequest request) { + LOG.entering(CLASS_NAME, "open", request); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("processOpen: "+request); + } + + HttpURI uri = request.getUri(); + Method method = request.getMethod(); + + if (request.getProxy() != null) { + throw new IllegalStateException("processOpen previously called with HttpRequest"); + } + + try { + Proxy proxy = BridgeUtil.createProxy(uri.getURI(), this); + proxy.setPeer(request); + request.setProxy(proxy); + + /* Dispatch create event to the bridge */ + String[] params = new String[] { "HTTPREQUEST", uri.toString(), method.toString(), request.isAsync() ? "Y" : "N" }; + proxy.processEvent(XoaEventKind.CREATE, params); + } catch (Exception e) { + LOG.log(Level.FINE, "While initializing HttpRequest proxy: "+e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + + private void handleRequestCreated(HttpRequest request) { + LOG.entering(CLASS_NAME, "handleRequestCreated"); + if (LOG.isLoggable(Level.FINE)) { + LOG.fine("handleRequestCreated: "+request); + } + + request.setReadyState(HttpRequest.ReadyState.READY); + try { + for (Entry entry : request.getHeaders().entrySet()) { + String header = entry.getKey(); + String value = entry.getValue(); + HttpRequestUtil.validateHeader(header); + Proxy proxy = (Proxy)request.getProxy(); + proxy.processEvent(XoaEventKind.SETREQUESTHEADER, new String[] { header, value }); + } + + // Nothing has been sent + if (request.getMethod() == Method.POST) { + listener.requestReady(request); + } + else { + processSend(request, null); + } + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + + @Override + public void processSend(HttpRequest request, WrappedByteBuffer content) { + LOG.entering(CLASS_NAME, "processSend", content); + + if (request.getReadyState() != HttpRequest.ReadyState.READY) { + throw new IllegalStateException("HttpRequest must be in READY state to send"); + } + + request.setReadyState(HttpRequest.ReadyState.SENDING); + + java.nio.ByteBuffer payload; + if (content == null) { + payload = java.nio.ByteBuffer.allocate(0); + } else { + payload = java.nio.ByteBuffer.wrap(content.array(), content.arrayOffset(), content.remaining()); + } + + Proxy proxy = (Proxy)request.getProxy(); + proxy.processEvent(XoaEventKind.SEND, new Object[] { payload }); + request.setReadyState(HttpRequest.ReadyState.SENT); + } + + private void handleRequestProgressed(HttpRequest request, WrappedByteBuffer payload) { + LOG.entering(CLASS_NAME, "handleRequestProgressed", payload); + + request.setReadyState(HttpRequest.ReadyState.LOADING); + try { + listener.requestProgressed(request, payload); + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + + private void handleRequestLoaded(HttpRequest request, WrappedByteBuffer responseBuffer) { + LOG.entering(CLASS_NAME, "handleRequestLoaded", responseBuffer); + + request.setReadyState(HttpRequest.ReadyState.LOADED); + + HttpResponse response = request.getResponse(); + response.setBody(responseBuffer); + + try { + listener.requestLoaded(request, response); + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + + public static void parseResponseHeaders(HttpResponse response, String in) { + LOG.entering(CLASS_NAME, "setResponseHeaders", in); + String headers = in + ""; + int lf = headers.indexOf("\n"); + while (lf != -1) { + String ret = headers.substring(0, lf); + ret.trim(); + int colonAt = ret.indexOf(":"); + String name = ret.substring(0, colonAt); + String value = ret.substring(colonAt + 1); + response.setHeader(name, value); + + if (lf != headers.length()) { + // check if the last char is the \n + headers = headers.substring(lf + 1); + if (headers.length() == 0) { + headers.trim(); + } + } else { + headers = ""; + } + + if (headers.length() == 0) { + break; + } + lf = headers.indexOf("\n"); + } + } + + @Override + public void eventReceived(Proxy proxy, XoaEventKind eventKind, Object[] params) { + LOG.entering(CLASS_NAME, "eventReceived", new Object[] { proxy, this, eventKind, params }); + + HttpRequest request = (HttpRequest)proxy.getPeer(); + + if (LOG.isLoggable(Level.FINE)) { + LOG.log(Level.FINE, "SOA <-- XOA:" + "id = " + proxy.getHandlerId() + " name: " + eventKind + " " + request); + } + + switch (eventKind) { + case OPEN: + handleRequestCreated(request); + break; + case READYSTATECHANGE: + int state = Integer.parseInt((String) params[0]); + if (state == 2) { + HttpResponse response = new HttpResponse(); + request.setResponse(response); + + if (params.length > 1) { + int responseCode = Integer.parseInt((String) params[1]); + if (responseCode != 0) { + response.setStatusCode(responseCode); + response.setMessage(((String) params[2])); + parseResponseHeaders(response, ((String) params[3])); + + } + } + request.setReadyState(HttpRequest.ReadyState.OPENED); + listener.requestOpened(request); + } + break; + case PROGRESS: + WrappedByteBuffer messageBuffer = WrappedByteBuffer.wrap((java.nio.ByteBuffer) params[0]); + handleRequestProgressed(request, messageBuffer); + break; + case LOAD: + WrappedByteBuffer responseBuffer = WrappedByteBuffer.wrap((java.nio.ByteBuffer) params[0]); + handleRequestLoaded(request, responseBuffer); + break; + case CLOSED: + proxy = null; + listener.requestClosed(request); + break; + case ERROR: + proxy = null; + String s = "HTTP Bridge Handler: ERROR event received"; + handleErrorOccurred(request, new IllegalStateException(s)); + break; + default: + throw new IllegalArgumentException("INVALID_STATE_ERR"); + } + } + + private void handleErrorOccurred(HttpRequest request, Exception exception) { + request.setReadyState(HttpRequest.ReadyState.ERROR); + listener.errorOccurred(request, exception); + } + + @Override + public void processAbort(HttpRequest request) { + if (request.getReadyState() == HttpRequest.ReadyState.UNSENT) { + throw new IllegalStateException("INVALID_STATE_ERR"); + } + Proxy proxy = (Proxy)request.getProxy(); + proxy.processEvent(XoaEventKind.ABORT, XoaEvent.EMPTY_ARGS); + } + + @Override + public void setListener(HttpRequestListener listener) { + this.listener = listener; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/Proxy.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/Proxy.java new file mode 100755 index 0000000..b08583c --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/Proxy.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.bridge; + +import java.net.URI; + +import org.kaazing.gateway.client.impl.bridge.XoaEvent.XoaEventKind; + +/** + * This class manages the handler id and URI associated with each proxy for the bridge. + */ +public class Proxy { + private Integer handlerId; + private URI uri; + private Object peer; + private ProxyListener listener; + + public Proxy() { + } + + public URI getUri() { + return uri; + } + + void setUri(URI uri) { + this.uri = uri; + } + + void setListener(ProxyListener listener) { + this.listener = listener; + } + + public void setHandlerId(Integer handlerId) { + this.handlerId = handlerId; + } + + public Integer getHandlerId() { + return handlerId; + } + + public void setPeer(Object peer) { + this.peer = peer; + } + + public Object getPeer() { + return peer; + } + + void processEvent(XoaEventKind kind, Object[] params) { + BridgeUtil.processEvent(new XoaEvent(handlerId, kind, params)); + } + + void eventReceived(Integer handlerId, XoaEventKind name, Object[] params) { + listener.eventReceived(this, name, params); + } + + public String toString() { + return "[Proxy "+handlerId+"]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/ProxyListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/ProxyListener.java new file mode 100755 index 0000000..6b4baf4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/ProxyListener.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.bridge; + +import org.kaazing.gateway.client.impl.bridge.XoaEvent.XoaEventKind; + +public interface ProxyListener { + + void eventReceived(Proxy proxy, XoaEventKind name, Object[] params); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/WebSocketNativeBridgeHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/WebSocketNativeBridgeHandler.java new file mode 100755 index 0000000..3427029 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/WebSocketNativeBridgeHandler.java @@ -0,0 +1,201 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.bridge; + +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.bridge.XoaEvent.XoaEventKind; +import org.kaazing.gateway.client.impl.wsn.WebSocketNativeChannel; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +/* + * WebSocket Native Handler Chain + * NativeHandler - AuthenticationHandler - HandshakeHandler - ControlFrameHandler - BalanceingHandler - Nodec - {BridgeHandler} + * Responsibilities: + * a). pass client actions over the bridge as events + * b). fire events to client when receive events from bridge (see eventReceived function) + * TODO: + * n/a + */ +public class WebSocketNativeBridgeHandler implements WebSocketHandler, ProxyListener { + private static final String CLASS_NAME = WebSocketNativeBridgeHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private WebSocketHandlerListener listener; + + /** + * WebSocket + * @throws Exception + */ + public WebSocketNativeBridgeHandler() { + LOG.entering(CLASS_NAME, ""); + } + + /** + * Establishes the websocket connection + */ + @Override + public synchronized void processConnect(WebSocketChannel channel, WSURI uri, String[] protocols) { + LOG.entering(CLASS_NAME, "processConnect", new Object[] { uri, protocols }); + + try { + WebSocketNativeChannel nativeChannel = (WebSocketNativeChannel)channel; + if (nativeChannel.getProxy() != null) { + throw new IllegalStateException("Bridge proxy previously set"); + } + + Proxy proxy = BridgeUtil.createProxy(uri.getURI(), this); + proxy.setPeer(channel); + nativeChannel.setProxy(proxy); + + String[] params; + if (protocols != null) { + String s = ""; + for (int i=0; i0) { + s += ","; + } + s += protocols[i]; + } + params = new String[] { "WEBSOCKET", uri.toString(), s, ""}; + } else { + params = new String[] { "WEBSOCKET", uri.toString() }; + } + proxy.processEvent(XoaEventKind.CREATE, params); + } + catch (Exception e) { + LOG.log(Level.FINE, "While initializing WebSocket proxy: "+e.getMessage(), e); + listener.connectionFailed(channel, e); + } + } + + /** + * Set the authorize token for future requests for "Basic" authentication. + */ + @Override + public void processAuthorize(WebSocketChannel channel, String authorizeToken) { + LOG.entering(CLASS_NAME, "processAuthorize"); + + WebSocketNativeChannel nativeChannel = (WebSocketNativeChannel)channel; + Proxy proxy = nativeChannel.getProxy(); + proxy.processEvent(XoaEventKind.AUTHORIZE, new String[] { authorizeToken }); + } + + @Override + public void processTextMessage(WebSocketChannel channel, String text) { + WebSocketNativeChannel nativeChannel = (WebSocketNativeChannel)channel; + Proxy proxy = (Proxy)nativeChannel.getProxy(); + proxy.processEvent(XoaEventKind.POSTMESSAGE, new Object[] { text }); + // throw new Error("Not implemented: Use binary message for wire traffic"); + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer message) { + java.nio.ByteBuffer buffer = java.nio.ByteBuffer.allocate(message.remaining()); + buffer.put(message.array(), message.arrayOffset(), message.remaining()); + buffer.flip(); + + WebSocketNativeChannel nativeChannel = (WebSocketNativeChannel)channel; + Proxy proxy = (Proxy)nativeChannel.getProxy(); + proxy.processEvent(XoaEventKind.POSTMESSAGE, new Object[] { buffer }); + } + + @Override + public synchronized void processClose(WebSocketChannel channel, int code, String reason) { + LOG.entering(CLASS_NAME, "processDisconnect"); + + WebSocketNativeChannel nativeChannel = (WebSocketNativeChannel)channel; + Proxy proxy = nativeChannel.getProxy(); + proxy.processEvent(XoaEventKind.DISCONNECT, new String[] {}); + } + + @Override + public final void eventReceived(Proxy proxy, XoaEventKind eventKind, Object[] params) { + LOG.entering(CLASS_NAME, "eventReceived", new Object[] { proxy.getHandlerId(), eventKind, params }); + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, "SOA <-- XOA:" + "id = " + proxy + " name: " + eventKind); + } + + WebSocketNativeChannel channel = (WebSocketNativeChannel)proxy.getPeer(); + + switch (eventKind) { + case OPEN: + String protocol = (String)params[0]; + listener.connectionOpened(channel, protocol); + break; + case CLOSED: + channel.setProxy(null); + listener.connectionClosed(channel, false, 1006, ""); //pass default close code and reason here + break; + case REDIRECT: + String redirectUrl = (String)params[0]; + listener.redirected(channel, redirectUrl); + break; + case AUTHENTICATE: + String location = channel.getLocation().toString(); + String challenge = (String)params[0]; + listener.authenticationRequested(channel, location, challenge); + break; + case MESSAGE: + WrappedByteBuffer messageBuffer = WrappedByteBuffer.wrap((java.nio.ByteBuffer) params[0]); + String messageType = params.length > 1 ? (String)params[1] : null; + + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, messageBuffer.getHexDump()); + } + + if (messageType == null) { + LOG.severe("Incompatible bridge detected"); + listener.connectionFailed(channel, new IllegalStateException("Incompatible bridge detected")); + } + + if ("TEXT".equals(messageType)) { + String text = messageBuffer.getString(UTF8); + listener.textMessageReceived(channel, text); + } + else { + listener.binaryMessageReceived(channel, messageBuffer); + } + break; + + case ERROR: + listener.connectionFailed(channel, new IllegalStateException("ERROR event in the native bridge handler")); + break; + } + } + + public void setListener(WebSocketHandlerListener listener) { + this.listener = listener; + } + + @Override + public void setIdleTimeout(WebSocketChannel channel, int timeout) { + + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/bridge/XoaEvent.java b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/XoaEvent.java new file mode 100755 index 0000000..de004fd --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/bridge/XoaEvent.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.bridge; + +import java.io.Serializable; +import java.util.logging.Logger; + +public class XoaEvent implements Serializable { + private static final String CLASS_NAME = XoaEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final long serialVersionUID = 1L; + public static final String[] EMPTY_ARGS = new String[] {}; + + private Integer handlerId; + private XoaEventKind kind; + private Object[] params; + + public XoaEvent(Integer handlerId, XoaEventKind event, Object[] params) { + LOG.entering(CLASS_NAME, "", new Object[] { handlerId, event, params }); + this.handlerId = handlerId; + this.kind = event; + this.params = params; + } + + public Integer getHandlerId() { + LOG.exiting(CLASS_NAME, "getHandlerId", handlerId); + return handlerId; + } + + public XoaEventKind getKind() { + LOG.exiting(CLASS_NAME, "getEvent", kind); + return kind; + } + + public Object[] getParams() { + LOG.exiting(CLASS_NAME, "getParams", params); + return params; + } + + public String toString() { + String out = "EventID:" + getHandlerId() + "," + getKind().name() + "["; + for (int i = 0; i < params.length; i++) { + out += params[i] + ","; + } + return out + "]"; + } + + public enum XoaEventKind { + + // Entries for WebSocket and ByteSocket events + OPEN("open"), // handlerid + MESSAGE("message"), // handlerid, message (string) + CLOSED("closed"), // handlerid + REDIRECT("redirect"), // handlerid, location(string) + AUTHENTICATE("authenticate"), // handlerid, challenge(string) + AUTHORIZE("authorize"), // handlerid, authorizeToken(string) + + // Entries for StreamingHttpRequest events + LOAD("load"), // handlerid + PROGRESS("progress"), // handlerid, ??? + READYSTATECHANGE("readystatechange"), // handlerid, state + ERROR("error"), // handlerid + ABORT("abort"), // handlerid + + // Entries for WebSocket and ByteSocket methods + CREATE("create"), // handlerid, type, wsurl, originurl + POSTMESSAGE("postMessage"), // handlerid, message (string) + DISCONNECT("disconnect"), // handlerid + + // OPEN("open"), // handlerid //*** Also an event name + SEND("send"), // handlerid + GETRESPONSEHEADER("getResponseHeader"), // handlerid, header + GEALLRESPONSEHEADERS("getAllResponseHeaders"), // handlerid + SETREQUESTHEADER("setRequestHeader"), // handlerid, header, value + // ABORT("abort"), // handlerid // *** also an event name + UNDEFINED(""); + + String name; + + XoaEventKind(String in) { + name = in; + } + + public static XoaEventKind getName(String in) { + final XoaEventKind[] v = values(); + for (int i = 0; i < v.length; i++) { + if (v[i].name.equals(in)) { + return v[i]; + } + } + return UNDEFINED; + } + + public String toString() { + return name; + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequest.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequest.java new file mode 100755 index 0000000..9b1fef3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequest.java @@ -0,0 +1,183 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.util.HttpURI; + +public class HttpRequest { + + private static final String CLASS_NAME = HttpRequest.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + static volatile int nextId = 1; + final int id; + + public static final HttpRequestFactory HTTP_REQUEST_FACTORY = new HttpRequestFactory() { + @Override + public HttpRequest createHttpRequest(Method method, HttpURI uri, boolean async) { + return new HttpRequest(method, uri, async); + } + }; + + /** Possible ready states for the request */ + public enum ReadyState { + /** Request has not yet been sent */ + UNSENT, + /** Request is ready to be sent. Data can be sent at this time for POST requests */ + READY, + /** Request is in the process of sending. No further data can be written at this time. */ + SENDING, + /** Request has been sent, but no response has been received */ + SENT, + /** Response has been partially received. All headers are available. */ + OPENED, + /** Response has been partially received. Some data is available. */ + LOADING, + /** Response has been completed. All data is available. */ + LOADED, + /** An error occurred during the request or response. */ + ERROR; + } + + /** Current ready state for this request */ + private ReadyState readyState = ReadyState.UNSENT; + + /** Methods available for HttpRequest */ + public enum Method { + GET, POST + } + + /** Method specified for this request */ + private Method method; + + /** URI specified for this request */ + private HttpURI uri; + + /** True if progress events are returned as data is received */ + private boolean async; + + /** Headers associated with request. Headers must be set before or during requestReady event */ + private Map headers = new HashMap(); + + /** Response received for this request */ + private HttpResponse response; + + /** Higher layer managing this request */ + public Channel parent; + + /** Underlying object representing this request */ + private Object proxy; + + /** Handler for this request */ + HttpRequestHandler transportHandler; + + /** Creates an HttpRequest with method and uri specified. Async is true by default. */ + public HttpRequest(Method method, HttpURI uri) { + this(method, uri, true); + } + + /** Creates an HttpRequest with method and uri specified. + * Async true means fire progress events as data is received. */ + public HttpRequest(Method method, HttpURI uri, boolean async) { + this.id = nextId++; + + if (uri == null) { + LOG.severe("HTTP request URL is null"); + throw new IllegalArgumentException("HTTP request URL is null"); + } + + if (method == null) { + LOG.severe("Invalid Method in an HTTP request"); + throw new IllegalArgumentException("Invalid Method in an HTTP request"); + } + + this.method = method; + this.uri = uri; + this.async = async; + } + + /** Get Method specified for this request */ + public Method getMethod() { + return method; + } + + /** Get URI specified for this request */ + public HttpURI getUri() { + return uri; + } + + /** Return true if progress events will fire as data is received */ + public boolean isAsync() { + return async; + } + + /** Set ready state for this request */ + public void setReadyState(ReadyState readyState) { + this.readyState = readyState; + } + + /** Get ready state for this request */ + public ReadyState getReadyState() { + return readyState; + } + + /** Set header for this request */ + public void setHeader(String header, String value) { + headers.put(header, value); + } + + /** Get all headers for this request */ + public Map getHeaders() { + return headers; + } + + /** Get the response associated with this request. + * Returns null if response has not yet been received. */ + public HttpResponse getResponse() { + return response; + } + + /** Sets the response associated with this request. */ + public void setResponse(HttpResponse response) { + this.response = response; + } + + /** Gets the proxy object associated with this request. */ + public Object getProxy() { + return proxy; + } + + /** Sets the proxy object associated with this request */ + public void setProxy(Object proxy) { + this.proxy = proxy; + } + + @Override + public String toString() { + return "[Request "+id+": "+method+" "+uri+" async:"+async+"]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestAuthenticationHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestAuthenticationHandler.java new file mode 100755 index 0000000..e675f18 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestAuthenticationHandler.java @@ -0,0 +1,340 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.net.HttpURLConnection; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.auth.AuthenticationUtil; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.wseb.WebSocketEmulatedChannel; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.StringUtils; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.auth.ChallengeRequest; +import org.kaazing.net.auth.ChallengeResponse; +import org.kaazing.net.impl.util.ResumableTimer; + +/* + * WebSocket Emulated Handler Chain + * EmulateHandler + * |- CreateHandler - HttpRequestAuthenticationHandler - {HttpRequestRedirectHandler} - HttpRequestBridgeHandler + * |- UpstreamHandler - HttpRequestBridgeHandler + * |- DownstreamHandler - HttpRequestBridgeHandler + * Responsibilities: + * a). handle authentication challenge (HTTP 401) + * + * TODO: + * n/a + */ +public class HttpRequestAuthenticationHandler extends HttpRequestHandlerAdapter { + + private static final String CLASS_NAME = HttpRequestAuthenticationHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final Charset UTF_8 = Charset.forName("UTF-8"); + + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + private static final String WWW_AUTHENTICATE = "WWW-Authenticate: "; + private static final String APPLICATION_PREFIX = "Application "; + + private static final String HTTP_1_1_START = "HTTP/1.1"; + private static final int HTTP_1_1_START_LEN = HTTP_1_1_START.length(); + private static final byte[] HTTP_1_1_START_BYTES = StringUtils.getUtf8Bytes(HTTP_1_1_START); + + + + + private void handleClearAuthenticationData(HttpRequest request) { + Channel channel = getWebSocketChannel(request); + if(channel == null) + return; + ChallengeHandler nextChallengeHandler = null; + if (channel.challengeResponse != null) { + nextChallengeHandler = channel.challengeResponse.getNextChallengeHandler(); + channel.challengeResponse.clearCredentials(); + channel.challengeResponse = null; + } + channel.challengeResponse = new ChallengeResponse(null, nextChallengeHandler); + } + + private void handleRemoveAuthenticationData(HttpRequest request) { + handleClearAuthenticationData(request); + } + + protected static String[] getLines(WrappedByteBuffer buf) { + List lineList = new ArrayList(); + while (buf.hasRemaining()) { + byte next = buf.get(); + List lineText = new ArrayList(); + while (next != 13) { // CR + lineText.add(next); + if (buf.hasRemaining()) { + next = buf.get(); + } else { + break; + } + } + if (buf.hasRemaining()) { + next = buf.get(); // should be LF + } + byte[] lineTextBytes = new byte[lineText.size()]; + int i = 0; + for (Byte text : lineText) { + lineTextBytes[i] = text; + i++; + } + lineList.add(new String(lineTextBytes, UTF_8)); + } + String[] lines = new String[lineList.size()]; + lineList.toArray(lines); + return lines; + } + + public static boolean isHTTPResponse(WrappedByteBuffer buf) { + if ( buf.remaining() < HTTP_1_1_START_LEN) { + return false; + } + + for (int i = 0; i < HTTP_1_1_START_LEN; i++) { + if (buf.getAt(i) != HTTP_1_1_START_BYTES[i]) { + return false; + } + } + + return true; + } + + private void onLoadWrappedHTTPResponse(HttpRequest request, HttpResponse response) throws Exception { + LOG.entering(CLASS_NAME, "onLoadWrappedHTTPResponse"); + + WrappedByteBuffer responseBody = response.getBody(); + String[] lines = getLines(responseBody); + + int statusCode = Integer.parseInt(lines[0].split(" ")[1]); + if (statusCode == HttpURLConnection.HTTP_UNAUTHORIZED) { + String wwwAuthenticate = null; + for (int i = 1; i < lines.length; i++) { + if (lines[i].startsWith(WWW_AUTHENTICATE)) { + wwwAuthenticate = lines[i].substring(WWW_AUTHENTICATE.length()); + break; + } + } + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("connectToWebSocket.onLoadWrappedHTTPResponse: WWW-Authenticate: " + StringUtils.stripControlCharacters(wwwAuthenticate)); + } + + if (wwwAuthenticate == null || "".equals(wwwAuthenticate)) { + throw new IllegalStateException("Missing authentication challenge in wrapped HTTP 401 response"); + } else if (!wwwAuthenticate.startsWith(APPLICATION_PREFIX)) { + throw new IllegalStateException("Only Application challenges are supported by the client"); + } + + String rawChallenge = wwwAuthenticate.substring(APPLICATION_PREFIX.length()); + handle401(request, rawChallenge); + } + else { + throw new IllegalStateException("Unsupported wrapped response with HTTP status code " + statusCode); + } + } + + private void handle401(HttpRequest request, String challenge) throws Exception { + LOG.entering(CLASS_NAME, "handle401"); + + HttpURI uri = request.getUri(); + WebSocketEmulatedChannel channel = (WebSocketEmulatedChannel)getWebSocketChannel(request); + if(channel == null) { + throw new IllegalStateException("There is no WebSocketChannel associated with this request"); + } + if (isWebSocketClosing(request)) { + return; //WebSocket is closing/closed, quit authenticate process + } + + ResumableTimer connectTimer = null; + if (((WebSocketCompositeChannel)channel.getParent()) != null) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + connectTimer = parent.getConnectTimer(); + if (connectTimer != null) { + // Pause the connect timer while the user is providing the credentials. + connectTimer.pause(); + } + } + + channel.authenticationReceived = true; + String challengeUrl = channel.getLocation().toString(); + if (channel.redirectUri != null) { + String path = channel.redirectUri.getPath(); + if ((path != null) && path.contains("/;e/")) { + // path "/;e/cbm" was added in WebSocketEmulatedHandler. It is + // returned by balancer, so we should remove it. + int index = path.indexOf("/;e/"); + path = path.substring(0, index); + } + + challengeUrl = channel.redirectUri.getScheme() + "://" + channel.redirectUri.getURI().getAuthority() + path; + } + ChallengeRequest challengeRequest = new ChallengeRequest(challengeUrl, challenge); + try { + channel.challengeResponse = AuthenticationUtil.getChallengeResponse(channel, challengeRequest, channel.challengeResponse); + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage()); + handleClearAuthenticationData(request); + throw new IllegalStateException("Unexpected error processing challenge "+challenge, e); + } + + if (channel.challengeResponse == null || channel.challengeResponse.getCredentials() == null) { + throw new IllegalStateException("No response possible for challenge "+challenge); + } + + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("response from challenge handler = " + StringUtils.stripControlCharacters(String.valueOf(channel.challengeResponse.getCredentials()))); + } + + try { + HttpRequest newRequest = new HttpRequest(request.getMethod(), uri, request.isAsync()); + newRequest.parent = request.parent; + +// newRequest.setHeader("Content-Type", "text/plain"); + for (Entry entry : request.getHeaders().entrySet()) { + newRequest.setHeader(entry.getKey(), entry.getValue()); + } + + // Resume the connect timer before invoking processOpen(). + if (connectTimer != null) { + connectTimer.resume(); + } + + processOpen(newRequest); + } + catch (Exception e1) { + LOG.log(Level.FINE, e1.getMessage(), e1); + throw new Exception("Unable to authenticate user", e1); + } + } + + @Override + public void processOpen(HttpRequest request) { + WebSocketEmulatedChannel channel = (WebSocketEmulatedChannel)getWebSocketChannel(request); + if(channel != null) { + if (isWebSocketClosing(request)) { + return; //WebSocket is closing/closed, quit authenticate process + } + if (channel.challengeResponse.getCredentials() != null) { + String credentials = new String(channel.challengeResponse.getCredentials()); + LOG.finest("requestOpened: Authorization: " + StringUtils.stripControlCharacters(credentials)); + request.setHeader(HEADER_AUTHORIZATION, credentials); + handleClearAuthenticationData(request); + } + } + nextHandler.processOpen(request); + } + + @Override + public void setNextHandler(HttpRequestHandler handler) { + super.setNextHandler(handler); + + handler.setListener(new HttpRequestListener() { + + @Override + public void requestReady(HttpRequest request) { + listener.requestReady(request); + } + + @Override + public void requestOpened(HttpRequest request) { + listener.requestOpened(request); + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + listener.requestProgressed(request, payload); + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + int responseCode = response.getStatusCode(); + switch (responseCode) { + case 200: + WrappedByteBuffer responseBuffer = response.getBody(); + if (isHTTPResponse(responseBuffer)) { + try { + onLoadWrappedHTTPResponse(request, response); + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + else { + handleRemoveAuthenticationData(request); + listener.requestLoaded(request, response); + } + break; + + case 401: + String challenge = response.getHeader(HEADER_WWW_AUTHENTICATE); + try { + handle401(request, challenge); + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage()); + listener.errorOccurred(request, e); + } + break; + + default: + handleRemoveAuthenticationData(request); + listener.requestLoaded(request, response); + } + } + + @Override + public void requestClosed(HttpRequest request) { + handleRemoveAuthenticationData(request); + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + handleRemoveAuthenticationData(request); + listener.errorOccurred(request, exception); + } + + @Override + public void requestAborted(HttpRequest request) { + handleRemoveAuthenticationData(request); + listener.requestAborted(request); + } + }); + } + + + @Override + public void setListener(HttpRequestListener listener) { + this.listener = listener; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestDelegateHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestDelegateHandler.java new file mode 100755 index 0000000..86d8fb0 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestDelegateHandler.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.net.URL; +import java.util.Map.Entry; + +import org.kaazing.gateway.client.impl.bridge.HttpRequestBridgeHandler; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.wseb.WebSocketEmulatedChannel; +import org.kaazing.gateway.client.impl.wsn.WebSocketNativeDelegateHandler; +import org.kaazing.gateway.client.transport.CloseEvent; +import org.kaazing.gateway.client.transport.ErrorEvent; +import org.kaazing.gateway.client.transport.LoadEvent; +import org.kaazing.gateway.client.transport.OpenEvent; +import org.kaazing.gateway.client.transport.ProgressEvent; +import org.kaazing.gateway.client.transport.ReadyStateChangedEvent; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegate; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegateImpl; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegateListener; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class HttpRequestDelegateHandler implements HttpRequestHandler { + + HttpRequestListener listener; + + @Override + public void processOpen(final HttpRequest request) { + HttpRequestDelegate delegate = new HttpRequestDelegateImpl(); + try { + request.setProxy(delegate); + String origin = "privileged://" + WebSocketNativeDelegateHandler.getCanonicalHostPort(request.getUri().getURI()); + + delegate.setListener(new HttpRequestDelegateListener() { + + @Override + public void readyStateChanged(ReadyStateChangedEvent event) { + Object[] params = (Object[]) event.getParams(); + int state = Integer.parseInt((String) params[0]); + if (state == 2) { + HttpResponse response = new HttpResponse(); + request.setResponse(response); + + if (params.length > 1) { + int responseCode = Integer.parseInt((String) params[1]); + if (responseCode != 0) { + response.setStatusCode(responseCode); + response.setMessage(((String) params[2])); + HttpRequestBridgeHandler.parseResponseHeaders(response, ((String) params[3])); + } + } + request.setReadyState(HttpRequest.ReadyState.OPENED); + listener.requestOpened(request); + } + } + + @Override + public void progressed(ProgressEvent progressEvent) { + java.nio.ByteBuffer payload = progressEvent.getPayload(); + WrappedByteBuffer buffer = WrappedByteBuffer.wrap(payload); + + request.setReadyState(HttpRequest.ReadyState.LOADING); + try { + listener.requestProgressed(request, buffer); + } catch (Exception e) { +// LOG.log(Level.FINE, e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + + @Override + public void opened(OpenEvent event) { + HttpRequestDelegate delegate = (HttpRequestDelegate)request.getProxy(); + + // Allow headers to be set via opened + request.setReadyState(HttpRequest.ReadyState.READY); + listener.requestOpened(request); + + // Then set headers + for (Entry entry : request.getHeaders().entrySet()) { + String header = entry.getKey(); + String value = entry.getValue(); + HttpRequestUtil.validateHeader(header); + delegate.setRequestHeader(header, value); + } + + // Nothing has been sent + if (request.getMethod() == Method.POST) { + listener.requestReady(request); + } + else { + processSend(request, null); + } + } + + @Override + public void loaded(LoadEvent event) { + WrappedByteBuffer responseBuffer = WrappedByteBuffer.wrap(event.getResponseBuffer()); + request.setReadyState(HttpRequest.ReadyState.LOADED); + + HttpResponse response = request.getResponse(); + response.setBody(responseBuffer); + + try { + listener.requestLoaded(request, response); + } catch (Exception e) { +// LOG.log(Level.FINE, e.getMessage(), e); + listener.errorOccurred(request, e); + } + } + + @Override + public void closed(CloseEvent event) { + listener.requestClosed(request); + } + + @Override + public void errorOccurred(ErrorEvent event) { + listener.errorOccurred(request, event.getException()); + } + }); + + String method = request.getMethod().toString(); + URL url = request.getUri().getURI().toURL(); + boolean isAsync = request.isAsync(); + int connectTimeout = (int) getConnectTimeout(request); + delegate.processOpen(method, url, origin, isAsync, connectTimeout); + } catch (Exception e) { + listener.errorOccurred(request, e); + } + } + + @Override + public void processSend(HttpRequest request, WrappedByteBuffer content) { +// LOG.entering(CLASS_NAME, "processSend", content); + + if (request.getReadyState() != HttpRequest.ReadyState.READY) { + throw new IllegalStateException("HttpRequest must be in READY state to send"); + } + + request.setReadyState(HttpRequest.ReadyState.SENDING); + + java.nio.ByteBuffer payload; + if (content == null) { + payload = java.nio.ByteBuffer.allocate(0); + } else { + payload = java.nio.ByteBuffer.wrap(content.array(), content.arrayOffset(), content.remaining()); + } + + HttpRequestDelegate delegate = (HttpRequestDelegate)request.getProxy(); + delegate.processSend(payload); + request.setReadyState(HttpRequest.ReadyState.SENT); + } + + @Override + public void processAbort(HttpRequest request) { + HttpRequestDelegate delegate = (HttpRequestDelegate)request.getProxy(); + delegate.processAbort(); + } + + @Override + public void setListener(HttpRequestListener listener) { + this.listener = listener; + } + + private long getConnectTimeout(HttpRequest request) { + WebSocketCompositeChannel compChannel = getWebSocketCompositeChannel(request); + if (compChannel != null) { + if (compChannel.getConnectTimer() != null) { + return compChannel.getConnectTimer().getDelay(); + } + } + + return 0L; + } + + private WebSocketCompositeChannel getWebSocketCompositeChannel(HttpRequest request) { + if (request.parent != null) { + WebSocketEmulatedChannel emulatedChannel = (WebSocketEmulatedChannel) request.parent.getParent(); + if (emulatedChannel != null) { + return (WebSocketCompositeChannel) emulatedChannel.getParent(); + } + + return null; + } + + return null; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestEvent.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestEvent.java new file mode 100755 index 0000000..ee629a2 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestEvent.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.util.logging.Logger; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class HttpRequestEvent { + private static final String CLASS_NAME = HttpRequestEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final long serialVersionUID = -7922410353957227356L; + + private HttpRequest source; + private final Kind kind; + private final WrappedByteBuffer data; + + /** + * Type of the HttpRequestEvent. + */ + public enum Kind { + OPEN, LOAD, PROGRESS, ERROR, READYSTATECHANGE, ABORT + } + + public HttpRequestEvent(HttpRequest source, Kind kind) { + this(source, kind, null); + } + + public HttpRequestEvent(HttpRequest source, Kind kind, WrappedByteBuffer data) { + this.source = source; + LOG.entering(CLASS_NAME, "", new Object[] { source, kind, data }); + this.kind = kind; + this.data = data; + } + + public HttpRequest getSource() { + return source; + } + + public Kind getKind() { + return kind; + } + + public WrappedByteBuffer getData() { + return data; + } + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestFactory.java new file mode 100755 index 0000000..1963a87 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestFactory.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.util.HttpURI; + +public interface HttpRequestFactory { + + HttpRequest createHttpRequest(Method method, HttpURI uri, boolean async); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandler.java new file mode 100755 index 0000000..bd04c8f --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandler.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public interface HttpRequestHandler { + + void processOpen(HttpRequest request); + void processSend(HttpRequest request, WrappedByteBuffer buffer); + void processAbort(HttpRequest request); + + void setListener(HttpRequestListener listener); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerAdapter.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerAdapter.java new file mode 100755 index 0000000..73c7838 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerAdapter.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.ws.ReadyState; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class HttpRequestHandlerAdapter implements HttpRequestHandler { + + protected HttpRequestHandler nextHandler; + protected HttpRequestListener listener; + + @Override + public void processOpen(HttpRequest request) { + nextHandler.processOpen(request); + } + + @Override + public void processSend(HttpRequest request, WrappedByteBuffer buffer) { + nextHandler.processSend(request, buffer); + } + + @Override + public void processAbort(HttpRequest request) { + nextHandler.processAbort(request); + } + + @Override + public void setListener(HttpRequestListener listener) { + this.listener = listener; + } + + public void setNextHandler(HttpRequestHandler handler) { + this.nextHandler = handler; + } + + + public Channel getWebSocketChannel(HttpRequest request) { + if (request.parent != null) { + return request.parent.getParent(); + } + else { + return null; + } + } + + // return true if WebSocket connection is closing or closed + // parameter: channel - WebSockectEmulatedChannel + public boolean isWebSocketClosing(HttpRequest request) { + Channel channel = getWebSocketChannel(request); + if (channel != null && channel.getParent() != null) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + if (parent != null) { + return parent.getReadyState() == ReadyState.CLOSED || parent.getReadyState() == ReadyState.CLOSING; + } + } + return false; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerFactory.java new file mode 100755 index 0000000..575918d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestHandlerFactory.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + + +public interface HttpRequestHandlerFactory { + + HttpRequestHandler createHandler(); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestListener.java new file mode 100755 index 0000000..1447f30 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestListener.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public interface HttpRequestListener { + + /** Invoked when the request is ready to send data upstream */ + void requestReady(HttpRequest request); + + /** Invoked when the response status code and headers are available */ + void requestOpened(HttpRequest request); + + /** Invoked when streamed data is received on the response */ + void requestProgressed(HttpRequest request, WrappedByteBuffer payload); + + /** Invoked when the response has completed and all data is available */ + void requestLoaded(HttpRequest request, HttpResponse response); + + /** Invoked when the request has been aborted */ + void requestAborted(HttpRequest request); + + /** Invoked when the request has closed and no longer valid */ + void requestClosed(HttpRequest request); + + /** Invoked when an error has occurred while processing the request or response */ + void errorOccurred(HttpRequest request, Exception exception); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestLoggingHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestLoggingHandler.java new file mode 100755 index 0000000..30098e9 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestLoggingHandler.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.util.logging.Logger; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class HttpRequestLoggingHandler extends HttpRequestHandlerAdapter { + + private static final String CLASS_NAME = HttpRequestLoggingHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + @Override + public void processOpen(HttpRequest request) { + LOG.fine("->OPEN: "+request); + super.processOpen(request); + } + + @Override + public void processSend(HttpRequest request, WrappedByteBuffer buffer) { + LOG.fine("->SEND: "+request+" "+buffer.getHexDump()); + super.processSend(request, buffer); + } + + @Override + public void processAbort(HttpRequest request) { + LOG.fine("->ABORT: "+request); + super.processAbort(request); + } + + @Override + public void setNextHandler(HttpRequestHandler handler) { + this.nextHandler = handler; + + handler.setListener(new HttpRequestListener() { + + @Override + public void requestReady(HttpRequest request) { + LOG.fine("<-READY: "+request); + listener.requestReady(request); + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + LOG.fine("<-PROGRESSED: "+request+" "+payload.getHexDump()); + listener.requestProgressed(request, payload); + } + + @Override + public void requestOpened(HttpRequest request) { + LOG.fine("<-OPENED: "+request); + listener.requestOpened(request); + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + LOG.fine("<-LOADED: "+request+" "+response); + listener.requestLoaded(request, response); + } + + @Override + public void requestClosed(HttpRequest request) { + LOG.fine("<-CLOSED: "+request); + listener.requestClosed(request); + } + + @Override + public void requestAborted(HttpRequest request) { + LOG.fine("<-ABORTED: "+request); + listener.requestAborted(request); + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + LOG.fine("<-ERROR: "+request); + listener.errorOccurred(request, exception); + } + }); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestRedirectHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestRedirectHandler.java new file mode 100755 index 0000000..f9df8d6 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestRedirectHandler.java @@ -0,0 +1,154 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import static java.util.Collections.unmodifiableMap; + +import java.net.URI; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.wseb.WebSocketEmulatedChannel; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.StringUtils; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.http.HttpRedirectPolicy; +/* + * WebSocket Emulated Handler Chain + * EmulateHandler + * |- CreateHandler - HttpRequestAuthenticationHandler - {HttpRequestRedirectHandler} - HttpRequestBridgeHandler + * |- UpstreamHandler - HttpRequestBridgeHandler + * |- DownstreamHandler - HttpRequestBridgeHandler + * Responsibilities: + * a). handle redirect (HTTP 301) + * + * TODO: + * n/a + */ +public class HttpRequestRedirectHandler extends HttpRequestHandlerAdapter { + + private static final String CLASS_NAME = HttpRequestRedirectHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + @Override + public void setNextHandler(HttpRequestHandler handler) { + super.setNextHandler(handler); + + handler.setListener(new HttpRequestListener() { + + @Override + public void requestReady(HttpRequest request) { + listener.requestReady(request); + } + + @Override + public void requestOpened(HttpRequest request) { + listener.requestOpened(request); + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + listener.requestProgressed(request, payload); + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + int responseCode = response.getStatusCode(); + switch (responseCode) { + case 301: + case 302: + case 307: + // handle the redirect (possibly cross-scheme) + String redirectedLocation = response.getHeader("Location"); + if (LOG.isLoggable(Level.FINEST)) { + LOG.finest("redirectedLocation = " + StringUtils.stripControlCharacters(redirectedLocation)); + } + + if (redirectedLocation == null) { + throw new IllegalStateException("Redirect response missing location header: " + responseCode); + } + + try { + HttpURI uri = new HttpURI(redirectedLocation); + + HttpRequest redirectRequest = new HttpRequest(request.getMethod(), uri, request.isAsync()); + redirectRequest.parent = request.parent; + WebSocketEmulatedChannel channel = (WebSocketEmulatedChannel)request.parent.getParent(); + channel.redirectUri = uri; + + WebSocketCompositeChannel compChannel = (WebSocketCompositeChannel)channel.getParent(); + HttpRedirectPolicy policy = compChannel.getFollowRedirect(); + URI currentURI = channel.getLocation().getURI(); + URI redirectURI = uri.getURI(); + + // When redirected while using emulated connection. the schemes of the currentURI and + // redirectURI will be different. So, we should normalize it before enforcing the + // redirect policy. + String normalizedRedirectScheme = redirectURI.getScheme().toLowerCase().replace("http", "ws"); + URI normalizedRedirectURI = new URI(normalizedRedirectScheme, redirectURI.getSchemeSpecificPart(), null); + if ((policy != null) && (policy.compare(currentURI, normalizedRedirectURI) != 0)) { + String s = String.format("%s: Cannot redirect from '%s' to '%s'", + policy, currentURI, normalizedRedirectURI); + channel.preventFallback = true; + throw new IllegalStateException(s); + } + + for (Entry entry : request.getHeaders().entrySet()) { + redirectRequest.setHeader(entry.getKey(), entry.getValue()); + } + nextHandler.processOpen(redirectRequest); + + } catch (Exception e) { + LOG.log(Level.WARNING, e.getMessage(), e); + throw new IllegalStateException("Redirect to a malformed URL: " + redirectedLocation, e); + } + break; + + default: + listener.requestLoaded(request, response); + break; + } + } + + @Override + public void requestClosed(HttpRequest request) { + listener.requestClosed(request); + } + + @Override + public void requestAborted(HttpRequest request) { + listener.requestAborted(request); + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + listener.errorOccurred(request, exception); + } + }); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestTransportHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestTransportHandler.java new file mode 100755 index 0000000..36da8f0 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestTransportHandler.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.bridge.HttpRequestBridgeHandler; +import org.kaazing.gateway.client.impl.ws.WebSocketTransportHandler; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class HttpRequestTransportHandler extends HttpRequestHandlerAdapter { + + private static final String CLASS_NAME = HttpRequestTransportHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + public static HttpRequestHandlerFactory DEFAULT_FACTORY = new HttpRequestHandlerFactory() { + + @Override + public HttpRequestHandler createHandler() { + HttpRequestHandler requestHandler = new HttpRequestTransportHandler(); + + if (LOG.isLoggable(Level.FINE)) { + HttpRequestLoggingHandler loggingHandler = new HttpRequestLoggingHandler(); + loggingHandler.setNextHandler(requestHandler); + requestHandler = loggingHandler; + } + + return requestHandler; + } + }; + + @Override + public void processOpen(HttpRequest request) { + LOG.entering(CLASS_NAME, "processOpen: "+request); + + HttpRequestHandler transportHandler; + if (WebSocketTransportHandler.useBridge(request.getUri().getURI())) { + transportHandler = new HttpRequestBridgeHandler(); + } + else { + transportHandler = new HttpRequestDelegateHandler(); + } + + request.transportHandler = transportHandler; + + transportHandler.setListener(new HttpRequestListener() { + + @Override + public void requestReady(HttpRequest request) { + listener.requestReady(request); + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + listener.requestProgressed(request, payload); + } + + @Override + public void requestOpened(HttpRequest request) { + listener.requestOpened(request); + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + listener.requestLoaded(request, response); + } + + @Override + public void requestClosed(HttpRequest request) { + listener.requestClosed(request); + } + + @Override + public void requestAborted(HttpRequest request) { + listener.requestAborted(request); + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + listener.errorOccurred(request, exception); + } + }); + + transportHandler.processOpen(request); + } + + @Override + public void processSend(HttpRequest request, WrappedByteBuffer buffer) { + LOG.entering(CLASS_NAME, "processSend: "+request); + + HttpRequestHandler transportHandler = request.transportHandler; + transportHandler.processSend(request, buffer); + } + + @Override + public void processAbort(HttpRequest request) { + LOG.entering(CLASS_NAME, "processAbort: "+request); + + HttpRequestHandler transportHandler = request.transportHandler; + transportHandler.processAbort(request); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestUtil.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestUtil.java new file mode 100755 index 0000000..ae2e362 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpRequestUtil.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.util.logging.Logger; + +public class HttpRequestUtil { + private static final String CLASS_NAME = HttpRequestUtil.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private HttpRequestUtil() { + LOG.entering(CLASS_NAME, ""); + } + + public static void validateHeader(String header) { + LOG.entering(CLASS_NAME, "validateHeader", header); + /* + * From the XMLHttpRequest spec: http://www.w3.org/TR/XMLHttpRequest/#setrequestheader + * + * For security reasons, these steps should be terminated if the header argument case-insensitively matches one of the + * following headers: + * + * Accept-Charset Accept-Encoding Connection Content-Length Content-Transfer-Encoding Date Expect Host Keep-Alive Referer + * TE Trailer Transfer-Encoding Upgrade Via Proxy-* Sec-* + * + * Also for security reasons, these steps should be terminated if the start of the header argument case-insensitively + * matches Proxy- or Se + */ + if (header == null || (header.length() == 0)) { + throw new IllegalArgumentException("Invalid header in the HTTP request"); + } + String lowerCaseHeader = header.toLowerCase(); + if (lowerCaseHeader.startsWith("proxy-") || lowerCaseHeader.startsWith("sec-")) { + throw new IllegalArgumentException("Headers starting with Proxy-* or Sec-* are prohibited"); + } + for (String prohibited : INVALID_HEADERS) { + if (header.equalsIgnoreCase(prohibited)) { + throw new IllegalArgumentException("Headers starting with Proxy-* or Sec-* are prohibited"); + } + } + } + + private static final String[] INVALID_HEADERS = new String[] { "Accept-Charset", "Accept-Encoding", "Connection", + "Content-Length", "Content-Transfer-Encoding", "Date", "Expect", "Host", "Keep-Alive", "Referer", "TE", "Trailer", + "Transfer-Encoding", "Upgrade", "Via" }; + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpResponse.java b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpResponse.java new file mode 100755 index 0000000..7da1db3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/http/HttpResponse.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.http; + +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class HttpResponse { + + private int statusCode = 0; + private String message; + private Map headers = new HashMap(); + private WrappedByteBuffer responseBuffer; + + public int getStatusCode() { + return statusCode; + } + + public void setStatusCode(int statusCode) { + this.statusCode = statusCode; + } + + public void setMessage(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public String setHeader(String header, String value) { + return headers.put(header, value); + } + + public String getHeader(String header) { + return headers.get(header); + } + + public String getAllHeaders() { + StringBuffer buf = new StringBuffer(); + for (Entry entry : headers.entrySet()) { + buf.append(entry.getKey() + ":" + entry.getValue() + "\n"); + } + return buf.toString(); + } + + public WrappedByteBuffer getBody() { + return responseBuffer.duplicate(); + } + + public void setBody(WrappedByteBuffer responseBuffer) { + this.responseBuffer = responseBuffer; + } + + public String toString() { + String headers = getAllHeaders(); + if (headers != null) { + headers = "\n" + headers; + } + return "[RESPONSE "+getStatusCode()+" "+getMessage()+headers+"]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/util/WSCompositeURI.java b/android/src/main/java/org/kaazing/gateway/client/impl/util/WSCompositeURI.java new file mode 100755 index 0000000..41a23e7 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/util/WSCompositeURI.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.util; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.HashMap; +import java.util.Map; + +import org.kaazing.gateway.client.util.GenericURI; + + +/** + * URI with guarantee to be a valid, non-null, ws URI + */ +public class WSCompositeURI extends GenericURI { + + static Map wsEquivalent = new HashMap(); + static { + wsEquivalent.put("ws", "ws"); + wsEquivalent.put("wse", "ws"); + wsEquivalent.put("wsn", "ws"); + + wsEquivalent.put("wss", "wss"); + wsEquivalent.put("wssn", "wss"); + wsEquivalent.put("wse+ssl", "wss"); + + wsEquivalent.put("java:ws", "ws"); + wsEquivalent.put("java:wse", "ws"); + wsEquivalent.put("java:wss", "wss"); + wsEquivalent.put("java:wse+ssl", "wss"); + } + + String scheme = null; + + protected boolean isValidScheme(String scheme) { + return wsEquivalent.get(scheme) != null; + } + + /* + * This class is needed to workaround java URI's inability to handle java:ws style composite prefixes. + */ + public WSCompositeURI(String location) throws URISyntaxException { + this(new URI(location)); + } + + public WSCompositeURI(URI uri) throws URISyntaxException { + super(uri); + } + +// private static String normalize(String location) { +// return location.startsWith("java:") ? location.substring(5) : location; +// } + + protected WSCompositeURI duplicate(URI uri) { + try { + return new WSCompositeURI(uri); + } + catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public boolean isSecure() { + String scheme = getScheme(); + return "wss".equals(wsEquivalent.get(scheme)); + } + + public WSURI getWSEquivalent() { + try { + String wsEquivScheme = wsEquivalent.get(getScheme()); + return WSURI.replaceScheme(this.uri, wsEquivScheme); + } + catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + @Override + public String getScheme() { + // Workaround URI behavior that returns only "java" instead of "java:ws" + if (scheme == null) { + String location = uri.toString(); + int schemeEndIndex = location.indexOf("://"); + if (schemeEndIndex != -1) { + scheme = location.substring(0, schemeEndIndex); + } + else { + scheme = uri.toString(); + } + } + return scheme; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/util/WSURI.java b/android/src/main/java/org/kaazing/gateway/client/impl/util/WSURI.java new file mode 100755 index 0000000..2f5fc92 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/util/WSURI.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.util; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.kaazing.gateway.client.util.GenericURI; +import org.kaazing.gateway.client.util.URIUtils; + + +/** + * URI with guarantee to be a valid, non-null, ws URI + */ +public class WSURI extends GenericURI { + + @Override + protected boolean isValidScheme(String scheme) { + return "ws".equals(scheme) || "wss".equals(scheme); + } + + public WSURI(String location) throws URISyntaxException { + this(new URI(location)); + } + + public WSURI(URI location) throws URISyntaxException { + super(location); + } + + protected WSURI duplicate(URI uri) { + try { + return new WSURI(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + static WSURI replaceScheme(URI uri, String scheme) throws URISyntaxException { + URI wsUri = URIUtils.replaceScheme(uri, scheme); + return new WSURI(wsUri); + } + + public boolean isSecure() { + String scheme = getScheme(); + return "wss".equals(scheme); + } + + public String getHttpEquivalentScheme() { + return (uri.getScheme().equals("ws") ? "http" : "https"); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/util/WebSocketUtil.java b/android/src/main/java/org/kaazing/gateway/client/impl/util/WebSocketUtil.java new file mode 100755 index 0000000..e134724 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/util/WebSocketUtil.java @@ -0,0 +1,113 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.util; + +import java.io.ByteArrayOutputStream; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; + + + +// Oh how i wish we had functional programming, then we could pass a function to +// encodeLength that will be called for each encodedByte and not need two versions +// in java this could be done with an interface that had something like Buffer.write(byte) +// and two anonymous implementations of it that would delegate to the appropriate +// type with the correct method +public class WebSocketUtil { + private static final String CLASS_NAME = WebSocketUtil.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + /* + * Length-bytes are written out in order from most to least significant, but are computed most efficiently (using bit shifts) + * from least to most significant. An integer serves as a temporary storage, which is then written out in reversed order. + */ + public static void encodeLength(ByteArrayOutputStream out, int length) { + LOG.entering(CLASS_NAME, "encodeLength", new Object[] { out, length }); + int byteCount = 0; + long encodedLength = 0; + + do { + // left shift one byte to make room for new data + encodedLength <<= 8; + // set 7 bits of length + encodedLength |= (byte) (length & 0x7f); + // right shift out the 7 bits we just set + length >>= 7; + // increment the byte count that we need to encode + byteCount++; + } + // continue if there are remaining set length bits + while (length > 0); + + do { + // get byte from encoded length + byte encodedByte = (byte) (encodedLength & 0xff); + // right shift encoded length past byte we just got + encodedLength >>= 8; + // The last length byte does not have the highest bit set + if (byteCount != 1) { + // set highest bit if this is not the last + encodedByte |= (byte) 0x80; + } + // write encoded byte + out.write(encodedByte); + } + // decrement and continue if we have more bytes left + while (--byteCount > 0); + } + + public static void encodeLength(WrappedByteBuffer buf, int length) { + LOG.entering(CLASS_NAME, "encodeLength", new Object[] { buf, length }); + int byteCount = 0; + int encodedLength = 0; + + do { + // left shift one byte to make room for new data + encodedLength <<= 8; + // set 7 bits of length + encodedLength |= (byte) (length & 0x7f); + // right shift out the 7 bits we just set + length >>= 7; + // increment the byte count that we need to encode + byteCount++; + } + // continue if there are remaining set length bits + while (length > 0); + + do { + // get byte from encoded length + byte encodedByte = (byte) (encodedLength & 0xff); + // right shift encoded length past byte we just got + encodedLength >>= 8; + // The last length byte does not have the highest bit set + if (byteCount != 1) { + // set highest bit if this is not the last + encodedByte |= (byte) 0x80; + } + // write encoded byte + buf.put(encodedByte); + } + // decrement and continue if we have more bytes left + while (--byteCount > 0); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/CloseCommandMessage.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/CloseCommandMessage.java new file mode 100755 index 0000000..ed60228 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/CloseCommandMessage.java @@ -0,0 +1,57 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.io.UnsupportedEncodingException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; + +public class CloseCommandMessage implements CommandMessage { + + private static final String CLASS_NAME = CloseCommandMessage.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + public static final int CLOSE_NO_STATUS = 1005; + public static final int CLOSE_ABNORMAL = 1006; + + private int code = 0; + private String reason; + + public CloseCommandMessage(int code, String reason) { + if (code == 0) { + code = CLOSE_NO_STATUS; + } + + this.code = code; + this.reason = reason; + } + + public int getCode() { + return code; + } + + public String getReason() { + return reason; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/ReadyState.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/ReadyState.java new file mode 100755 index 0000000..e0ab32f --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/ReadyState.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +/** + * Values are CONNECTING = 0, OPEN = 1, CLOSING = 2, and CLOSED = 3; + */ +public enum ReadyState { + CONNECTING, OPEN, CLOSING, CLOSED +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeChannel.java new file mode 100755 index 0000000..ad91561 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeChannel.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.net.URI; +import java.util.LinkedList; +import java.util.List; + + +//import org.kaazing.gateway.client.html5.WebSocket; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.util.WSCompositeURI; +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.impl.util.ResumableTimer; + +public class WebSocketCompositeChannel extends WebSocketChannel { + public WebSocketSelectedChannel selectedChannel; + + String[] requestedProtocols; + + protected List connectionStrategies = new LinkedList(); + protected ReadyState readyState = ReadyState.CLOSED; + protected boolean closing = false; + + private Object webSocket; + private String compositeScheme; + private ChallengeHandler challengeHandler; // This might be temporary till we move off of legacy stuff. + private ResumableTimer connectTimer; + + public WebSocketCompositeChannel(WSCompositeURI location) { + super(location.getWSEquivalent()); + this.compositeScheme = location.getScheme(); + } + + public ChallengeHandler getChallengeHandler() { + return challengeHandler; + } + + public void setChallengeHandler(ChallengeHandler challengeHandler) { + this.challengeHandler = challengeHandler; + } + + public ReadyState getReadyState() { + return readyState; + } + + public Object getWebSocket() { + return webSocket; + } + + public void setWebSocket(Object webSocket) { + this.webSocket = webSocket; + } + + public String getOrigin() { + URI uri = getLocation().getURI(); + return uri.getScheme()+"://"+uri.getHost()+":"+uri.getPort(); + } + + public URI getURL() { + return getLocation().getURI(); + } + + public String getCompositeScheme() { + return compositeScheme; + } + + public String getNextStrategy() { + if (connectionStrategies.isEmpty()) { + return null; + } + else { + return connectionStrategies.remove(0); + } + } + + public synchronized ResumableTimer getConnectTimer() { + return connectTimer; + } + + public synchronized void setConnectTimer(ResumableTimer connectTimer) { + if (this.connectTimer != null) { + this.connectTimer.cancel(); + this.connectTimer = null; + } + + this.connectTimer = connectTimer; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeHandler.java new file mode 100755 index 0000000..4d31803 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketCompositeHandler.java @@ -0,0 +1,432 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.util.HashMap; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.ws.WebSocketSelectedHandlerImpl.WebSocketSelectedHandlerFactory; +import org.kaazing.gateway.client.impl.wseb.WebSocketEmulatedChannel; +import org.kaazing.gateway.client.impl.wseb.WebSocketEmulatedHandler; +import org.kaazing.gateway.client.impl.wsn.WebSocketNativeChannel; +import org.kaazing.gateway.client.impl.wsn.WebSocketNativeHandler; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +/* + * WebSocket Handler Chain + * WebSocket - {CompoisteHandler} + * |- SelctedHandler - NativeHandler (see nativeHandler chain) + * |- SelectedHandler - EmulatedHandler (see emulatedHandler chain) + * Responsibilities: + * a). decide connection strategy + * use native first + * b). handle fallback + * if native failed, fallback to emulated + * + * TODO: + * n/a + */ +public class WebSocketCompositeHandler implements WebSocketHandler { + private static final String CLASS_NAME = WebSocketCompositeHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private WebSocketHandlerListener handlerListener = createListener(); + + static WebSocketSelectedHandlerFactory WEBSOCKET_NATIVE_HANDLER_FACTORY = new WebSocketSelectedHandlerFactory() { + @Override + public WebSocketSelectedHandler createSelectedHandler() { + WebSocketSelectedHandler selectedHandler = new WebSocketSelectedHandlerImpl(); + WebSocketNativeHandler nativeHandler = new WebSocketNativeHandler(); + selectedHandler.setNextHandler(nativeHandler); + return selectedHandler; + } + }; + + static WebSocketSelectedHandlerFactory WEBSOCKET_EMULATED_HANDLER_FACTORY = new WebSocketSelectedHandlerFactory() { + @Override + public WebSocketSelectedHandler createSelectedHandler() { + WebSocketSelectedHandler selectedHandler = new WebSocketSelectedHandlerImpl(); + WebSocketEmulatedHandler emulatedHandler = new WebSocketEmulatedHandler(); + selectedHandler.setNextHandler(emulatedHandler); + return selectedHandler; + } + }; + + static interface WebSocketSelectedChannelFactory { + WebSocketSelectedChannel createChannel(WSURI location); + } + + private final WebSocketSelectedChannelFactory WEBSOCKET_NATIVE_CHANNEL_FACTORY = new WebSocketSelectedChannelFactory() { + @Override + public WebSocketSelectedChannel createChannel(WSURI location) { + return new WebSocketNativeChannel(location); + } + }; + + private final WebSocketSelectedChannelFactory WEBSOCKET_EMULATED_CHANNEL_FACTORY = new WebSocketSelectedChannelFactory() { + @Override + public WebSocketSelectedChannel createChannel(WSURI location) { + return new WebSocketEmulatedChannel(location); + } + }; + + static class WebSocketStrategy { + String nativeEquivalent; // e.g. "ws" + WebSocketHandler handler; + WebSocketSelectedChannelFactory channelFactory; + + WebSocketStrategy(String nativeEquivalent, WebSocketHandler handler, WebSocketSelectedChannelFactory channelFactory) { + this.nativeEquivalent = nativeEquivalent; + this.handler = handler; + this.channelFactory = channelFactory; + } + } + + public static final WebSocketCompositeHandler COMPOSITE_HANDLER = new WebSocketCompositeHandler(); + + final Map strategyChoices = new HashMap(); + final Map strategyMap = new HashMap(); + + private WebSocketHandlerListener listener; + + /** + * Creates a WebSocket that opens up a full-duplex connection to the target location on a supported WebSocket provider + * + * @throws Exception + */ + public WebSocketCompositeHandler() { + LOG.entering(CLASS_NAME, ""); + + WebSocketSelectedHandler nativeHandler = WEBSOCKET_NATIVE_HANDLER_FACTORY.createSelectedHandler(); + nativeHandler.setListener(handlerListener); + + WebSocketSelectedHandler emulatedHandler = WEBSOCKET_EMULATED_HANDLER_FACTORY.createSelectedHandler(); + emulatedHandler.setListener(handlerListener); + + strategyChoices.put("ws", new String[] { "java:ws", "java:wse" }); + strategyChoices.put("wss", new String[] { "java:wss", "java:wse+ssl" }); + strategyChoices.put("wsn", new String[] { "java:ws" }); + strategyChoices.put("wssn", new String[] { "java:wsn" }); + strategyChoices.put("wse", new String[] { "java:wse" }); + strategyChoices.put("wse+ssl", new String[] { "java:wse+ssl" }); + + strategyMap.put("java:ws", new WebSocketStrategy("ws", nativeHandler, WEBSOCKET_NATIVE_CHANNEL_FACTORY)); + strategyMap.put("java:wss", new WebSocketStrategy("wss", nativeHandler, WEBSOCKET_NATIVE_CHANNEL_FACTORY)); + strategyMap.put("java:wse", new WebSocketStrategy("ws", emulatedHandler, WEBSOCKET_EMULATED_CHANNEL_FACTORY)); + strategyMap.put("java:wse+ssl", new WebSocketStrategy("wss", emulatedHandler, WEBSOCKET_EMULATED_CHANNEL_FACTORY)); + strategyMap.put("java:wsn", new WebSocketStrategy("wss", nativeHandler, WEBSOCKET_NATIVE_CHANNEL_FACTORY)); + } + + /** + * Connect the WebSocket object to the remote location + * @param channel WebSocket channel + * @param location location of the WebSocket + * @param protocol protocol spoken over the WebSocket + */ + @Override + public void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + LOG.entering(CLASS_NAME, "connect", channel); + WebSocketCompositeChannel compositeChannel = (WebSocketCompositeChannel)channel; + + if (compositeChannel.readyState != ReadyState.CLOSED) { + LOG.warning("Attempt to reconnect an existing open WebSocket to a different location"); + throw new IllegalStateException("Attempt to reconnect an existing open WebSocket to a different location"); + } + + compositeChannel.readyState = ReadyState.CONNECTING; + compositeChannel.requestedProtocols = protocols; + + String scheme = compositeChannel.getCompositeScheme(); + if (scheme.indexOf(":") >= 0) { + // qualified scheme: e.g. "java:wse" + WebSocketStrategy strategy = strategyMap.get(scheme); + if (strategy == null) { + throw new IllegalArgumentException("Invalid connection scheme: "+scheme); + } + + LOG.finest("Turning off fallback since the URL is prefixed with java:"); + compositeChannel.connectionStrategies.add(scheme); + } + else { + String[] connectionStrategies = strategyChoices.get(scheme); + if (connectionStrategies != null) { + for (String each : connectionStrategies) { + compositeChannel.connectionStrategies.add(each); + } + } + else { + throw new IllegalArgumentException("Invalid connection scheme: "+scheme); + } + } + + fallbackNext(compositeChannel, null); + } + + private void fallbackNext(WebSocketCompositeChannel channel, Exception exception) { + LOG.entering(CLASS_NAME, "fallbackNext"); + try { + String strategyName = channel.getNextStrategy(); + if (strategyName == null) { + if (exception == null) { + doClose(channel, false, 1006, null); + } + else { + doClose(channel, exception); + } + } + else { + initDelegate(channel, strategyName); + } + } catch (Exception e) { + LOG.log(Level.INFO, e.getMessage(), e); + throw new RuntimeException(e); + } + } + + private void initDelegate(WebSocketCompositeChannel channel, String strategyName) { + WebSocketStrategy strategy = strategyMap.get(strategyName); + WebSocketSelectedChannelFactory channelFactory = strategy.channelFactory; + + WSURI location = channel.getLocation(); + + WebSocketSelectedChannel selectedChannel = channelFactory.createChannel(location); + channel.selectedChannel = selectedChannel; + selectedChannel.setParent(channel); + selectedChannel.handler = (WebSocketSelectedHandler)strategy.handler; + selectedChannel.requestedProtocols = channel.requestedProtocols; + + selectedChannel.handler.processConnect(channel.selectedChannel, location, channel.requestedProtocols); + } + + /** + * Writes the message to the WebSocket. + * + * @param message + * String to be written + * @throws Exception + * if contents cannot be written successfully + */ + @Override + public void processTextMessage(WebSocketChannel channel, String message) { + LOG.entering(CLASS_NAME, "send", message); + + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel; + if (parent.readyState != ReadyState.OPEN) { + LOG.warning("Attempt to post message on unopened or closed web socket"); + throw new IllegalStateException("Attempt to post message on unopened or closed web socket"); + } + + WebSocketSelectedChannel selectedChannel = parent.selectedChannel; + selectedChannel.handler.processTextMessage(selectedChannel, message); + } + + /** + * Writes the message to the WebSocket. + * + * @param message + * WrappedByteBuffer to be written + * @throws Exception + * if contents cannot be written successfully + */ + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer message) { + LOG.entering(CLASS_NAME, "send", message); + + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel; + if (parent.readyState != ReadyState.OPEN) { + LOG.warning("Attempt to post message on unopened or closed web socket"); + throw new IllegalStateException("Attempt to post message on unopened or closed web socket"); + } + + WebSocketSelectedChannel selectedChannel = parent.selectedChannel; + selectedChannel.handler.processBinaryMessage(selectedChannel, message); + } + + @Override + public void processAuthorize(WebSocketChannel channel, String authorizeToken) { + // Currently not used + } + + @Override + public void setIdleTimeout(WebSocketChannel channel, int timeout) { + // Currently not used + } + + /** + * Disconnect the WebSocket + * + * @throws Exception + * if the disconnect does not succeed + */ + @Override + public void processClose(WebSocketChannel channel, int code, String reason) { + LOG.entering(CLASS_NAME, "close"); + + //2. check current readyState + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel; + + // When the connection timeout expires due to network loss, we first + // invoke doClose() to inform the application immediately. Then, we + // invoke processClose() to close the connection but it may take a + // while to return. When doClose() is invoked, readyState is set to + // CLOSED. However, we do want processClose() to be invoked all the + // all the way down to close the connection. That's why we are no + // longer throwing an exception here if readyState is CLOSED. + + if (!parent.closing) { + parent.closing = true; + parent.readyState = ReadyState.CLOSING; + + try { + WebSocketSelectedChannel selectedChannel = parent.selectedChannel; + selectedChannel.handler.processClose(selectedChannel, code, reason); + } + catch (Exception e) { + doClose(parent, false, CloseCommandMessage.CLOSE_ABNORMAL, e.getMessage()); + } + } + } + + private WebSocketHandlerListener createListener() { + return new WebSocketHandlerListener() { + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + parent.setProtocol(protocol); + doOpen(parent); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + listener.textMessageReceived(parent, message); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + listener.binaryMessageReceived(parent, buf); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + + // TODO: This is an abstration violation - authenticationReceived should not be exposed on Channel + if ((parent.readyState == ReadyState.CONNECTING) && + !channel.authenticationReceived && + !channel.preventFallback) { + fallbackNext(parent, null); + } + else { + doClose(parent, wasClean, code, reason); + } + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + + // TODO: This is an abstration violation - authenticationReceived should not be exposed on Channel + if ((parent.readyState == ReadyState.CONNECTING) && + !channel.authenticationReceived && + !channel.preventFallback) { + fallbackNext(parent, ex); + } + else { + if (ex == null) { + doClose(parent, false, 1006, null); + } + else { + doClose(parent, ex); + } + } + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + + // TODO: This is an abstration violation - authenticationReceived should not be exposed on Channel + if ((parent.readyState == ReadyState.CONNECTING) && + !channel.authenticationReceived && + !channel.preventFallback) { + fallbackNext(parent, ex); + } + else { + if (ex == null) { + doClose(parent, false, 1006, null); + } + else { + doClose(parent, ex); + } + } + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + // authenticate should not reach here + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + // redirect should not reach here + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + // ignore + } + }; + } + + private void doOpen(WebSocketCompositeChannel channel) { + if (channel.readyState == ReadyState.CONNECTING) { + channel.readyState = ReadyState.OPEN; + listener.connectionOpened(channel, channel.getProtocol()); + } + } + + public void doClose(WebSocketCompositeChannel channel, boolean wasClean, int code, String reason) { + if (channel.readyState == ReadyState.CONNECTING || channel.readyState == ReadyState.CLOSING || channel.readyState == ReadyState.OPEN) { + channel.readyState = ReadyState.CLOSED; + listener.connectionClosed(channel, wasClean, code, reason); + } + } + + public void doClose(WebSocketCompositeChannel channel, Exception ex) { + if (channel.readyState == ReadyState.CONNECTING || channel.readyState == ReadyState.CLOSING || channel.readyState == ReadyState.OPEN) { + channel.readyState = ReadyState.CLOSED; + listener.connectionClosed(channel, ex); + } + } + + public void setListener(WebSocketHandlerListener listener) { + this.listener = listener; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketHandshakeObject.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketHandshakeObject.java new file mode 100755 index 0000000..0ecd053 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketHandshakeObject.java @@ -0,0 +1,81 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +public class WebSocketHandshakeObject { + + private String name; + private String escape; + private HandshakeStatus status; + + /** + * @return the name + */ + public String getName() { + return name; + } + + /** + * @param name the name to set + */ + public void setName(String name) { + this.name = name; + } + + /** + * @return the escape + */ + public String getEscape() { + return escape; + } + + /** + * @param escape the escape to set + */ + public void setEscape(String escape) { + this.escape = escape; + } + + /** + * @return the status + */ + public HandshakeStatus getStatus() { + return status; + } + + /** + * @param status the status to set + */ + public void setStatus(HandshakeStatus status) { + this.status = status; + } + + public enum HandshakeStatus { + Pending, + Accepted + } + + /* Kaazing default objects */ + public final static String KAAZING_EXTENDED_HANDSHAKE = "x-kaazing-handshake"; + public static final String KAAZING_SEC_EXTENSION_IDLETIMEOUT = "x-kaazing-idle-timeout"; + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketLoggingHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketLoggingHandler.java new file mode 100755 index 0000000..ea8bb8c --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketLoggingHandler.java @@ -0,0 +1,148 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketLoggingHandler extends WebSocketHandlerAdapter { + + private static final String CLASS_NAME = WebSocketLoggingHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + @Override + public synchronized void processAuthorize(WebSocketChannel channel, String authorizeToken) { + LOG.fine("->AUTHORIZE: "+channel+" "+authorizeToken); + super.processAuthorize(channel, authorizeToken); + } + + @Override + public void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + LOG.fine("->CONNECT: "+channel+" "+location+" "+toString(protocols)); + super.processConnect(channel, location, protocols); + } + + @Override + public synchronized void processClose(WebSocketChannel channel, int code, String reason) { + LOG.fine("->CLOSE: "+channel); + super.processClose(channel, code, reason); + } + + @Override + public void processTextMessage(WebSocketChannel channel, String text) { + LOG.fine("->TEXT: "+channel+" "+text); + super.processTextMessage(channel, text); + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer buffer) { + LOG.fine("->BINARY: "+channel+" "+buffer.getHexDump()); + super.processBinaryMessage(channel, buffer); + } + + @Override + public void setNextHandler(WebSocketHandler nextHandler) { + super.setNextHandler(nextHandler); + + nextHandler.setListener(new WebSocketHandlerListener() { + @Override + public void redirected(WebSocketChannel channel, String location) { + LOG.fine("<-REDIRECTED: "+channel+" "+location); + listener.redirected(channel, location); + } + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + LOG.fine("<-OPENED: "+channel+" "+protocol); + listener.connectionOpened(channel, protocol); + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + LOG.fine("<-FAILED: "+channel); + listener.connectionFailed(channel, ex); + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + LOG.fine("<-CLOSED: "+channel); + listener.connectionClosed(channel, ex); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + LOG.fine("<-CLOSED: "+channel+" "+wasClean+" "+code+": "+reason); + listener.connectionClosed(channel, wasClean, code, reason); + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + LOG.fine("<-COMMAND: "+channel+" "+message); + listener.commandMessageReceived(channel, message); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + LOG.fine("<-TEXT: "+channel+" "+message); + listener.textMessageReceived(channel, message); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + LOG.fine("<-BINARY: "+channel+" "+buf.getHexDump()); + listener.binaryMessageReceived(channel, buf); + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + LOG.fine("<-AUTHENTICATION REQUESTED: "+channel+" "+location+" Challenge:"+challenge); + listener.authenticationRequested(channel, location, challenge); + } + }); + } + + private String toString(String[] protocols) { + if (protocols == null) { + return "-"; + } + else if (protocols.length == 1) { + return protocols[0]; + } + else { + StringBuilder builder = new StringBuilder(100); + for (int i=0; i0) { + builder.append(","); + } + builder.append(protocols[i]); + } + return builder.toString(); + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketReAuthenticateHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketReAuthenticateHandler.java new file mode 100755 index 0000000..d6e7558 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketReAuthenticateHandler.java @@ -0,0 +1,105 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.nio.charset.Charset; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.Handler; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.impl.http.HttpRequestAuthenticationHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestListener; +import org.kaazing.gateway.client.impl.http.HttpRequestTransportHandler; +import org.kaazing.gateway.client.impl.http.HttpResponse; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketReAuthenticateHandler implements Handler { + + private static final String CLASS_NAME = WebSocketReAuthenticateHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + static final String HEADER_CONTENT_TYPE = "Content-Type"; + static final String HEADER_COOKIE = "Cookie"; + static final String HEADER_SET_COOKIE = "Set-Cookie"; + static final Charset UTF_8 = Charset.forName("UTF-8"); + HttpRequestHandler nextHandler; + //CreateHandlerListener listener; //no listener for this operation. + + HttpRequestAuthenticationHandler authHandler = new HttpRequestAuthenticationHandler(); + HttpRequestHandler transportHandler = HttpRequestTransportHandler.DEFAULT_FACTORY.createHandler(); + + public WebSocketReAuthenticateHandler() { + setNextHandler(authHandler); + authHandler.setNextHandler(transportHandler); + } + + public void processOpen(Channel channel, HttpURI location) { + LOG.entering(CLASS_NAME, "processOpen", location); + + HttpRequest request = HttpRequest.HTTP_REQUEST_FACTORY.createHttpRequest(Method.GET, location, false); + /* + * create a dummy channel in the middle to match emulated Channel structure + * WebSoecktEmulatedChannel->CreateChannel->HttpRequest + */ + request.parent = new Channel(); + request.parent.setParent(channel); + nextHandler.processOpen(request); + } + + public void setNextHandler(HttpRequestHandler handler) { + this.nextHandler = handler; + + handler.setListener(new HttpRequestListener() { + @Override + public void requestReady(HttpRequest request) { + } + + @Override + public void requestOpened(HttpRequest request) { + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer data) { + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + } + + @Override + public void requestAborted(HttpRequest request) { + } + + @Override + public void requestClosed(HttpRequest request) { + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + } + }); + } + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedChannel.java new file mode 100755 index 0000000..1b296f3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedChannel.java @@ -0,0 +1,49 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.util.WSURI; + +public abstract class WebSocketSelectedChannel extends WebSocketChannel { + + WebSocketSelectedHandler handler; + + protected ReadyState readyState = ReadyState.CONNECTING; + + protected String[] requestedProtocols; + +// /** The protocol selected upon the completion of the WebSocket handshake */ +// protected String selectedProtocol; + + public WebSocketSelectedChannel(WSURI location) { + super(location); + } + + public ReadyState getReadyState() { + return readyState; + } + + public String[] getRequestedProtocols() { + return requestedProtocols; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandler.java new file mode 100755 index 0000000..327e9d4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandler.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import org.kaazing.gateway.client.impl.WebSocketHandler; + +public interface WebSocketSelectedHandler extends WebSocketHandler { + + public void setNextHandler(WebSocketHandler nextHandler); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandlerImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandlerImpl.java new file mode 100755 index 0000000..6081187 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketSelectedHandlerImpl.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +/* + * WebSocket Handler Chain + * WebSocket - CompoisteHandler + * |- {SelctedHandler} - NativeHandler (see nativeHandler chain) + * |- {SelectedHandler} - EmulatedHandler (see emulatedHandler chain) + * Responsibilities: + * a). change Channel readyState when events are received + * b). fire events only necessary + * + * TODO: + * n/a + */ +public class WebSocketSelectedHandlerImpl extends WebSocketHandlerAdapter implements WebSocketSelectedHandler { + private static final String CLASS_NAME = WebSocketSelectedHandlerImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + public static interface WebSocketSelectedHandlerFactory { + WebSocketSelectedHandler createSelectedHandler(); + } + + static WebSocketSelectedHandlerFactory FACTORY = new WebSocketSelectedHandlerFactory() { + @Override + public WebSocketSelectedHandlerImpl createSelectedHandler() { + return new WebSocketSelectedHandlerImpl(); + } + }; + + protected WebSocketHandlerListener listener; + + public WebSocketSelectedHandlerImpl() { + LOG.entering(CLASS_NAME, ""); + } + + /** + * Establishes the websocket connection + */ + @Override + public void processConnect(WebSocketChannel channel, WSURI uri, String[] protocols) { + LOG.entering(CLASS_NAME, "connect", channel); + + if (((WebSocketSelectedChannel)channel).readyState == ReadyState.CLOSED) { + throw new IllegalStateException("WebSocket is already closed"); + } + + nextHandler.processConnect(channel, uri, protocols); + } + + @Override + public void processClose(WebSocketChannel channel, int code, String reason) { + LOG.entering(CLASS_NAME, "processDisconnect"); + WebSocketSelectedChannel ch = (WebSocketSelectedChannel)channel; + if (ch.readyState == ReadyState.OPEN || ch.readyState == ReadyState.CONNECTING) { + ch.readyState = ReadyState.CLOSING; + nextHandler.processClose(channel, code, reason); + } + } + + public void handleConnectionOpened(WebSocketChannel channel, String protocol) { + LOG.entering(CLASS_NAME, "handleConnectionOpened"); + + WebSocketSelectedChannel selectedChannel = (WebSocketSelectedChannel)channel; + if (selectedChannel.readyState == ReadyState.CONNECTING) { + selectedChannel.readyState = ReadyState.OPEN; + listener.connectionOpened(channel, protocol); + } + } + + + public void handleBinaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer message) { + LOG.entering(CLASS_NAME, "handleMessageReceived", message); + + if (((WebSocketSelectedChannel)channel).readyState != ReadyState.OPEN) { + return; + } + + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, message.getHexDump()); + } + + listener.binaryMessageReceived(channel, message); + } + + public void handleTextMessageReceived(WebSocketChannel channel, String message) { + LOG.entering(CLASS_NAME, "handleTextMessageReceived", message); + + if (((WebSocketSelectedChannel)channel).readyState != ReadyState.OPEN) { + return; + } + + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, message); + } + + listener.textMessageReceived(channel, message); + } + + protected void handleConnectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + LOG.entering(CLASS_NAME, "handleConnectionClosed"); + + WebSocketSelectedChannel selectedChannel = (WebSocketSelectedChannel)channel; + if (selectedChannel.readyState != ReadyState.CLOSED) { + selectedChannel.readyState = ReadyState.CLOSED; + listener.connectionClosed(channel, wasClean, code, reason); + } + } + + protected void handleConnectionClosed(WebSocketChannel channel, Exception ex) { + LOG.entering(CLASS_NAME, "handleConnectionClosed"); + + WebSocketSelectedChannel selectedChannel = (WebSocketSelectedChannel)channel; + if (selectedChannel.readyState != ReadyState.CLOSED) { + selectedChannel.readyState = ReadyState.CLOSED; + listener.connectionClosed(channel, ex); + } + } + + protected void handleConnectionFailed(WebSocketChannel channel, Exception ex) { + LOG.entering(CLASS_NAME, "connectionFailed"); + + WebSocketSelectedChannel selectedChannel = (WebSocketSelectedChannel)channel; + if (selectedChannel.readyState != ReadyState.CLOSED) { + selectedChannel.readyState = ReadyState.CLOSED; + listener.connectionFailed(channel, ex); + } + } + + @Override + public void setNextHandler(WebSocketHandler nextHandler) { + this.nextHandler = nextHandler; + + nextHandler.setListener(new WebSocketHandlerListener() { + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + handleConnectionOpened(channel, protocol); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer message) { + handleBinaryMessageReceived(channel, message); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + handleTextMessageReceived(channel, message); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + handleConnectionClosed(channel, wasClean, code, reason); + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + handleConnectionClosed(channel, ex); + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + handleConnectionFailed(channel, ex); + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + } + + }); + } + + public void setListener(WebSocketHandlerListener listener) { + this.listener = listener; + } + +} + diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketTransportHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketTransportHandler.java new file mode 100755 index 0000000..57a2ee8 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/ws/WebSocketTransportHandler.java @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.ws; + +import java.net.URI; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.bridge.WebSocketNativeBridgeHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.impl.wsn.WebSocketNativeDelegateHandler; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketTransportHandler extends WebSocketHandlerAdapter { + + private static final String CLASS_NAME = WebSocketTransportHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + public WebSocketHandler WEB_SOCKET_NATIVE_HANDLER = null; + public HttpRequestHandler HTTP_REQUEST_HANDLER = null; + + public static boolean useBridge(URI uri) { + LOG.fine("Determine whether bridge needs to be used"); + + try { + SecurityManager securityManager = System.getSecurityManager(); + if (securityManager != null) { + String host = uri.getHost(); + int port = uri.getPort(); + securityManager.checkConnect(host, port); + } + + LOG.fine("Bypassing the bridge: "+uri); + return false; + } catch (Exception e) { + LOG.fine("Must use bridge: "+uri+": "+e.getMessage()); + return true; + } + } + + @Override + public void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + + WebSocketHandler transportHandler = channel.transportHandler; + if (useBridge(location.getURI())) { + transportHandler = new WebSocketNativeBridgeHandler(); + } else { + transportHandler = new WebSocketNativeDelegateHandler(); + } + + channel.transportHandler = transportHandler; + + transportHandler.setListener(new WebSocketHandlerListener() { + @Override + public void redirected(WebSocketChannel channel, String location) { + listener.redirected(channel, location); + } + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + listener.connectionOpened(channel, protocol); + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + listener.connectionFailed(channel, ex); + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + listener.connectionClosed(channel, ex); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + listener.connectionClosed(channel, wasClean, code, reason); + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + listener.commandMessageReceived(channel, message); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + listener.textMessageReceived(channel, message); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + listener.binaryMessageReceived(channel, buf); + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + listener.authenticationRequested(channel, location, challenge); + } + }); + + transportHandler.processConnect(channel, location, protocols); + } + + @Override + public void processAuthorize(WebSocketChannel channel, String authorizeToken) { + WebSocketHandler transportHandler = channel.transportHandler; + transportHandler.processAuthorize(channel, authorizeToken); + } + + @Override + public void processClose(WebSocketChannel channel, int code, String reason) { + WebSocketHandler transportHandler = channel.transportHandler; + transportHandler.processClose(channel, code, reason); + } + + @Override + public void processTextMessage(WebSocketChannel channel, String text) { + WebSocketHandler transportHandler = channel.transportHandler; + transportHandler.processTextMessage(channel, text); + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer buffer) { + WebSocketHandler transportHandler = channel.transportHandler; + transportHandler.processBinaryMessage(channel, buffer); + } + + @Override + public void setIdleTimeout(WebSocketChannel channel, int timeout) { + WebSocketHandler transportHandler = channel.transportHandler; + transportHandler.setIdleTimeout(channel, timeout); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateChannel.java new file mode 100755 index 0000000..84c6be6 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateChannel.java @@ -0,0 +1,56 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.http.HttpRequest; + +import java.util.HashMap; +import java.util.Map; + +class CreateChannel extends Channel { + + protected String cookie = null; + Map controlFrames; + String[] protocols; + private HttpRequest request; + + public CreateChannel() { + super(0); + controlFrames = new HashMap(); + } + + public void setProtocols(String[] protocols) { + this.protocols = protocols; + } + public String[] getProtocols() { + return protocols; + } + + public HttpRequest getRequest() { + return request; + } + + public void setRequest(HttpRequest request) { + this.request = request; + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandler.java new file mode 100755 index 0000000..73581d7 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandler.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.util.HttpURI; + +interface CreateHandler { + + void setListener(CreateHandlerListener createHandlerListener); + void processOpen(CreateChannel createChannel, HttpURI createUri); + void processClose(CreateChannel crateChannel); + void setNextHandler(HttpRequestHandler nextHandler); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerFactory.java new file mode 100755 index 0000000..001153c --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +interface CreateHandlerFactory { + CreateHandler createCreateHandler(); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerImpl.java new file mode 100755 index 0000000..66d446d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerImpl.java @@ -0,0 +1,217 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import static org.kaazing.gateway.client.impl.Channel.HEADER_SEQUENCE; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.impl.http.HttpRequestAuthenticationHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestListener; +import org.kaazing.gateway.client.impl.http.HttpRequestRedirectHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestTransportHandler; +import org.kaazing.gateway.client.impl.http.HttpResponse; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.ws.WebSocketSelectedChannel; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +/* + * WebSocket Emulated Handler Chain + * EmulateHandler + * |- {CreateHandler} - HttpRequestAuthenticationHandler - HttpRequestRedirectHandler - HttpRequestBridgeHandler + * |- UpstreamHandler - HttpRequestBridgeHandler + * |- DownstreamHandler - HttpRequestBridgeHandler + * Responsibilities: + * a). process Connect + * send httpRequest + * if connected, save ControlFrame bytes to channel + * TODO: + * n/a + */ +class CreateHandlerImpl implements CreateHandler { + private static final String CLASS_NAME = CreateHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + static CreateHandlerFactory FACTORY = new CreateHandlerFactory() { + @Override + public CreateHandler createCreateHandler() { + return new CreateHandlerImpl(); + } + }; + + private static final String HEADER_WEBSOCKET_PROTOCOL = "X-WebSocket-Protocol"; + private static final String HEADER_SEC_EXTENSIONS = "X-WebSocket-Extensions"; + private static final String HEADER_WEBSOCKET_VERSION = "X-WebSocket-Version"; + private static final String HEADER_ACCEPT_COMMANDS = "X-Accept-Commands"; + private static final String WEBSOCKET_VERSION = "wseb-1.0"; + + HttpRequestHandler nextHandler; + CreateHandlerListener listener; + + HttpRequestAuthenticationHandler authHandler = new HttpRequestAuthenticationHandler(); + HttpRequestRedirectHandler redirectHandler = new HttpRequestRedirectHandler(); + HttpRequestHandler transportHandler = HttpRequestTransportHandler.DEFAULT_FACTORY.createHandler(); + + public CreateHandlerImpl() { + setNextHandler(authHandler); + authHandler.setNextHandler(redirectHandler); + redirectHandler.setNextHandler(transportHandler); + } + + @Override + public void processOpen(CreateChannel channel, HttpURI location) { + HttpRequest request = HttpRequest.HTTP_REQUEST_FACTORY.createHttpRequest(Method.GET, location, false); + + // request.getHeaders().put(HEADER_SEC_EXTENSIONS, WebSocketHandshakeObject.KAAZING_SEC_EXTENSION_REVALIDATE); + if (channel.getProtocols() != null && channel.getProtocols().length > 0 ) { + StringBuilder sb = new StringBuilder(); + for (String p : channel.getProtocols()) { + sb.append(p); + sb.append(','); + } + + // strip out comma that gets appended at the end + request.getHeaders().put(HEADER_WEBSOCKET_PROTOCOL, sb.substring(0, sb.length() - 1)); + } + request.getHeaders().put(HEADER_SEC_EXTENSIONS, getEnabledExtensions(channel)); + request.getHeaders().put(HEADER_WEBSOCKET_VERSION, WEBSOCKET_VERSION); + request.getHeaders().put(HEADER_ACCEPT_COMMANDS, "ping"); + request.getHeaders().put(HEADER_SEQUENCE, Long.toString(channel.nextSequence())); + request.parent = channel; + channel.setRequest(request); + nextHandler.processOpen(request); + } + + @Override + public void processClose(CreateChannel channel){ + HttpRequest request = channel.getRequest(); + if (request != null) { + nextHandler.processAbort(request); + } + } + + @Override + public void setNextHandler(HttpRequestHandler handler) { + this.nextHandler = handler; + + handler.setListener(new HttpRequestListener() { + @Override + public void requestReady(HttpRequest request) { + } + + @Override + public void requestOpened(HttpRequest request) { + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer data) { + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + WebSocketEmulatedHandler.LOG.entering(WebSocketEmulatedHandler.CLASS_NAME, "requestLoaded"); + + CreateChannel channel = (CreateChannel)request.parent; + try { + channel.cookie = response.getHeader(WebSocketEmulatedHandler.HEADER_SET_COOKIE); + + //get supported extensions escape bytes + String protocol = response.getHeader(HEADER_WEBSOCKET_PROTOCOL); + ((WebSocketChannel)channel.getParent()).setProtocol(protocol); + + //get supported extensions escape bytes + String extensionsHeader = response.getHeader(HEADER_SEC_EXTENSIONS); + ((WebSocketChannel)channel.getParent()).setNegotiatedExtensions(extensionsHeader); + if (extensionsHeader != null && extensionsHeader.length() > 0) { + String[] extensions = extensionsHeader.split(","); + for (String extension : extensions) { + String[] tmp = extension.split(";"); + if (tmp.length > 1) { + //has escape bytes + String escape = tmp[1].trim(); + if (escape.length() == 8) { + try { + int escapeKey = Integer.parseInt(escape, 16); + channel.controlFrames.put(escapeKey, tmp[0].trim()); + } catch(NumberFormatException e) { + // this is not an escape parameter + LOG.log(Level.FINE, e.getMessage(), e); + } + + } + } + } + } + + WrappedByteBuffer responseBody = response.getBody(); + String urls = responseBody.getString(WebSocketEmulatedHandler.UTF_8); + String[] parts = urls.split("\n"); + + HttpURI upstreamUri = new HttpURI(parts[0]); + HttpURI downstreamUri = new HttpURI((parts.length == 2) ? parts[1] : parts[0]); + listener.createCompleted(channel, upstreamUri, downstreamUri, null); + + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + + listener.createFailed(channel, e); + throw new IllegalStateException("WebSocketEmulation failed", e); + } + } + + @Override + public void requestAborted(HttpRequest request) { + CreateChannel channel = (CreateChannel) request.parent; + channel.setRequest(null); + } + + @Override + public void requestClosed(HttpRequest request) { + CreateChannel channel = (CreateChannel) request.parent; + channel.setRequest(null); + } + + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + CreateChannel createChannel = (CreateChannel)request.parent; + listener.createFailed(createChannel, exception); + } + }); + } + + public void setListener(CreateHandlerListener listener) { + this.listener = listener; + } + + private String getEnabledExtensions(CreateChannel channel) { + WebSocketSelectedChannel selChannel = (WebSocketSelectedChannel) channel.getParent(); + WebSocketCompositeChannel compChannel = (WebSocketCompositeChannel) selChannel.getParent(); + return compChannel.getEnabledExtensions(); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerListener.java new file mode 100755 index 0000000..f19cbb3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/CreateHandlerListener.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.util.HttpURI; + +public interface CreateHandlerListener { + + void createCompleted(CreateChannel channel, HttpURI upstreamUri, HttpURI downstreamUri, String protocol); + void createFailed(CreateChannel channel, Exception exception); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamChannel.java new file mode 100755 index 0000000..50972df --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamChannel.java @@ -0,0 +1,72 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import java.util.HashSet; +import java.util.LinkedList; +import java.util.Queue; +import java.util.Set; +import java.util.Timer; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +class DownstreamChannel extends Channel { + HttpURI location; + public String protocol; + + final AtomicBoolean reconnecting = new AtomicBoolean(false); + final AtomicBoolean closing = new AtomicBoolean(false); + final AtomicBoolean attemptProxyModeFallback = new AtomicBoolean(false); + Set outstandingRequests = new HashSet(5); + Queue buffersToRead = new LinkedList(); + int nextMessageAt; + + //--------Idle Timeout-------------// + final AtomicInteger idleTimeout = new AtomicInteger(); + final AtomicLong lastMessageTimestamp = new AtomicLong(); + Timer idleTimer = null; + + /** Cookie required for auth credentials */ + String cookie; + + //KG-6984 move decoder into DownstreamChannel - persist state information for each websocket downstream + WebSocketEmulatedDecoder decoder; + + public DownstreamChannel(HttpURI location, String cookie) { + this(location, cookie, 0); + } + + public DownstreamChannel(HttpURI location, String cookie, long sequence) { + super(sequence); + this.cookie = cookie; + this.location = location; + this.decoder = new WebSocketEmulatedDecoderImpl(); + + attemptProxyModeFallback.set(!location.isSecure()); + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandler.java new file mode 100755 index 0000000..b8979a6 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandler.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.util.HttpURI; + +interface DownstreamHandler { + void processConnect(DownstreamChannel downstreamChannel, HttpURI downstreamUri); + public void processClose(DownstreamChannel channel); + void setListener(DownstreamHandlerListener downstreamHandlerListener); + void setNextHandler(HttpRequestHandler nextHandler); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerFactory.java new file mode 100755 index 0000000..4de03bf --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +interface DownstreamHandlerFactory { + DownstreamHandler createDownstreamHandler(); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerImpl.java new file mode 100755 index 0000000..15a72aa --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerImpl.java @@ -0,0 +1,392 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import static org.kaazing.gateway.client.impl.Channel.HEADER_SEQUENCE; + +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.Timer; +import java.util.TimerTask; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.DecoderInput; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestListener; +import org.kaazing.gateway.client.impl.http.HttpRequestTransportHandler; +import org.kaazing.gateway.client.impl.http.HttpResponse; +import org.kaazing.gateway.client.impl.ws.CloseCommandMessage; +import org.kaazing.gateway.client.impl.ws.WebSocketReAuthenticateHandler; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +/* + * WebSocket Emulated Handler Chain + * EmulateHandler + * |- CreateHandler - HttpRequestAuthenticationHandler - HttpRequestRedirectHandler - HttpRequestBridgeHandler + * |- UpstreamHandler - HttpRequestBridgeHandler + * |- {DownstreamHandler} - HttpRequestBridgeHandler + * Responsibilities: + * a). process receiving messages + */ +class DownstreamHandlerImpl implements DownstreamHandler { + + private static final String CLASS_NAME = DownstreamHandlerImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static String IDLE_TIMEOUT_HEADER = "X-Idle-Timeout"; + + static DownstreamHandlerFactory FACTORY = new DownstreamHandlerFactory() { + @Override + public DownstreamHandler createDownstreamHandler() { + return new DownstreamHandlerImpl(); + } + }; + + private static final int PROXY_MODE_TIMEOUT_MILLIS = 5000; + static boolean DISABLE_FALLBACK = false; + + private HttpRequestHandler nextHandler; + private DownstreamHandlerListener listener; + + DownstreamHandlerImpl() { + LOG.entering(CLASS_NAME, ""); + + HttpRequestHandler transportHandler = HttpRequestTransportHandler.DEFAULT_FACTORY.createHandler(); + setNextHandler(transportHandler); + } + + @Override + public void processConnect(final DownstreamChannel channel, final HttpURI uri) { + LOG.entering(CLASS_NAME, "processConnect"); + makeRequest(channel, uri); + } + + private void makeRequest(final DownstreamChannel channel, final HttpURI uri) { + LOG.entering(CLASS_NAME, "makeRequest"); + + try { + // Cancel idle timer if running + stopIdleTimer(channel); + + HttpURI requestUri = HttpURI.replaceScheme(uri.getURI(), uri.getScheme().replaceAll("ws", "http")); + HttpRequest request = HttpRequest.HTTP_REQUEST_FACTORY.createHttpRequest(Method.POST, requestUri, true); + request.parent = channel; + channel.outstandingRequests.add(request); + + if (channel.cookie != null) { + request.setHeader(WebSocketEmulatedHandler.HEADER_COOKIE, channel.cookie); + } + + // Annotate request with sequence number + request.setHeader(HEADER_SEQUENCE, Long.toString(channel.nextSequence())); + + nextHandler.processOpen(request); + + // Note: attemptProxyModeFallback is only set on the channel for http, not https, + // since attempting detection for HTTPS can also lead to problems if SSL handshake + // takes more than 5 seconds to complete + if (!DISABLE_FALLBACK && channel.attemptProxyModeFallback.get()) { + TimerTask timerTask = new TimerTask() { + + @Override + public void run() { + fallbackToProxyMode(channel); + } + }; + Timer t = new Timer("ProxyModeFallback", true); + t.schedule(timerTask, PROXY_MODE_TIMEOUT_MILLIS); + } + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + listener.downstreamFailed(channel, e); + } + } + + private void fallbackToProxyMode(DownstreamChannel channel) { + if (channel.attemptProxyModeFallback.get()) { + LOG.fine("useProxyMode"); + + channel.attemptProxyModeFallback.set(false); + + HttpURI uri = channel.location; + if (uri.getQuery() == null || !uri.getQuery().contains(".ki=p")) { + uri = channel.location.addQueryParameter(".ki=p"); + channel.location = uri; + } + + makeRequest(channel, uri); + } + } + + private void reconnectIfNecessary(DownstreamChannel channel) { + LOG.entering(CLASS_NAME, "reconnectIfNecessary"); + + if (channel.closing.get() == true) { + if (channel.outstandingRequests.size() == 0) { + LOG.fine("Closing: "+channel); + listener.downstreamClosed(channel); + } + + } + else if (channel.reconnecting.compareAndSet(true, false)) { + // reconnect if necessary + LOG.fine("Reconnecting: "+channel); + makeRequest(channel, channel.location); + } else { + LOG.fine("Downstream failed: "+channel); + listener.downstreamFailed(channel, new Exception("Connection closed abruptly")); + } + } + + //------------------------------Idle Timer Start/Stop/Handler---------------------// + + private void startIdleTimer(final DownstreamChannel downstreamChannel, int delayInMilliseconds) { + LOG.fine("Starting idle timer"); + if (downstreamChannel.idleTimer != null) { + downstreamChannel.idleTimer.cancel(); + downstreamChannel.idleTimer = null; + } + + downstreamChannel.idleTimer = new Timer(); + downstreamChannel.idleTimer.schedule(new TimerTask() { + + @Override + public void run() { + idleTimerHandler(downstreamChannel); + } + + }, delayInMilliseconds); + } + + private void idleTimerHandler(DownstreamChannel downstreamChannel) { + LOG.fine("Idle timer scheduled"); + int idleDuration = (int)(System.currentTimeMillis() - downstreamChannel.lastMessageTimestamp.get()); + if (idleDuration > downstreamChannel.idleTimeout.get()) { + String message = "idle duration - " + idleDuration + " exceeded idle timeout - " + downstreamChannel.idleTimeout; + LOG.fine(message); + Exception exception = new Exception(message); + listener.downstreamFailed(downstreamChannel, exception); + } + else { + // Reschedule timer + startIdleTimer(downstreamChannel, downstreamChannel.idleTimeout.get() - idleDuration); + } + } + + private void stopIdleTimer(DownstreamChannel downstreamChannel) { + LOG.fine("Stopping idle timer"); + if (downstreamChannel.idleTimer != null) { + downstreamChannel.idleTimer.cancel(); + downstreamChannel.idleTimer = null; + } + } + //-------------------------------------------------------------------------------// + + DecoderInput in = new DecoderInput() { + + @Override + public WrappedByteBuffer read(DownstreamChannel channel) { + return channel.buffersToRead.poll(); + } + }; + + //KG-6984 move decoder into DownstreamChannel - persist state information for each websocket downstream + //private WebSocketEmulatedDecoder decoder = new WebSocketEmulatedDecoderImpl(); + + private synchronized void processProgressEvent(DownstreamChannel channel, WrappedByteBuffer buffer) { + LOG.entering(CLASS_NAME, "processProgressEvent", buffer); + try { + + // update timestamp that is used to record the timestamp of last received message + channel.lastMessageTimestamp.set(System.currentTimeMillis()); + channel.buffersToRead.add(buffer); + + WebSocketEmulatedDecoderListener decoderListener = new WebSocketEmulatedDecoderListener() { + + @Override + public void messageDecoded(DownstreamChannel channel, WrappedByteBuffer message) { + processMessage(channel, message); + } + + @Override + public void messageDecoded(DownstreamChannel channel, String message) { + processMessage(channel, message); + } + + @Override + public void commandDecoded(DownstreamChannel channel, WrappedByteBuffer command) { + int commandByte = command.array()[0]; + if (commandByte == 0x30 && command.array()[1] == 0x31) { //reconnect + // KG-5615: Set flag - but do not reconnect until request has loaded + LOG.fine("Reconnect command"); + channel.reconnecting.set(true); + } + else if (commandByte == 0x30 && command.array()[1] == 0x32) { + channel.closing.set(true); + + // Cancel the idle timer if running + stopIdleTimer(channel); + + //close frame received + int code = CloseCommandMessage.CLOSE_NO_STATUS; + String reason = null; + command.skip(2); //skip first 2 bytes 0x30, 0x32 + if (command.hasRemaining()) { + code = command.getShort(); + } + if (command.hasRemaining()) { + reason = command.getString(Charset.forName("UTF-8")); + } + CloseCommandMessage message = new CloseCommandMessage(code, reason); + listener.commandMessageReceived(channel, message); + } + } + + @Override + public void pingReceived(DownstreamChannel channel) { + listener.pingReceived(channel); + } + }; + + // Prevent multiple threads from entering + synchronized (channel.decoder) { + channel.decoder.decode(channel, in, decoderListener); + } + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + e.printStackTrace(); + + listener.downstreamFailed(channel, e); + } + } + + private void processMessage(DownstreamChannel channel, String message) { + listener.textMessageReceived(channel, message); + } + + private void processMessage(DownstreamChannel channel, WrappedByteBuffer message) { + listener.binaryMessageReceived(channel, message); + } + + void handleReAuthenticationRequested(DownstreamChannel channel, String location, String challenge) { + LOG.entering(CLASS_NAME, "handleAuthenticationRequested"); + + //handle revalidate event + String url = channel.location.getScheme() + "://" + channel.location.getURI().getAuthority() + location; + WebSocketReAuthenticateHandler reAuthHandler = new WebSocketReAuthenticateHandler(); + try { + WebSocketEmulatedChannel parent = (WebSocketEmulatedChannel)channel.getParent(); + if (parent.redirectUri != null) { + //this connection has been redirected to cluster member + url = parent.redirectUri.getScheme() + "://" + parent.redirectUri.getURI().getAuthority() + location; + } + WebSocketEmulatedChannel revalidateChannel = new WebSocketEmulatedChannel(parent.getLocation()); + revalidateChannel.redirectUri = parent.redirectUri; + revalidateChannel.setParent(parent.getParent()); + reAuthHandler.processOpen(revalidateChannel, new HttpURI(url)); + } catch (URISyntaxException ex) { + LOG.log(Level.SEVERE, null, ex); + } + return; + } + + @Override + public void processClose(DownstreamChannel channel) { + LOG.entering(CLASS_NAME, "stop"); + for (HttpRequest request : channel.outstandingRequests) { + nextHandler.processAbort(request); + } + } + + @Override + public void setNextHandler(HttpRequestHandler handler) { + this.nextHandler = handler; + + handler.setListener(new HttpRequestListener() { + + @Override + public void requestReady(HttpRequest request) { + nextHandler.processSend(request, WrappedByteBuffer.wrap(">|<".getBytes())); + } + + @Override + public void requestOpened(HttpRequest request) { + HttpResponse response = request.getResponse(); + if (response != null) { + DownstreamChannel channel = (DownstreamChannel) request.parent; + channel.attemptProxyModeFallback.set(false); + String idleTimeoutString = response.getHeader(IDLE_TIMEOUT_HEADER); + if (idleTimeoutString != null) { + int idleTimeout = Integer.parseInt(idleTimeoutString); + if (idleTimeout > 0) { + + // save in milliseconds + idleTimeout = idleTimeout * 1000; + channel.idleTimeout.set(idleTimeout); + channel.lastMessageTimestamp.set(System.currentTimeMillis()); + startIdleTimer(channel, idleTimeout); + } + } + listener.downstreamOpened(channel); + } + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + DownstreamChannel channel = (DownstreamChannel) request.parent; + processProgressEvent(channel, payload); + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + LOG.entering(CLASS_NAME, "requestLoaded", request); + DownstreamChannel channel = (DownstreamChannel) request.parent; + channel.outstandingRequests.remove(request); + reconnectIfNecessary(channel); + } + + @Override + public void requestClosed(HttpRequest request) { + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + LOG.entering(CLASS_NAME, "errorOccurred", request); + DownstreamChannel channel = (DownstreamChannel) request.parent; + listener.downstreamFailed(channel, exception); + } + + @Override + public void requestAborted(HttpRequest request) { + LOG.entering(CLASS_NAME, "errorOccurred", request); + } + }); + } + + public void setListener(DownstreamHandlerListener listener) { + this.listener = listener; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerListener.java new file mode 100755 index 0000000..5f0eb07 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/DownstreamHandlerListener.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public interface DownstreamHandlerListener { + + public void downstreamOpened(DownstreamChannel channel); + public void binaryMessageReceived(DownstreamChannel channel, WrappedByteBuffer data); + public void textMessageReceived(DownstreamChannel channel, String text); + public void commandMessageReceived(DownstreamChannel channel, CommandMessage message); + public void downstreamFailed(DownstreamChannel channel, Exception exception); + public void downstreamClosed(DownstreamChannel channel); + public void pingReceived(DownstreamChannel channel); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamChannel.java new file mode 100755 index 0000000..fcae06e --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamChannel.java @@ -0,0 +1,51 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.kaazing.gateway.client.impl.Channel; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +class UpstreamChannel extends Channel { + HttpURI location; + String cookie; + + ConcurrentLinkedQueue sendQueue = new ConcurrentLinkedQueue(); + AtomicBoolean sendInFlight = new AtomicBoolean(false); + HttpRequest request; + + WebSocketEmulatedChannel parent; + + public UpstreamChannel(HttpURI location, String cookie) { + this(location, cookie, 0); + } + + UpstreamChannel(HttpURI location, String cookie, long sequence) { + super(sequence); + this.location = location; + this.cookie = cookie; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandler.java new file mode 100755 index 0000000..8e142ee --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandler.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +interface UpstreamHandler { + + void setListener(UpstreamHandlerListener upstreamHandlerListener); + void processOpen(UpstreamChannel channel); + void processClose(UpstreamChannel upstreamChannel, int code, String reason); + void processTextMessage(UpstreamChannel upstreamChannel, String message); + void processBinaryMessage(UpstreamChannel upstreamChannel, WrappedByteBuffer message); + void setNextHandler(HttpRequestHandler nextHandler); + void processPong(UpstreamChannel upstreamChannel); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerFactory.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerFactory.java new file mode 100755 index 0000000..536375c --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerFactory.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +interface UpstreamHandlerFactory { + UpstreamHandler createUpstreamHandler(); +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerImpl.java new file mode 100755 index 0000000..84e33cd --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerImpl.java @@ -0,0 +1,225 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import static org.kaazing.gateway.client.impl.Channel.HEADER_SEQUENCE; + +import java.nio.charset.Charset; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.EncoderOutput; +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestListener; +import org.kaazing.gateway.client.impl.http.HttpRequestTransportHandler; +import org.kaazing.gateway.client.impl.http.HttpResponse; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.impl.ws.CloseCommandMessage; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +/* + * WebSocket Emulated Handler Chain + * EmulateHandler + * |- CreateHandler - HttpRequestAuthenticationHandler - HttpRequestRedirectHandler - HttpRequestBridgeHandler + * |- {UpstreamHandler} - HttpRequestBridgeHandler + * |- DownstreamHandler - HttpRequestBridgeHandler + * Responsibilities: + * a). process send messages + * + * TODO: + * n/a + */ +class UpstreamHandlerImpl implements UpstreamHandler { + + static final String CLASS_NAME = UpstreamHandlerImpl.class.getName(); + static final Logger LOG = Logger.getLogger(CLASS_NAME); + + static UpstreamHandlerFactory FACTORY = new UpstreamHandlerFactory() { + @Override + public UpstreamHandler createUpstreamHandler() { + return new UpstreamHandlerImpl(); + } + }; + + // command frame with 02 (close) instruction + private static final byte WSF_COMMAND_FRAME_START = (byte) 0x01; + private static final byte WSF_COMMAND_FRAME_END = (byte) 0xff; + private static final byte WSE_PONG_FRAME_CODE = (byte) 0x8A; + private static final byte[] RECONNECT_EVENT_BYTES = { WSF_COMMAND_FRAME_START, 0x30, 0x31, WSF_COMMAND_FRAME_END }; + private static final byte[] CLOSE_EVENT_BYTES = { WSF_COMMAND_FRAME_START, 0x30, 0x32, WSF_COMMAND_FRAME_END }; + + WebSocketEmulatedEncoder encoder = new WebSocketEmulatedEncoderImpl(); + private EncoderOutput out = new EncoderOutput() { + @Override + public void write(UpstreamChannel channel, WrappedByteBuffer buf) { + processMessageWrite(channel, buf); + } + }; + + HttpRequestHandler nextHandler; + UpstreamHandlerListener listener; + + UpstreamHandlerImpl() { + HttpRequestHandler transportHandler = HttpRequestTransportHandler.DEFAULT_FACTORY.createHandler(); + setNextHandler(transportHandler); + } + + @Override + public void setNextHandler(HttpRequestHandler handler) { + nextHandler = handler; + + nextHandler.setListener(new HttpRequestListener() { + + @Override + public void requestReady(HttpRequest request) { + UpstreamChannel channel = (UpstreamChannel)request.parent; + ConcurrentLinkedQueue sendQueue = channel.sendQueue; + + // build up a bigger payload from all queued up payloads + WrappedByteBuffer payload = WrappedByteBuffer.allocate(1024); + while (!sendQueue.isEmpty()) { + payload.putBuffer(sendQueue.poll()); + } + + // reconnect event bytes *required* to terminate upstream + payload.putBytes(RECONNECT_EVENT_BYTES); + payload.flip(); + + nextHandler.processSend(request, payload); + } + + @Override + public void requestOpened(HttpRequest request) { + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + UpstreamChannel channel = (UpstreamChannel)request.parent; + channel.sendInFlight.set(false); + if (!channel.sendQueue.isEmpty()) { + flushIfNecessary(channel); + } + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + UpstreamChannel channel = (UpstreamChannel)request.parent; + channel.sendInFlight.set(false); + listener.upstreamFailed(channel, exception); + } + + @Override + public void requestAborted(HttpRequest request) { + } + + @Override + public void requestClosed(HttpRequest request) { + } + }); + } + + @Override + public void processOpen(UpstreamChannel channel) { + } + + @Override + public void processTextMessage(UpstreamChannel channel, String message) { + LOG.entering(CLASS_NAME, "processsTextMessage", message); + encoder.encodeTextMessage(channel, message, out); + } + + @Override + public void processBinaryMessage(UpstreamChannel channel, WrappedByteBuffer message) { + LOG.entering(CLASS_NAME, "processsBinaryMessage", message); + encoder.encodeBinaryMessage(channel, message, out); + } + + private void processMessageWrite(UpstreamChannel channel, WrappedByteBuffer payload) { + LOG.entering(CLASS_NAME, "processMessageWrite", payload); + // queue the post request, even if another thread is + // in the middle of sending a request - in which case + // this request will piggy back on it + channel.sendQueue.offer(payload); + + flushIfNecessary(channel); + } + + private void flushIfNecessary(final UpstreamChannel channel) { + LOG.entering(CLASS_NAME, "flushIfNecessary"); + if (channel.sendInFlight.compareAndSet(false, true)) { + final HttpRequest request = HttpRequest.HTTP_REQUEST_FACTORY.createHttpRequest(Method.POST, channel.location, false); + request.setHeader(WebSocketEmulatedHandler.HEADER_CONTENT_TYPE, "application/octet-stream"); + if (channel.cookie != null) { + request.setHeader(WebSocketEmulatedHandler.HEADER_COOKIE, channel.cookie); + } + // Annotate request with sequence number + request.setHeader(HEADER_SEQUENCE, Long.toString(channel.nextSequence())); + request.parent = channel; + channel.request = request; + + nextHandler.processOpen(request); + } + } + + @Override + public void processClose(UpstreamChannel channel, int code, String reason) { + // ### TODO: This is temporary till Gateway is ready for the CLOSE frame. + // Till then, we will just set code to zero and NOT send + // the CLOSE frame. + code = 0; + + // send close event + + if (code == 0 || code == CloseCommandMessage.CLOSE_NO_STATUS) { + processMessageWrite(channel, WrappedByteBuffer.wrap(CLOSE_EVENT_BYTES)); + } + else { + WrappedByteBuffer buf = new WrappedByteBuffer(); + buf.put(CLOSE_EVENT_BYTES, 0, 3); + buf.putShort((short)code); //put code - 2 bytes + buf.putString(reason, Charset.forName("UTF-8")); + buf.put(WSF_COMMAND_FRAME_END); + buf.flip(); + processMessageWrite(channel, buf); + } + } + + @Override + public void setListener(UpstreamHandlerListener listener) { + this.listener = listener; + } + + @Override + public void processPong(UpstreamChannel upstreamChannel) { + + // The wire representation of PONG is - 0x8a 0x00 + WrappedByteBuffer pongBuffer = WrappedByteBuffer.allocate(2); + pongBuffer.put(WSE_PONG_FRAME_CODE); + pongBuffer.put((byte)0x00); + pongBuffer.flip(); + processMessageWrite(upstreamChannel, pongBuffer); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerListener.java new file mode 100755 index 0000000..d1c6ce0 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/UpstreamHandlerListener.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +public interface UpstreamHandlerListener { + + void upstreamFailed(UpstreamChannel channel, Exception exception); + void upstreamCompleted(UpstreamChannel channel); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedChannel.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedChannel.java new file mode 100755 index 0000000..973d0e0 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedChannel.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.ws.CloseCommandMessage; +import org.kaazing.gateway.client.impl.ws.WebSocketSelectedChannel; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.HttpURI; + +public class WebSocketEmulatedChannel extends WebSocketSelectedChannel { + + public HttpURI redirectUri; + CreateChannel createChannel; + UpstreamChannel upstreamChannel; + DownstreamChannel downstreamChannel; + + protected String cookie = null; + + /* close event */ + boolean wasCleanClose = false; + int closeCode = CloseCommandMessage.CLOSE_ABNORMAL; + String closeReason = ""; + + public WebSocketEmulatedChannel(WSURI location) { + super(location); + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoder.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoder.java new file mode 100755 index 0000000..7800717 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoder.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.DecoderInput; + +public interface WebSocketEmulatedDecoder { + + void decode(C channel, DecoderInput in, WebSocketEmulatedDecoderListener listener); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderImpl.java new file mode 100755 index 0000000..dc33b9d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderImpl.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import java.nio.charset.Charset; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.DecoderInput; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketEmulatedDecoderImpl implements WebSocketEmulatedDecoder { + + private static final String CLASS_NAME = WebSocketEmulatedDecoderImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private static final byte WSF_COMMAND_FRAME_START = (byte) 0x01; + private static final byte WS_TEXT_FRAME_END = (byte) 0xff; + private static final byte WS_BINARY_FRAME_START = (byte) 0x80; + private static final byte WS_SPECIFIEDLENGTH_TEXT_FRAME_START = (byte) 0x81; + private static final byte WSE_PING_FRAME_CODE = (byte) 0x89; + + /* + * Processing state machine + */ + enum DecodingState { + START_OF_FRAME, + READING_TEXT_FRAME, + READING_COMMAND_FRAME, + READING_BINARY_FRAME_HEADER, + READING_BINARY_FRAME, + READING_PING_FRAME + }; + + private DecodingState processingState = DecodingState.START_OF_FRAME; + private WrappedByteBuffer readBuffer = null; + private WrappedByteBuffer messageBuffer = null; + private int binaryFrameLength = 0; + private byte opCode = 0; + + @Override + public void decode(C channel, DecoderInput in, WebSocketEmulatedDecoderListener listener) { + LOG.fine("process() START"); + + while (true) { + if (readBuffer == null) { + readBuffer = in.read(channel); + if (readBuffer == null) { + break; + } + } + + if (!readBuffer.hasRemaining()) { + readBuffer = null; + continue; + } + + if (processingState == DecodingState.START_OF_FRAME) { + // handle alignment with start-of-frame boundary (after mark) + + opCode = readBuffer.get(); + + if (opCode == WSF_COMMAND_FRAME_START) { + processingState = DecodingState.READING_COMMAND_FRAME; + messageBuffer = WrappedByteBuffer.allocate(512); + } + else if (opCode == WSE_PING_FRAME_CODE) { + processingState = DecodingState.READING_PING_FRAME; + } + else if (opCode == WS_BINARY_FRAME_START || opCode == WS_SPECIFIEDLENGTH_TEXT_FRAME_START) { + processingState = DecodingState.READING_BINARY_FRAME_HEADER; + binaryFrameLength = 0; + } + } + else if (processingState == DecodingState.READING_COMMAND_FRAME) { + int endOfFrameAt = readBuffer.indexOf(WS_TEXT_FRAME_END); + if (endOfFrameAt == -1) { + int numBytes = readBuffer.remaining(); + //LOG.finest("process() TEXT_FRAME: partial: "+numBytes); + messageBuffer.putBuffer(readBuffer); + + //KG-6984 putBuffer already move position in readBuffer, no skip required + //readBuffer.skip(numBytes); + } + else { + // complete payload + maybe next payload + int dataLength = endOfFrameAt - readBuffer.position(); + messageBuffer.putBytes(readBuffer.getBytes(dataLength)); // Should advance both buffers + readBuffer.skip(1); //skip endOfFrame byte + + boolean isCommandFrame = (processingState == DecodingState.READING_COMMAND_FRAME); + processingState = DecodingState.START_OF_FRAME; + + // is this a command frame + if (isCommandFrame) { + //LOG.finest("process() COMMAND_FRAME"); + messageBuffer.flip(); + if (messageBuffer.array()[0] == 0x30 && messageBuffer.array()[1] == 0x30) { + //NOOP_COMMAND: + // ignore + } + else { + listener.commandDecoded(channel, messageBuffer.duplicate()); + } + } + // otherwise it is a text frame + else { + // deliver the text frame + messageBuffer.flip(); + + String text = messageBuffer.getString(UTF8); + listener.messageDecoded(channel, text); + } + } + } + else if (processingState == DecodingState.READING_BINARY_FRAME_HEADER) { + while (readBuffer.hasRemaining()) { + byte b = readBuffer.get(); + binaryFrameLength <<= 7; + binaryFrameLength |= (b & 0x7f); + if ((b & 0x80) != 0x80) { + //LOG.finest("process() BINARY_FRAME_HEADER: " + binaryFrameLength); + processingState = DecodingState.READING_BINARY_FRAME; + messageBuffer = WrappedByteBuffer.allocate(binaryFrameLength); + break; + } + } + } + else if (processingState == DecodingState.READING_BINARY_FRAME) { + if (readBuffer.remaining() < binaryFrameLength) { + // incomplete payload + int numbytes = readBuffer.remaining(); + messageBuffer.putBuffer(readBuffer); + binaryFrameLength -= numbytes; + //LOG.finest("process() BINARY_FRAME: partial: " + numbytes); + } + else { + //completed payload + maybe next payload + messageBuffer.putBytes(readBuffer.getBytes(binaryFrameLength)); + processingState = DecodingState.START_OF_FRAME; + + // deliver the binary frame + messageBuffer.flip(); + if (opCode == WS_SPECIFIEDLENGTH_TEXT_FRAME_START) { + String text = messageBuffer.getString(UTF8); + listener.messageDecoded(channel, text); + } else if (opCode == WS_BINARY_FRAME_START){ + listener.messageDecoded(channel, messageBuffer); + } + else { + throw new IllegalArgumentException("Invalid frame opcode. opcode = " + opCode); + } + } + } + else if (processingState == DecodingState.READING_PING_FRAME) { + byte byteFollowingPingFrameCode = readBuffer.get(); + processingState = DecodingState.START_OF_FRAME; + + if (byteFollowingPingFrameCode != 0x00) { + throw new IllegalArgumentException("Expected 0x00 after the PING frame code but received - " + byteFollowingPingFrameCode); + } + + listener.pingReceived(channel); + } + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderListener.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderListener.java new file mode 100755 index 0000000..6bff0a0 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedDecoderListener.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.DecoderListener; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +//TODO: Create a Command abstraction instead of passing WrappedByteBuffer +public interface WebSocketEmulatedDecoderListener extends DecoderListener { + + void commandDecoded(C channel, WrappedByteBuffer command); + void pingReceived(C channel); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoder.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoder.java new file mode 100755 index 0000000..c550cc3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoder.java @@ -0,0 +1,32 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.EncoderOutput; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public interface WebSocketEmulatedEncoder { + + void encodeTextMessage(C channel, String message, EncoderOutput out); + void encodeBinaryMessage(C channel, WrappedByteBuffer message, EncoderOutput out); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoderImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoderImpl.java new file mode 100755 index 0000000..bac1534 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedEncoderImpl.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; + +import org.kaazing.gateway.client.impl.EncoderOutput; +import org.kaazing.gateway.client.impl.util.WebSocketUtil; +import org.kaazing.gateway.client.util.StringUtils; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketEmulatedEncoderImpl implements WebSocketEmulatedEncoder { + + private static final byte WS_BINARY_FRAME_START = (byte) 0x80; + private static final byte WS_SPECIFIED_LENGTH_TEXT_FRAME_START = (byte) 0x81; + + @Override + public void encodeBinaryMessage(C channel, WrappedByteBuffer message, EncoderOutput out) { + int length = message.remaining(); + + // The largest frame that can be received is 5 bytes (encoded 32 bit length header + trailing byte) + WrappedByteBuffer frame = WrappedByteBuffer.allocate(length + 6); + frame.put(WS_BINARY_FRAME_START); // write binary type header + WebSocketUtil.encodeLength(frame, length); // write length prefix + frame.putBuffer(message.duplicate()); // write payload + frame.flip(); + + out.write(channel, frame); + } + + @Override + public void encodeTextMessage(C channel, String message, EncoderOutput out) { + byte[] payload = StringUtils.getUtf8Bytes(message); + int length = payload.length; + // The largest frame that can be received is 5 bytes (encoded 32 bit length header + trailing byte) + WrappedByteBuffer frame = WrappedByteBuffer.allocate(length + 6); + frame.put(WS_SPECIFIED_LENGTH_TEXT_FRAME_START); // write binary type header + WebSocketUtil.encodeLength(frame, length); // write length prefix + frame.putBytes(payload); // write payload + frame.flip(); + + out.write(channel, frame); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedHandler.java new file mode 100755 index 0000000..c29322d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wseb/WebSocketEmulatedHandler.java @@ -0,0 +1,341 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wseb; +/* + * WebSocket Emulated Handler Chain + * {EmulateHandler} + * |- CreateHandler - HttpRequestAuthenticationHandler - HttpRequestRedirectHandler - HttpRequestBridgeHandler + * |- UpstreamHandler - HttpRequestBridgeHandler + * |- DownstreamHandler - HttpRequestBridgeHandler + * Responsibilities: + * a). process Connect + * build handler chain + * start createHandler.processOpen + * on connect, build upstreamHandler and downstreamHandler + * b). process close + * send Close frame via upstream + * fire connectionClosed event + * TODO: + * n/a + */ +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.ws.CloseCommandMessage; +import org.kaazing.gateway.client.impl.ws.ReadyState; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketEmulatedHandler extends WebSocketHandlerAdapter { + + static final String CLASS_NAME = WebSocketEmulatedHandler.class.getName(); + static final Logger LOG = Logger.getLogger(CLASS_NAME); + + static final String HEADER_CONTENT_TYPE = "Content-Type"; + static final String HEADER_COOKIE = "Cookie"; + static final String HEADER_SET_COOKIE = "Set-Cookie"; + + static final Charset UTF_8 = Charset.forName("UTF-8"); + + static CreateHandlerFactory createHandlerFactory = CreateHandlerImpl.FACTORY; + static DownstreamHandlerFactory downstreamHandlerFactory = DownstreamHandlerImpl.FACTORY; + static UpstreamHandlerFactory upstreamHandlerFactory = UpstreamHandlerImpl.FACTORY; + + private final CreateHandler createHandler = createHandlerFactory.createCreateHandler(); + private final UpstreamHandler upstreamHandler = upstreamHandlerFactory.createUpstreamHandler(); + private final DownstreamHandler downstreamHandler = downstreamHandlerFactory.createDownstreamHandler(); + + public WebSocketEmulatedHandler() { + LOG.entering(CLASS_NAME, ""); + initCreateHandler(createHandler); + initUpstreamHandler(upstreamHandler); + initDownstreamHandler(downstreamHandler); + } + + void initCreateHandler(CreateHandler handler) { + + handler.setListener(new CreateHandlerListener() { + + @Override + public void createCompleted(CreateChannel channel, HttpURI upstreamUri, HttpURI downstreamUri, String protocol) { + LOG.entering(CLASS_NAME, "createCompleted"); + + WebSocketEmulatedChannel parent = (WebSocketEmulatedChannel)channel.getParent(); + parent.createChannel = null; + parent.setProtocol(protocol); + + long nextSequence = channel.nextSequence(); + + UpstreamChannel upstreamChannel = new UpstreamChannel(upstreamUri, channel.cookie, nextSequence); + upstreamChannel.setParent(parent); + parent.upstreamChannel = upstreamChannel; + + DownstreamChannel downstreamChannel = new DownstreamChannel(downstreamUri, channel.cookie, nextSequence); + downstreamChannel.setParent(parent); + parent.downstreamChannel = downstreamChannel; + + parent.cookie = channel.cookie; + + downstreamHandler.processConnect(parent.downstreamChannel, downstreamUri); + listener.connectionOpened(parent, protocol); + } + + @Override + public void createFailed(CreateChannel channel, Exception exception) { + LOG.entering(CLASS_NAME, "createFailed"); + + WebSocketEmulatedChannel parent = (WebSocketEmulatedChannel)channel.getParent(); + listener.connectionFailed(parent, exception); + } + }); + + } + + void initUpstreamHandler(UpstreamHandler handler) { + + handler.setListener(new UpstreamHandlerListener() { + + @Override + public void upstreamCompleted(UpstreamChannel channel) { + } + + @Override + public void upstreamFailed(UpstreamChannel channel, Exception exception) { + if (channel != null && channel.parent != null) { + WebSocketEmulatedChannel parent = channel.parent; + parent.upstreamChannel = null; + doError(parent, exception); + } + else { + throw new IllegalStateException("WebSocket upstream channel already closed"); + } + } + + }); + + } + + void initDownstreamHandler(DownstreamHandler handler) { + + handler.setListener(new DownstreamHandlerListener() { + + @Override + public void downstreamOpened(DownstreamChannel channel) { + + } + + @Override + public void binaryMessageReceived(DownstreamChannel channel, WrappedByteBuffer data) { + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel.getParent(); + listener.binaryMessageReceived(wsebChannel, data); + } + + @Override + public void textMessageReceived(DownstreamChannel channel, String text) { + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel.getParent(); + listener.textMessageReceived(wsebChannel, text); + } + + @Override + public void downstreamFailed(DownstreamChannel channel, Exception exception) { + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel.getParent(); + doError(wsebChannel, exception); + } + + @Override + public void downstreamClosed(DownstreamChannel channel) { + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel.getParent(); + doClose(wsebChannel); + } + + @Override + public void commandMessageReceived(DownstreamChannel channel, CommandMessage message) { + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel.getParent(); + if (message instanceof CloseCommandMessage) { + //close frame received, save code and reason + CloseCommandMessage msg = (CloseCommandMessage) message; + wsebChannel.wasCleanClose = true; + wsebChannel.closeCode = msg.getCode(); + wsebChannel.closeReason = msg.getReason(); + + if (wsebChannel.getReadyState() == ReadyState.OPEN) { + //server initiated close, echo close command message + upstreamHandler.processClose(wsebChannel.upstreamChannel, msg.getCode(), msg.getReason()); + } + + } + listener.commandMessageReceived(wsebChannel, message); + } + + @Override + public void pingReceived(DownstreamChannel channel) { + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel.getParent(); + + // Server sent PING, reponse with PONG via upstream handler + upstreamHandler.processPong(wsebChannel.upstreamChannel); + } + }); + + } + + @Override + public synchronized void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + LOG.entering(CLASS_NAME, "connect", channel); + + String path = location.getPath(); + if (path.endsWith("/")) { + // eliminate duplicate slash when appending create suffix + path = path.substring(0, path.length()-1); + } + + try { + CreateChannel createChannel = new CreateChannel(); + createChannel.setParent(channel); + createChannel.setProtocols(protocols); + HttpURI createUri = HttpURI.replaceScheme(location, location.getHttpEquivalentScheme()) + .replacePath(path + "/;e/cbm"); + createHandler.processOpen(createChannel, createUri); + + } catch (Exception e) { + LOG.log(Level.FINE, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + } + + @Override + public synchronized void processClose(WebSocketChannel channel, int code, String reason) { + LOG.entering(CLASS_NAME, "processDisconnect"); + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel; + + // ### TODO: This is temporary till Gateway sends us the CLOSE frame + // with code and reason while closing emulated downstream + // connection. + wsebChannel.closeCode = code; + wsebChannel.closeReason = reason; + + upstreamHandler.processClose(wsebChannel.upstreamChannel, code, reason); + + } + + @Override + public void processTextMessage(WebSocketChannel channel, String message) { + LOG.entering(CLASS_NAME, "processTextMessage", message); + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel; + upstreamHandler.processTextMessage(wsebChannel.upstreamChannel, message); + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer message) { + LOG.entering(CLASS_NAME, "processBinaryMessage", message); + WebSocketEmulatedChannel wsebChannel = (WebSocketEmulatedChannel)channel; + upstreamHandler.processBinaryMessage(wsebChannel.upstreamChannel, message); + } + + private void doError(WebSocketEmulatedChannel channel, Exception exception) + { + LOG.entering(CLASS_NAME, "Error handler. Tearing down WebSocket connection."); + try + { + if (channel.createChannel != null) + { + createHandler.processClose(channel.createChannel); + } + + if (channel.downstreamChannel != null) + { + downstreamHandler.processClose(channel.downstreamChannel); + } + } + catch (Exception e) + { + LOG.entering(CLASS_NAME, "Exception while tearing down the connection: " + e.getMessage()); + } + + LOG.entering(CLASS_NAME, "Firing Close Event"); + try + { + listener.connectionFailed(channel, exception); + } + catch (Exception e) + { + LOG.entering(CLASS_NAME, "Unhandled exception in Close Event: " + e.getMessage()); + } + } + + private void doClose(WebSocketEmulatedChannel channel) + { + LOG.entering(CLASS_NAME, "Close"); + // TODO: the 'SelectedHandler' was already setting the _readyState on the WebSocketEmulatedChannel to CLOSED + // Commenting out the below IF statement, because it is NEVER true (since the channel state was already updated + + //if (channel._readyState == WebSocket.OPEN || channel._readyState == WebSocket.CONNECTING) + //{ + try + { + // TODO: why did we set the _readyState to CLOSE? + // The channel is passed down to the listener (see WebSocketSelectedHandler) + // and in there (like in Java) the state is set to CLOSE _and_ we continue to + // close the close/clean-up the connection. + + // Setting the state here causes that the _listener.HandleConnectionClosed() + // is doing nothing! + + //channel._readyState = WebSocket.CLOSED; + if (channel.createChannel != null) + { + createHandler.processClose(channel.createChannel); + } + if (channel.downstreamChannel != null) + { + downstreamHandler.processClose(channel.downstreamChannel); + } + } + catch (Exception e) + { + LOG.entering(CLASS_NAME, "While closing: " + e.getMessage()); + } + + LOG.entering(CLASS_NAME, "Firing Close Event"); + + try + { + // ### TODO: Till Gateway supports CLOSE frame, we are going to + // workaround with the following hardcoded values. + channel.wasCleanClose = true; + if (channel.closeCode == 0) { + channel.closeCode = CloseCommandMessage.CLOSE_NO_STATUS; + } + + listener.connectionClosed(channel, channel.wasCleanClose, channel.closeCode, channel.closeReason); + } + catch (Exception e) + { + LOG.entering(CLASS_NAME, "Unhandled exception in Close Event: " + e.getMessage()); + } + //} + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeAuthenticationHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeAuthenticationHandler.java new file mode 100755 index 0000000..4299e49 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeAuthenticationHandler.java @@ -0,0 +1,182 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import java.net.URISyntaxException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.auth.AuthenticationUtil; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.ws.WebSocketHandshakeObject; +import org.kaazing.gateway.client.impl.ws.WebSocketReAuthenticateHandler; +import org.kaazing.gateway.client.impl.wseb.WebSocketEmulatedChannel; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.auth.ChallengeRequest; +import org.kaazing.net.auth.ChallengeResponse; +import org.kaazing.net.impl.util.ResumableTimer; + +/* + * WebSocket Native Handler Chain + * NativeHandler - {AuthenticationHandler} - HandshakeHandler - ControlFrameHandler - BalanceingHandler - Nodec - BridgeHandler + * Responsibilities: + * a). handle authenticationRequested event + */ +public class WebSocketNativeAuthenticationHandler extends WebSocketHandlerAdapter { + + private static final String CLASS_NAME = WebSocketNativeAuthenticationHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private void handleAuthenticationRequested(WebSocketChannel channel, String location, String challenge) { + LOG.entering(CLASS_NAME, "handleAuthenticationRequested"); + + channel.authenticationReceived = true; + + WSURI serverURI; + WebSocketNativeChannel ch = (WebSocketNativeChannel)channel; + ResumableTimer connectTimer = null; + + if (((WebSocketCompositeChannel)channel.getParent()) != null) { + WebSocketCompositeChannel parent = (WebSocketCompositeChannel)channel.getParent(); + connectTimer = parent.getConnectTimer(); + if (connectTimer != null) { + // Pause the connect timer while the user is providing the credentials. + connectTimer.pause(); + } + } + + // get server location + if (ch.redirectUri != null) { + //this connection has been redirected + serverURI = ch.redirectUri; + } + else { + serverURI = channel.getLocation(); + } + + //handle handshake 401 - use original url as ChallengeHandler lookup + ChallengeRequest challengeRequest = new ChallengeRequest(serverURI.toString(), challenge); + try { + channel.challengeResponse = AuthenticationUtil.getChallengeResponse(channel, challengeRequest, channel.challengeResponse); + } catch (Exception e) { + clearAuthenticationCredentials(channel); + doError(channel, e); + //throw new IllegalStateException("Unexpected error processing challenge: "+challengeRequest, e); + return; + } + char[] authResponse = channel.challengeResponse.getCredentials(); + if (authResponse == null) { + doError(channel, new IllegalStateException("No response possible for challenge")); + //throw new IllegalStateException("No response possible for challenge"); + return; + } + + // Resume the connect timer before invoking processAuthorize(). + if (connectTimer != null) { + connectTimer.resume(); + } + + processAuthorize(channel, String.valueOf(authResponse)); + clearAuthenticationCredentials(channel); + } + + private void doError(WebSocketChannel channel, Exception exception) { + LOG.entering(CLASS_NAME, "handleConnectionClosed"); + this.nextHandler.processClose(channel, 1000, null); + listener.connectionClosed(channel, exception); + } + + private void clearAuthenticationCredentials(WebSocketChannel channel) { + ChallengeHandler nextChallengeHandler = null; + if (channel.challengeResponse != null) { + nextChallengeHandler = channel.challengeResponse.getNextChallengeHandler(); + channel.challengeResponse.clearCredentials(); + // prevent leak in case challengeResponse below throws an exception + channel.challengeResponse = null; + } + channel.challengeResponse = new ChallengeResponse(null, nextChallengeHandler); + } + + @Override + public void setNextHandler(WebSocketHandler handler) { + super.setNextHandler(handler); + + handler.setListener(new WebSocketHandlerListener() { + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + clearAuthenticationCredentials(channel); + listener.connectionOpened(channel, protocol); + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + clearAuthenticationCredentials(channel); + listener.redirected(channel, location); + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + handleAuthenticationRequested(channel, location, challenge); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + listener.binaryMessageReceived(channel, buf); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + listener.textMessageReceived(channel, message); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + clearAuthenticationCredentials(channel); + listener.connectionClosed(channel, wasClean, code, reason); + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + listener.connectionClosed(channel, ex); + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + clearAuthenticationCredentials(channel); + listener.connectionFailed(channel, ex); + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + } + }); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeBalancingHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeBalancingHandler.java new file mode 100755 index 0000000..280932f --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeBalancingHandler.java @@ -0,0 +1,307 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.ws.WebSocketHandshakeObject; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.StringUtils; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.http.HttpRedirectPolicy; +/* + * WebSocket Native Handler Chain + * NativeHandler - AuthenticationHandler - HandshakeHandler - ControlFrameHandler - {BalancingHandler} - Codec - BridgeHandler + * Responsibilities: + * a). handle balancer messages + * balancer message is the first message after connection is established + * if message is "\uf0ff" + 'N' - fire connectionOpen event + * if message is "\uf0ff" + 'R' + redirectURl - start reConnect process + * TODO: + * a). server will remove balancer message. instead, server will sent a 'HTTP 301' to redirect client + * client needs to change accordingly + */ +public class WebSocketNativeBalancingHandler extends WebSocketHandlerAdapter { + private static final String CLASS_NAME = WebSocketNativeBalancingHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + private static final Charset UTF8 = Charset.forName("UTF-8"); + + /** + * Connect to the WebSocket + * + * @throws Exception + */ + @Override + public void processConnect(WebSocketChannel channel, WSURI uri, String[] protocols) { + LOG.entering(CLASS_NAME, "connect", new Object[] { uri, protocols }); + WebSocketNativeChannel wsChannel = (WebSocketNativeChannel)channel; + wsChannel.balanced.set(0); + nextHandler.processConnect(channel, uri.addQueryParameter(".kl=Y"), protocols); + } + + /** + * Connect to the WebSocket + * + * @throws Exception + */ + private void reconnect(WebSocketChannel channel, WSURI uri, String protocol) { + LOG.entering(CLASS_NAME, "reconnect", new Object[] { uri, protocol }); + + WebSocketNativeChannel wsChannel = (WebSocketNativeChannel)channel; + wsChannel.redirectUri = uri; + + WebSocketCompositeChannel compChannel = (WebSocketCompositeChannel)channel.getParent(); + HttpRedirectPolicy option = compChannel.getFollowRedirect(); + URI currentURI = channel.getLocation().getURI(); + URI redirectURI = uri.getURI(); + + // option will be null only for unit tests. + if ((option != null) && (option.compare(currentURI, redirectURI) != 0)) { + String s = String.format("%s: Cannot redirect from '%s' to '%s'", + option, currentURI, redirectURI); + channel.preventFallback = true; + throw new IllegalStateException(s); + } + + wsChannel.reconnecting.compareAndSet(false, true); + } + + void handleBinaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer message) { + LOG.entering(CLASS_NAME, "handleMessageReceived", message); + WebSocketNativeChannel wsChannel = (WebSocketNativeChannel)channel; + if (wsChannel.balanced.get() <= 1 && message.remaining() >= 4) { + byte[] prefix = new byte[3]; + message.mark(); + message.get(prefix); + String prefixString; + try { + prefixString = new String(prefix, "UTF-8"); + } + catch (UnsupportedEncodingException e1) { + throw new IllegalStateException(e1); + } + if (prefixString.charAt(0) == '\uf0ff') { + int code = message.get(); + LOG.finest("Balancer code = " + code); + if (code == 'N') { + /* Balancer responded, fire open event */ + if (wsChannel.balanced.getAndIncrement() == 0) { + //first balancer message, fire kaazing handshake + listener.connectionOpened(channel, WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE); + } + else { + //second balancer message, fire open + //TODO: how to pass 'real' protocol to client? + listener.connectionOpened(channel,""); + } + return; + } + else if (code == 'R') { + try { + String reconnectLocation = message.getString(UTF8); + LOG.finest("Balancer redirect location = " + StringUtils.stripControlCharacters(reconnectLocation)); + + WSURI uri = new WSURI(reconnectLocation); + reconnect(channel, uri, channel.getProtocol()); + nextHandler.processClose(channel, 0, null); + return; + } + catch (URISyntaxException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + catch (Exception e) { + LOG.log(Level.WARNING, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + } + } + else { + message.reset(); + listener.binaryMessageReceived(wsChannel, message); + } + } + else { + listener.binaryMessageReceived(channel, message); + } + } + + void handleTextMessageReceived(WebSocketChannel channel, String message) { + LOG.entering(CLASS_NAME, "handleTextMessageReceived", message); + WebSocketNativeChannel wsChannel = (WebSocketNativeChannel)channel; + if (wsChannel.balanced.get() <= 1 && + message.length() >= 2 && + message.charAt(0) == '\uf0ff') { + + int code = message.charAt(1); + LOG.finest("Balancer code = " + code); + if (code == 'N') { + /* Balancer responded, fire open event */ + // NOTE: this will cause OPEN to fire twice on the same channel, but it is currently + // required because the Gateway sends a balancer message both before and after the + // Extended Handshake. + if (wsChannel.balanced.incrementAndGet() == 1) { + listener.connectionOpened(channel, WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE); + } + else { +// listener.connectionOpened(channel, WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE); + listener.connectionOpened(channel, ""); + } + } + else if (code == 'R') { + try { + String reconnectLocation = message.substring(2); + LOG.finest("Balancer redirect location = " + StringUtils.stripControlCharacters(reconnectLocation)); + + WSURI uri = new WSURI(reconnectLocation); + reconnect(channel, uri, channel.getProtocol()); + nextHandler.processClose(channel, 0, null); + } + catch (URISyntaxException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + catch (Exception e) { + LOG.log(Level.WARNING, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + } + else { + listener.textMessageReceived(channel, message); + } + } + else { + listener.textMessageReceived(channel, message); + } + } + + public void setNextHandler(WebSocketHandler handler) { + this.nextHandler = handler; + + handler.setListener(new WebSocketHandlerListener() { + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + /* We have to wait until the balancer responds for kaazing gateway */ + if (!WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE.equals(protocol)) { + //Non-kaazing gateway, fire open event + WebSocketNativeChannel wsChannel = (WebSocketNativeChannel)channel; + wsChannel.balanced.set(2); //turn off balancer message check + listener.connectionOpened(channel, protocol); + } + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + try { + LOG.finest("Balancer redirect location = " + StringUtils.stripControlCharacters(location)); + + WSURI uri = new WSURI(location); + reconnect(channel, uri, channel.getProtocol()); + nextHandler.processClose(channel, 0, null); + } + catch (URISyntaxException e) { + LOG.log(Level.WARNING, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + catch (Exception e) { + LOG.log(Level.WARNING, e.getMessage(), e); + listener.connectionFailed(channel, e); + } + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + listener.authenticationRequested(channel, location, challenge); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + handleBinaryMessageReceived(channel, buf); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + handleTextMessageReceived(channel, message); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + WebSocketNativeChannel wsChannel = (WebSocketNativeChannel)channel; + if (wsChannel.reconnecting.compareAndSet(true, false)) { + //balancer redirect, open a new connection to redirectUri + wsChannel.reconnected.set(true); + + // add kaazing protocol header + String[] nextProtocols; + String[] requestedProtocols = wsChannel.getRequestedProtocols(); + if (requestedProtocols == null || requestedProtocols.length == 0) { + nextProtocols = new String[] { WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE }; + } + else { + nextProtocols = new String[requestedProtocols.length+1]; + nextProtocols[0] = WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE; + for (int i=0; i out; + + public WebSocketNativeCodec() { + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer message) { + encoder.encodeBinaryMessage(channel, message, out); + } + + @Override + public void processTextMessage(WebSocketChannel channel, String message) { + encoder.encodeTextMessage(channel, message, out); + } + + @Override + public void setNextHandler(final WebSocketHandler handler) { + super.setNextHandler(handler); + + out = new EncoderOutput() { + @Override + public void write(WebSocketChannel channel, WrappedByteBuffer buffer) { + handler.processBinaryMessage(channel, buffer); + } + }; + + // TODO: use WebSocketHandlerListenerAdapter + nextHandler.setListener(new WebSocketHandlerListener() { + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + listener.connectionOpened(channel, protocol); + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + listener.redirected(channel, location); + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + listener.authenticationRequested(channel, location, challenge); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + listener.binaryMessageReceived(channel, buf); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + listener.textMessageReceived(channel, message); + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + listener.commandMessageReceived(channel, message); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + listener.connectionClosed(channel, wasClean, code, reason); + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + listener.connectionClosed(channel, ex); + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + listener.connectionFailed(channel, ex); + } + }); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeDelegateHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeDelegateHandler.java new file mode 100755 index 0000000..0b6e8e1 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeDelegateHandler.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import java.io.IOException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.charset.Charset; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.transport.AuthenticateEvent; +import org.kaazing.gateway.client.transport.CloseEvent; +import org.kaazing.gateway.client.transport.ErrorEvent; +import org.kaazing.gateway.client.transport.MessageEvent; +import org.kaazing.gateway.client.transport.OpenEvent; +import org.kaazing.gateway.client.transport.RedirectEvent; +import org.kaazing.gateway.client.transport.ws.WebSocketDelegate; +import org.kaazing.gateway.client.transport.ws.WebSocketDelegateImpl; +import org.kaazing.gateway.client.transport.ws.WebSocketDelegateListener; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public class WebSocketNativeDelegateHandler implements WebSocketHandler { + + private static final String CLASS_NAME = WebSocketNativeDelegateHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final Charset UTF8 = Charset.forName("UTF-8"); + + WebSocketHandlerListener listener; + + @Override + public void setIdleTimeout(WebSocketChannel channel, int timeout) { + WebSocketNativeChannel wsnChannel = (WebSocketNativeChannel)channel; + WebSocketDelegate delegate = wsnChannel.getDelegate(); + delegate.setIdleTimeout(timeout); + } + + @Override + public void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + final WebSocketNativeChannel wsnChannel = (WebSocketNativeChannel)channel; + final WebSocketCompositeChannel parentChannel = (WebSocketCompositeChannel) wsnChannel.getParent(); + long connectTimeout = 0; + + if (parentChannel.getConnectTimer() != null) { + connectTimeout = parentChannel.getConnectTimer().getDelay(); + } + + URI origin; + try { + origin = new URI("privileged://" + getCanonicalHostPort(location.getURI())); + + WebSocketDelegate delegate = new WebSocketDelegateImpl(location.getURI(), origin, protocols, connectTimeout); + wsnChannel.setDelegate(delegate); + delegate.setListener(new WebSocketDelegateListener() { + + @Override + public void opened(OpenEvent event) { + LOG.entering(CLASS_NAME, "opened"); + String protocol = event.getProtocol(); + listener.connectionOpened(wsnChannel, protocol); + } + + @Override + public void closed(CloseEvent event) { + LOG.entering(CLASS_NAME, "closed"); + wsnChannel.setDelegate(null); + + Exception ex = event.getException(); + if (ex == null) { + listener.connectionClosed(wsnChannel, event.wasClean(), event.getCode(), event.getReason()); + } + else { + listener.connectionClosed(wsnChannel, ex); + } + } + + @Override + public void redirected(RedirectEvent redirectEvent) { + LOG.entering(CLASS_NAME, "redirected"); + String redirectUrl = redirectEvent.getLocation(); + listener.redirected(wsnChannel, redirectUrl); + } + + @Override + public void authenticationRequested(AuthenticateEvent authenticateEvent) { + LOG.entering(CLASS_NAME, "authenticationRequested"); + String location = wsnChannel.getLocation().toString(); + String challenge = authenticateEvent.getChallenge(); + listener.authenticationRequested(wsnChannel, location, challenge); + } + + @Override + public void messageReceived(MessageEvent messageEvent) { + LOG.entering(CLASS_NAME, "messageReceived"); + WrappedByteBuffer messageBuffer = WrappedByteBuffer.wrap(messageEvent.getData()); + String messageType = messageEvent.getMessageType(); + + if (LOG.isLoggable(Level.FINEST)) { + LOG.log(Level.FINEST, messageBuffer.getHexDump()); + } + + if (messageType == null) { + throw new NullPointerException("Message type is null"); + } + + if ("TEXT".equals(messageType)) { + String text = messageBuffer.getString(UTF8); + listener.textMessageReceived(wsnChannel, text); + } + else { + listener.binaryMessageReceived(wsnChannel, messageBuffer); + } + } + + @Override + public void errorOccurred(ErrorEvent event) { + listener.connectionFailed(wsnChannel, event.getException()); + } + }); + + delegate.processOpen(); + } catch (URISyntaxException e) { + LOG.log(Level.FINE, "During connect processing: "+e.getMessage(), e); + listener.connectionFailed(wsnChannel, e); + } + } + + @Override + public void processClose(WebSocketChannel channel, int code, String reason) { + WebSocketNativeChannel wsnChannel = (WebSocketNativeChannel)channel; + try { + WebSocketDelegate delegate = wsnChannel.getDelegate(); + delegate.processDisconnect((short)code, reason); + } catch (IOException e) { + LOG.log(Level.FINE, "During close processing: "+e.getMessage(), e); + listener.connectionFailed(wsnChannel, e); + } + } + + @Override + public void processAuthorize(WebSocketChannel channel, String authorizeToken) { + WebSocketNativeChannel wsnChannel = (WebSocketNativeChannel)channel; + WebSocketDelegate delegate = wsnChannel.getDelegate(); + delegate.processAuthorize(authorizeToken); + } + + @Override + public void processBinaryMessage(WebSocketChannel channel, WrappedByteBuffer buffer) { + WebSocketNativeChannel wsnChannel = (WebSocketNativeChannel)channel; + WebSocketDelegate delegate = wsnChannel.getDelegate(); + java.nio.ByteBuffer nioBuffer = java.nio.ByteBuffer.wrap(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.remaining()); + delegate.processSend(nioBuffer); + } + + @Override + public void processTextMessage(WebSocketChannel channel, String text) { + WebSocketNativeChannel wsnChannel = (WebSocketNativeChannel)channel; + WebSocketDelegate delegate = wsnChannel.getDelegate(); + delegate.processSend(text); + // throw new IllegalStateException("Not implemented"); + } + + @Override + public void setListener(WebSocketHandlerListener listener) { + this.listener = listener; + } + + public static String getCanonicalHostPort(URI uri) { + int port = uri.getPort(); + if (port == -1) { + String scheme = uri.getScheme(); + if (scheme.equals("https") || scheme.equals("wss") || scheme.equals("wse+ssl")) { + port = 443; + } + else { + port = 80; + } + } + return uri.getHost()+":"+port; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoder.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoder.java new file mode 100755 index 0000000..144c0aa --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoder.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import org.kaazing.gateway.client.impl.EncoderOutput; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +public interface WebSocketNativeEncoder { + + void encodeTextMessage(WebSocketChannel channel, String message, EncoderOutput out); + void encodeBinaryMessage(WebSocketChannel channel, WrappedByteBuffer message, EncoderOutput out); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoderImpl.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoderImpl.java new file mode 100755 index 0000000..863e7a2 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeEncoderImpl.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import java.nio.charset.Charset; +import java.util.Random; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.EncoderOutput; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + + +public class WebSocketNativeEncoderImpl implements WebSocketNativeEncoder { + private static final String CLASS_NAME = WebSocketNativeEncoderImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + private static final Charset UTF8 = Charset.forName("UTF-8"); + + protected WebSocketNativeEncoderImpl() { + } + + @Override + public void encodeTextMessage(WebSocketChannel channel, String message, EncoderOutput out) { + LOG.entering(CLASS_NAME, "processTextMessage", message); + + WrappedByteBuffer buf = new WrappedByteBuffer(); + buf.putString(message, UTF8); + buf.flip(); + + WrappedByteBuffer buffer = encodeRFC6455(buf, false); + + out.write(channel, buffer); + } + + @Override + public void encodeBinaryMessage(WebSocketChannel channel, WrappedByteBuffer message, EncoderOutput out) { + LOG.entering(CLASS_NAME, "processBinaryMessage", message); + + WrappedByteBuffer buffer = encodeRFC6455(message, true); + + out.write(channel, buffer); + } + + private WrappedByteBuffer encodeRFC6455(WrappedByteBuffer buf, boolean isBinary) { + + final boolean mask = true; + int maskValue = new Random().nextInt(); + + boolean fin = true; // TODO continued frames? + + int remaining = buf.remaining(); + + int offset = 2 + (mask ? 4 : 0) + calculateLengthSize(remaining); + + WrappedByteBuffer b = WrappedByteBuffer.allocate(offset + remaining); + + int start = b.position(); + + byte b1 = (byte) (fin ? 0x80 : 0x00); + byte b2 = (byte) (mask ? 0x80 : 0x00); + + b1 = doEncodeOpcode(b1, isBinary); + b2 |= lenBits(remaining); + + b.put(b1).put(b2); + + doEncodeLength(b, remaining); + + if (mask) { + b.putInt(maskValue); + } + //put message data + b.putBuffer(buf); + + if ( mask ) { + b.position(offset); + mask(b, maskValue); + } + + b.limit(b.position()); + b.position(start); + return b; + + } + + private static byte doEncodeOpcode(byte b, boolean isBinary) { + return (byte) (b | (isBinary ? 0x02 : 0x01)); + } + + private static byte lenBits(int length) { + if (length < 126) { + return (byte) length; + } else if (length < 65535) { + return (byte) 126; + } else { + return (byte) 127; + } + } + + private static int calculateLengthSize(int length) { + if (length < 126) { + return 0; + } else if (length < 65535) { + return 2; + } else { + return 8; + } + } + + private static void doEncodeLength(WrappedByteBuffer buf, int length) { + if (length < 126) { + return; + } else if (length < 65535) { + buf.putShort((short) length); + } else { + // Unsigned long (should never have a message that large! really!) + buf.putLong((long) length); + } + } + + /** + * Performs an in-situ masking of the readable buf bytes. + * Preserves the position of the buffer whilst masking all the readable bytes, + * such that the masked bytes will be readable after this invocation. + * + * @param buf the buffer containing readable bytes to be masked. + * @param mask the mask to apply against the readable bytes of buffer. + */ + public static void mask(WrappedByteBuffer buf, int mask) { + // masking is the same as unmasking due to the use of bitwise XOR. + unmask(buf, mask); + } + + + /** + * Performs an in-situ unmasking of the readable buf bytes. + * Preserves the position of the buffer whilst unmasking all the readable bytes, + * such that the unmasked bytes will be readable after this invocation. + * + * @param buf the buffer containing readable bytes to be unmasked. + * @param mask the mask to apply against the readable bytes of buffer. + */ + public static void unmask(WrappedByteBuffer buf, int mask) { + byte b; + int remainder = buf.remaining() % 4; + int remaining = buf.remaining() - remainder; + int end = remaining + buf.position(); + + // xor a 32bit word at a time as long as possible + while (buf.position() < end) { + int plaintext = buf.getIntAt(buf.position()) ^ mask; + buf.putInt(plaintext); + } + + // xor the remaining 3, 2, or 1 bytes + switch (remainder) { + case 3: + b = (byte) (buf.getAt(buf.position()) ^ ((mask >> 24) & 0xff)); + buf.put(b); + b = (byte) (buf.getAt(buf.position()) ^ ((mask >> 16) & 0xff)); + buf.put(b); + b = (byte) (buf.getAt(buf.position()) ^ ((mask >> 8) & 0xff)); + buf.put(b); + break; + case 2: + b = (byte) (buf.getAt(buf.position()) ^ ((mask >> 24) & 0xff)); + buf.put(b); + b = (byte) (buf.getAt(buf.position()) ^ ((mask >> 16) & 0xff)); + buf.put(b); + break; + case 1: + b = (byte) (buf.getAt(buf.position()) ^ (mask >> 24)); + buf.put(b); + break; + case 0: + default: + break; + } + //buf.position(start); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandler.java new file mode 100755 index 0000000..4dbc1ac --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandler.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerFactory; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.ws.WebSocketLoggingHandler; +import org.kaazing.gateway.client.impl.ws.WebSocketTransportHandler; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +/* + * WebSocket Native Handler Chain + * {NativeHandler} - AuthenticationHandler - HandshakeHandler - ControlFrameHandler - BalanceingHandler - Nodec - BridgeHandler + * Responsibilities: + * a). build up native handler chain + */ +public class WebSocketNativeHandler extends WebSocketHandlerAdapter { + private static final String CLASS_NAME = WebSocketNativeHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + public static WebSocketHandlerFactory TRANSPORT_HANDLER_FACTORY = new WebSocketHandlerFactory() { + @Override + public WebSocketHandler createWebSocketHandler() { + return new WebSocketTransportHandler(); + } + }; + + private WebSocketNativeAuthenticationHandler authHandler = new WebSocketNativeAuthenticationHandler(); + private WebSocketNativeHandshakeHandler handshakeHandler = new WebSocketNativeHandshakeHandler(); + private WebSocketNativeBalancingHandler balancingHandler = new WebSocketNativeBalancingHandler(); + // private WebSocketNativeCodec codec = new WebSocketNativeCodec(); + + /** + * WebSocket + * @throws Exception + */ + public WebSocketNativeHandler() { + LOG.entering(CLASS_NAME, ""); + + authHandler.setNextHandler(handshakeHandler); + handshakeHandler.setNextHandler(balancingHandler); + + WebSocketHandler transportHandler = TRANSPORT_HANDLER_FACTORY.createWebSocketHandler(); + if (LOG.isLoggable(Level.FINE)) { + WebSocketLoggingHandler loggingHandler = new WebSocketLoggingHandler(); + loggingHandler.setNextHandler(transportHandler); + transportHandler = loggingHandler; + } + + balancingHandler.setNextHandler(transportHandler); + + nextHandler = authHandler; + + // TODO: Use WebSocketHandlerListenerAdapter + nextHandler.setListener(new WebSocketHandlerListener() { + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + listener.connectionOpened(channel, protocol); + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + listener.binaryMessageReceived(channel, buf); + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String message) { + listener.textMessageReceived(channel, message); + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, CommandMessage message) { + listener.commandMessageReceived(channel, message); + } + + @Override + public void connectionClosed(WebSocketChannel channel, boolean wasClean, int code, String reason) { + listener.connectionClosed(channel, wasClean, code, reason); + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + listener.connectionClosed(channel, ex); + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + listener.connectionFailed(channel, ex); + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + } + + @Override + public void authenticationRequested(WebSocketChannel channel, String location, String challenge) { + } + }); + } + + /** + * Connect to the WebSocket + * + * @throws Exception + */ + @Override + public void processConnect(WebSocketChannel channel, WSURI location, String[] protocols) { + LOG.entering(CLASS_NAME, "connect", channel); + + nextHandler.processConnect(channel, location, protocols); + } + + /** + * The number of bytes queued to be sent + */ + public int getBufferedAmount() { + // Payloads are sent immediately in native-protocol mode, and we + // do not have visibility of any buffering at the TCP layer. + return 0; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandshakeHandler.java b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandshakeHandler.java new file mode 100755 index 0000000..703d2f4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/impl/wsn/WebSocketNativeHandshakeHandler.java @@ -0,0 +1,388 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.impl.wsn; + +import java.net.URI; +import java.nio.charset.Charset; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandler; +import org.kaazing.gateway.client.impl.WebSocketHandlerAdapter; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.impl.ws.ReadyState; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.ws.WebSocketHandshakeObject; +import org.kaazing.gateway.client.impl.ws.WebSocketSelectedChannel; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +/* + * WebSocket Native Handler Chain + * NativeHandler - AuthenticationHandler - {HandshakeHandler} - ControlFrameHandler - BalanceingHandler - Nodec - BridgeHandler + * Responsibilities: + * a). handle kaazing handshake + * if response protocol is "x-kaazing-handshake", start handshake process + * otherwise, fire connectionOpened event + * b). process 401 + * if response is enveloped 401 challenge, fire a authenticationRequested event + * TODO: + * a). add more hand shake objects in the future + */ +public class WebSocketNativeHandshakeHandler extends WebSocketHandlerAdapter { + + private static final String CLASS_NAME = WebSocketNativeHandshakeHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final byte[] GET_BYTES = "GET".getBytes(); + private static final byte[] HTTP_1_1_BYTES = "HTTP/1.1".getBytes(); + private static final byte[] COLON_BYTES = ":".getBytes(); + private static final byte[] SPACE_BYTES = " ".getBytes(); + private static final byte[] CRLF_BYTES = "\r\n".getBytes(); + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + private static final String HEADER_PROTOCOL = "WebSocket-Protocol"; + private static final String HEADER_SEC_PROTOCOL = "Sec-WebSocket-Protocol"; + private static final String HEADER_SEC_EXTENSIONS = "Sec-WebSocket-Extensions"; + + + public WebSocketNativeHandshakeHandler() { + } + + @Override + public void processConnect(WebSocketChannel channel, WSURI uri, String[] protocols) { + LOG.entering(CLASS_NAME, "connect", new Object[]{uri, protocols}); + // add kaazing protocol header + String[] nextProtocols; + if (protocols == null || protocols.length == 0) { + nextProtocols = new String[] { WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE }; + } + else { + nextProtocols = new String[protocols.length+1]; + nextProtocols[0] = WebSocketHandshakeObject.KAAZING_EXTENDED_HANDSHAKE; + for (int i=0; i lineList = new ArrayList(); + int i=0; + + while (i < payload.length()) { + int endOfLine = payload.indexOf(13, i); + String nextLine; + if (endOfLine >= 0) { + if (payload.charAt(endOfLine+1) != (char)10) { + throw new IllegalArgumentException("Invalid payload"); + } + } + else { + endOfLine = payload.length(); + } + + nextLine = payload.substring(i, endOfLine); + lineList.add(nextLine); + i = endOfLine+2; + } + + String[] lines = new String[lineList.size()]; + lineList.toArray(lines); + return lines; + } + + private void handleHandshakeMessage(WebSocketChannel channel, WrappedByteBuffer buf) { + String s = buf.getString(Charset.forName("UTF-8")); + handleHandshakeMessage(channel, s); + } + + private void handleHandshakeMessage(WebSocketChannel channel, String message) { + + channel.handshakePayload.append(message); + + if (message.length() > 0) { + // Continue reading until an empty message is received. + // wait for more messages + return; + } + + String[] lines = getLines(channel.handshakePayload.toString()); + channel.handshakePayload.setLength(0); + + String httpCode = ""; + //parse the message for embedded http response, should read last one if there are more than one HTTP header + for (int i = lines.length - 1; i >= 0; i--) { + if (lines[i].startsWith("HTTP/1.1")) { //"HTTP/1.1 101 ..." + String[] temp = lines[i].split(" "); + httpCode = temp[1]; + break; + } + } + + if ("101".equals(httpCode)) { + //handshake completed, websocket Open + + String extensionsHeader = ""; + String negotiatedextensions = ""; + for (String line :lines) { + if (line != null && line.startsWith(HEADER_SEC_PROTOCOL)) { + String protocol = line.substring(HEADER_SEC_PROTOCOL.length() + 1).trim(); + channel.setProtocol(protocol); + } + + if (line != null && line.startsWith(HEADER_SEC_EXTENSIONS)) { + // Get Protocol and extensions - note: extensions may contains multiple entries + // concatenate extensions to one line, separated by "," + extensionsHeader += (extensionsHeader == "" ? "" : ",") + line.substring(HEADER_SEC_EXTENSIONS.length() + 1).trim(); + } + } + + // Parse extensions header + if (extensionsHeader.length() > 0) { + String[] extensions = extensionsHeader.split(","); + for (String extension : extensions) { + String[] tmp = extension.split(";"); + String extName = tmp[0].trim(); + if (extName.equals(WebSocketHandshakeObject.KAAZING_SEC_EXTENSION_IDLETIMEOUT)) { + //idle timeout extension supported by server + try { + //x-kaazing-idle-timeout extension, parameter = "timeout=10000" + int timeout = Integer.parseInt(tmp[1].trim().substring(8)); + if (timeout > 0) { + nextHandler.setIdleTimeout(channel, timeout); + } + } catch (Exception e) { + throw new IllegalArgumentException("Cannot find timeout parameter in x-kaazing-idle-timeout extension: " + extension); + } + // x-kaazing-idle-timeout extension is internal extension, do not add to negotiated extensions + continue; + } + + negotiatedextensions += (negotiatedextensions == "" ? extension : ("," + extension)); + } + if (negotiatedextensions.length() > 0) { + channel.setNegotiatedExtensions(negotiatedextensions); + } + } + } + else if ("401".equals(httpCode)) { + //receive HTTP/1.1 401 from server, pass event to Authentication handler + String challenge = ""; + for (String line : lines) { + if (line.startsWith(HEADER_WWW_AUTHENTICATE)) { + challenge = line.substring(HEADER_WWW_AUTHENTICATE.length()+ 1).trim(); + break; + } + } + listener.authenticationRequested(channel, channel.getLocation().toString(), challenge); + } + else { + // Error during handshake, close connect, report connectionFailed + listener.connectionFailed(channel, + new IllegalStateException("Error during handshake. HTTP Status Code: " + httpCode)); + } + } + + private void sendHandshakePayload(WebSocketChannel channel, String authToken) { + + String[] headerNames = new String[4]; + String[] headerValues = new String[4]; + headerNames[0] = HEADER_PROTOCOL; + headerValues[0] = null; //for now use Sec-Websockect-Protocol header instead + headerNames[1] = HEADER_SEC_PROTOCOL; + headerValues[1] = channel.getProtocol(); //now send the Websockect-Protocol + + //KG-9978 add x-kaazing-idle-timeout extension + headerNames[2] = HEADER_SEC_EXTENSIONS; + headerValues[2] = WebSocketHandshakeObject.KAAZING_SEC_EXTENSION_IDLETIMEOUT; + + String enabledExtensions = ((WebSocketCompositeChannel)channel.getParent()).getEnabledExtensions(); + if ((enabledExtensions != null) && (enabledExtensions.trim().length() != 0)) { + headerValues[2] += "," + enabledExtensions; + } + + headerNames[3] = HEADER_AUTHORIZATION; + headerValues[3] = authToken; //send authorization token + + byte[] payload = encodeGetRequest(channel.getLocation().getURI(), headerNames, headerValues); + nextHandler.processBinaryMessage(channel, new WrappedByteBuffer(payload)); + } + + private byte[] encodeGetRequest(URI requestURI, String[] names, String[] values) { + + // Any changes to this method should result in the getEncodeRequestSize method below + // to get accurate length of the buffer that needs to be allocated. + + LOG.entering(CLASS_NAME, "encodeGetRequest", new Object[]{requestURI, names, values}); + int requestSize = getEncodeRequestSize(requestURI, names, values); + java.nio.ByteBuffer buf = java.nio.ByteBuffer.allocate(requestSize); + + // Encode Request line + buf.put(GET_BYTES); + buf.put(SPACE_BYTES); + String path = requestURI.getPath(); // + "?.kl=Y&.kv=10.05"; + if (path.length() == 0) { + path = "/"; + } + if (requestURI.getQuery() != null) { + path += "?" + requestURI.getQuery(); + } + buf.put(path.getBytes()); + buf.put(SPACE_BYTES); + buf.put(HTTP_1_1_BYTES); + buf.put(CRLF_BYTES); + + // Encode headers + for (int i = 0; i < names.length; i++) { + String headerName = names[i]; + String headerValue = values[i]; + if (headerName != null && headerValue != null) { + buf.put(headerName.getBytes()); + buf.put(COLON_BYTES); + buf.put(SPACE_BYTES); + buf.put(headerValue.getBytes()); + buf.put(CRLF_BYTES); + } + } + + // Encoding cookies, content length and content not done here as we + // don't have it in the initial GET request. + + buf.put(CRLF_BYTES); + buf.flip(); + return buf.array(); + } + + private int getEncodeRequestSize(URI requestURI, String[] names, String[] values) { + int size = 0; + + // Encode Request line + size += GET_BYTES.length; + size += SPACE_BYTES.length; + String path = requestURI.getPath(); // + "?.kl=Y&.kv=10.05"; + if (path.length() == 0) { + path = "/"; + } + if (requestURI.getQuery() != null) { + path += "?" + requestURI.getQuery(); + } + size += path.getBytes().length; + size += SPACE_BYTES.length; + size += HTTP_1_1_BYTES.length; + size += CRLF_BYTES.length; + + // Encode headers + for (int i = 0; i < names.length; i++) { + String headerName = names[i]; + String headerValue = values[i]; + if (headerName != null && headerValue != null) { + size += headerName.getBytes().length; + size += COLON_BYTES.length; + size += SPACE_BYTES.length; + size += headerValue.getBytes().length; + size += CRLF_BYTES.length; + } + } + + size += CRLF_BYTES.length; + + LOG.fine("Returning a request size of " + size); + return size; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/AuthenticateEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/AuthenticateEvent.java new file mode 100755 index 0000000..987efe4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/AuthenticateEvent.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.util.logging.Logger; + +public class AuthenticateEvent extends Event { + private static final String CLASS_NAME = AuthenticateEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private String challenge; + + /** + * Authenticate Event + * + * @param challenge + */ + public AuthenticateEvent(String challenge) { + super(Event.AUTHENTICATE); + LOG.entering(CLASS_NAME, "", new Object[] { type, challenge }); + this.challenge = challenge; + } + + public String getChallenge() { + LOG.exiting(CLASS_NAME, "getLocation", challenge); + return challenge; + } + + public String toString() { + String ret = "AuthenticateEvent [type=" + type + " challenge=" + challenge + "{"; + for (Object a : params) { + ret += a + " "; + } + return ret + "}]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/BridgeDelegate.java b/android/src/main/java/org/kaazing/gateway/client/transport/BridgeDelegate.java new file mode 100755 index 0000000..11ec170 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/BridgeDelegate.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +public interface BridgeDelegate { + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/CloseEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/CloseEvent.java new file mode 100755 index 0000000..31e55ff --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/CloseEvent.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +public class CloseEvent extends Event { + + private int code; + private String reason; + private boolean wasClean; + private Exception exception; + + public CloseEvent(int code, boolean wasClean, String reason) { + super(Event.CLOSED); + this.code = code; + this.wasClean = wasClean; + this.reason = reason; + } + + public CloseEvent(Exception exception) { + super(Event.CLOSED); + this.exception = exception; + } + + public int getCode() { + return code; + } + + public boolean wasClean() { + return wasClean; + } + + public String getReason() { + return reason; + } + + public Exception getException() { + return exception; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ErrorEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/ErrorEvent.java new file mode 100755 index 0000000..90fd7cf --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ErrorEvent.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +public class ErrorEvent extends Event { + private Exception exception; + + public ErrorEvent() { + super(Event.ERROR); + } + + public ErrorEvent(Exception exception) { + super(Event.ERROR); + this.exception = exception; + } + + public void setException(Exception exception) { + this.exception = exception; + } + + public Exception getException() { + return exception; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/Event.java b/android/src/main/java/org/kaazing/gateway/client/transport/Event.java new file mode 100755 index 0000000..07f2136 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/Event.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +/** + * Class representing the HTML 5 DOM event + */ +public class Event { + /** + * WebSocket and ByteSocket Events + */ + public static final String MESSAGE = "message"; + public static final String OPEN = "open"; + public static final String CLOSED = "closed"; + public static final String REDIRECT = "redirect"; + public static final String AUTHENTICATE = "authenticate"; + + /** + * EventSource Events use MESSAGE, OPEN from the list above + */ + public static final String ERROR = "error"; + + /** + * HttpRequest Events use OPEN and ERROR from the list above + */ + public static final String READY_STATE_CHANGE = "readystatechange"; + public static final String LOAD = "load"; + public static final String ABORT = "abort"; + public static final String PROGRESS = "progress"; + + private static final String[] EMPTY_PARAMS = {}; + Object[] params; + String type; + + public Event(String type) { + this(type, EMPTY_PARAMS); + } + + public Event(String type, Object[] params) { + this.type = type; + this.params = params; + } + + public String getType() { + return type; + } + + public Object[] getParams() { + return params; + } + + public String toString() { + String ret = "Event[type:" + type + "{"; + for (Object a : params) { + ret += a + " "; + } + return ret + "}]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/IoBufferUtil.java b/android/src/main/java/org/kaazing/gateway/client/transport/IoBufferUtil.java new file mode 100755 index 0000000..7600187 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/IoBufferUtil.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.nio.ByteBuffer; + +public class IoBufferUtil { + + public static ByteBuffer expandBuffer(ByteBuffer existingBuffer, int additionalRequired) { + int pos = existingBuffer.position(); + if ((pos + additionalRequired) > existingBuffer.limit()) { + if ((pos + additionalRequired) < existingBuffer.capacity()) { + existingBuffer.limit(pos + additionalRequired); + } else { + // reallocate the underlying byte buffer and keep the original buffer + // intact. The resetting of the position is required because, one + // could be in the middle of a read of an existing buffer, when they + // decide to over write only few bytes but still keep the remaining + // part of the buffer unchanged. + int newCapacity = existingBuffer.capacity() + additionalRequired ; + java.nio.ByteBuffer newBuffer = java.nio.ByteBuffer.allocate(newCapacity); + existingBuffer.flip(); + newBuffer.put(existingBuffer); + return newBuffer; + } + } + return existingBuffer; + } + + public static boolean canAccomodate(ByteBuffer existingBuffer, int additionalLength) { + return ((existingBuffer.position() + additionalLength) <= existingBuffer.limit()); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/LoadEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/LoadEvent.java new file mode 100755 index 0000000..be17acf --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/LoadEvent.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.nio.ByteBuffer; +import java.util.logging.Logger; + +public class LoadEvent extends Event { + private static final String CLASS_NAME = LoadEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private ByteBuffer responseBuffer; + + public LoadEvent(ByteBuffer responseBuffer) { + super(Event.LOAD); + LOG.entering(CLASS_NAME, "", new Object[]{responseBuffer}); + this.responseBuffer = responseBuffer; + } + + public ByteBuffer getResponseBuffer() { + return responseBuffer; + } + + public String toString() { + String ret = "LoadEvent [type=" + type + " responseBuffer=" + responseBuffer + "{"; + for(Object a: params) { + ret += a + " "; + } + return ret + "}]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/MessageEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/MessageEvent.java new file mode 100755 index 0000000..7065e44 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/MessageEvent.java @@ -0,0 +1,84 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.nio.ByteBuffer; +import java.util.logging.Logger; + +public class MessageEvent extends Event { + private static final String CLASS_NAME = MessageEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private ByteBuffer data; + private String origin; + private String lastEventId; + private String messageType; // "TEXT" or "BINARY" + + /** + * Message Event + * + * @param data + * @param origin + * @param lastEventId + * @param type + */ + public MessageEvent(ByteBuffer data, String origin, String lastEventId, String messageType) { + super(Event.MESSAGE); + LOG.entering(CLASS_NAME, "", new Object[]{type, data, origin, lastEventId}); + this.data = data; + this.origin = origin; + this.lastEventId = lastEventId; + this.messageType = messageType; + } + + public ByteBuffer getData() { + LOG.exiting(CLASS_NAME, "getData", data); + return data; + } + + public String getOrigin() { + LOG.exiting(CLASS_NAME, "getOrigin", origin); + return origin; + } + + public String getLastEventId() { + LOG.exiting(CLASS_NAME, "getLastEventId", lastEventId); + return lastEventId; + } + + /** + * Return "TEXT" or "BINARY" depending on the message type + * @return + */ + public String getMessageType() { + LOG.exiting(CLASS_NAME, "getMessageType", messageType); + return messageType; + } + + public String toString() { + String ret = "MessageEvent [type=" + type + " messageType="+messageType+" data=" + data + " origin " + origin + " lastEventId=" + lastEventId + "{"; + for (Object a : params) { + ret += a + " "; + } + return ret + "}]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/OpenEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/OpenEvent.java new file mode 100755 index 0000000..8e321eb --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/OpenEvent.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.util.logging.Logger; + +public class OpenEvent extends Event { + private static final String CLASS_NAME = OpenEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private String protocol; + + public OpenEvent() { + super(Event.OPEN); + LOG.entering(CLASS_NAME, ""); + } + + public OpenEvent(String protocol) { + super(Event.OPEN); + this.protocol = protocol; + LOG.entering(CLASS_NAME, ""); + } + + public String toString() { + String ret = "OpenEvent [type=" + type + " + {"; + for(Object a: params) { + ret += a + " "; + } + return ret + "}]"; + } + + /** + * @return the protocol, if more than one protocols are defined, separated by comma + */ + public String getProtocol() { + return protocol; + } + + /** + * @param protocol the protocol to set + */ + public void setProtocol(String protocol) { + this.protocol = protocol; + } + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ProgressEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/ProgressEvent.java new file mode 100755 index 0000000..4cb12ff --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ProgressEvent.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.nio.ByteBuffer; +import java.util.logging.Logger; + + +public class ProgressEvent extends Event { + private static final String CLASS_NAME = ProgressEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private int bytesTotal; + private int bytesLoaded; + private ByteBuffer payload; + + public ProgressEvent(ByteBuffer payload, int bytesLoaded, int bytesTotal) { + super("progress"); + LOG.entering(CLASS_NAME, "", new Object[]{payload, bytesLoaded, bytesTotal}); + this.payload = payload; + this.bytesLoaded = bytesLoaded; + this.bytesTotal = bytesTotal; + } + + public int getBytesTotal() { + LOG.exiting(CLASS_NAME, "getBytesTotal", bytesTotal); + return bytesTotal; + } + + public int getBytesLoaded() { + LOG.exiting(CLASS_NAME, "getBytesLoaded", bytesLoaded); + return bytesLoaded; + } + + public ByteBuffer getPayload() { + LOG.exiting(CLASS_NAME, "getPayload", payload); + return payload; + } + + public String toString() { + String ret = "ProgressEvent [type=" + type + " payload=" + payload + "{"; + for(Object a: params) { + ret += a + " "; + } + return ret + "}]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ReadyStateChangedEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/ReadyStateChangedEvent.java new file mode 100755 index 0000000..195a898 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ReadyStateChangedEvent.java @@ -0,0 +1,29 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +public class ReadyStateChangedEvent extends Event { + + public ReadyStateChangedEvent(String[] params) { + super(Event.READY_STATE_CHANGE, params); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/RedirectEvent.java b/android/src/main/java/org/kaazing/gateway/client/transport/RedirectEvent.java new file mode 100755 index 0000000..8e9c18b --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/RedirectEvent.java @@ -0,0 +1,55 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport; + +import java.util.logging.Logger; + +public class RedirectEvent extends Event { + private static final String CLASS_NAME = RedirectEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private String location; + + /** + * Redirect Event + * + * @param location + */ + public RedirectEvent(String location) { + super(Event.REDIRECT); + LOG.entering(CLASS_NAME, "", new Object[]{type, location}); + this.location = location; + } + + public String getLocation() { + LOG.exiting(CLASS_NAME, "getLocation", location); + return location; + } + + public String toString() { + String ret = "RedirectEvent [type=" + type + " location=" + location + "{"; + for (Object a : params) { + ret += a + " "; + } + return ret + "}]"; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegate.java b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegate.java new file mode 100755 index 0000000..f335c3c --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegate.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.http; + +import java.net.URL; +import java.nio.ByteBuffer; + +import org.kaazing.gateway.client.transport.BridgeDelegate; + +public interface HttpRequestDelegate extends BridgeDelegate { + + void setRequestHeader(String header, String value); + + int getStatusCode(); + String getResponseHeader(String headerLocation); + ByteBuffer getResponseText(); + + void processOpen(String method, URL url, String origin, boolean async, long connectTimeout) throws Exception; + void processSend(ByteBuffer content); + void processAbort(); + + void setListener(HttpRequestDelegateListener listener); + +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateFactory.java b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateFactory.java new file mode 100755 index 0000000..e0adcfc --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateFactory.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.http; + +public interface HttpRequestDelegateFactory { + + HttpRequestDelegate createHttpRequestDelegate(); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateImpl.java b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateImpl.java new file mode 100755 index 0000000..ce31cc4 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateImpl.java @@ -0,0 +1,364 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.http; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.transport.CloseEvent; +import org.kaazing.gateway.client.transport.ErrorEvent; +import org.kaazing.gateway.client.transport.IoBufferUtil; +import org.kaazing.gateway.client.transport.LoadEvent; +import org.kaazing.gateway.client.transport.OpenEvent; +import org.kaazing.gateway.client.transport.ProgressEvent; +import org.kaazing.gateway.client.transport.ReadyStateChangedEvent; + +public class HttpRequestDelegateImpl implements HttpRequestDelegate { + private static final String CLASS_NAME = HttpRequestDelegateImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + /* + * XmlHTTPReqeuest states const unsigned short UNSENT = 0; const unsigned short OPENED = 1; const unsigned short HEADERS_RECEIVED = 2; const unsigned short + * LOADING = 3; const unsigned short DONE = 4; + */ + + public static enum State { + UNSENT, OPENED, HEADERS_RECEIVED, LOADING, DONE + } + + private State readyState = State.UNSENT; + + private ByteBuffer responseBuffer = ByteBuffer.allocate(5000); + private ByteBuffer completedResponseBuffer; + HttpURLConnection connection = null; + private HttpRequestDelegateListener listener; + + private int httpResponseCode; + + private StreamReader reader; + + private boolean async; + + public HttpRequestDelegateImpl() { + LOG.entering(CLASS_NAME, ""); + } + + public final State getReadyState() { + LOG.exiting(CLASS_NAME, "getReadyState", readyState); + return readyState; + } + + public ByteBuffer getResponseText() { + LOG.entering(CLASS_NAME, "getResponseText"); + switch (readyState) { + case LOADING: + case OPENED: + return responseBuffer.duplicate(); + case DONE: + return completedResponseBuffer; + } + return null; + } + + public int getStatusCode() { + LOG.exiting(CLASS_NAME, "getStatusCode", httpResponseCode); + return httpResponseCode; + } + + /* (non-Javadoc) + * @see org.kaazing.gateway.client.transport.http.HttpRequestDelegate#abort() + */ + public void processAbort() { + LOG.entering(CLASS_NAME, "abort"); + if (reader != null) { + reader.stop(); + connection.disconnect(); + reader = null; + connection = null; + } + }; + + public String getAllResponseHeaders() { + LOG.entering(CLASS_NAME, "getAllResponseHeaders"); + if (readyState == State.LOADING || readyState == State.DONE) { + // return all headers from the HttpUrlConnection; + String headerText = connection.getHeaderFields().toString(); + LOG.exiting(CLASS_NAME, "getAllResponseHeaders", headerText); + return headerText; + } + else { + LOG.exiting(CLASS_NAME, "getAllResponseHeaders"); + return null; + } + } + + public String getResponseHeader(String header) { + LOG.entering(CLASS_NAME, "getResponseHeader", header); + if (readyState == State.LOADING || readyState == State.DONE || readyState == State.HEADERS_RECEIVED) { + String headerText = connection.getHeaderField(header); + LOG.exiting(CLASS_NAME, "getResponseHeader", headerText); + return headerText; + } + else { + LOG.exiting(CLASS_NAME, "getResponseHeader"); + return null; + } + } + + /* (non-Javadoc) + * @see org.kaazing.gateway.client.transport.http.HttpRequestDelegate#open(java.lang.String, java.net.URL, java.lang.String, boolean) + */ + public void processOpen(String method, URL url, String origin, boolean async, long connectTimeout) throws Exception { + LOG.entering(CLASS_NAME, "processOpen", new Object[] {method, url, origin, async}); + + this.async = async; + connection = (HttpURLConnection)url.openConnection(); + connection.setRequestMethod(method); + connection.setInstanceFollowRedirects(false); + connection.setConnectTimeout((int) connectTimeout); + + if (!origin.equalsIgnoreCase("null") && !origin.startsWith("privileged")) { + URL originUrl = new URL(origin); + origin = originUrl.getProtocol() + "://" + originUrl.getAuthority(); + } + + // connection.addRequestProperty("X-Origin", origin); + connection.addRequestProperty("Origin", origin); + setReadyState(State.OPENED); + + listener.opened(new OpenEvent()); + } + + /* (non-Javadoc) + * @see org.kaazing.gateway.client.transport.http.HttpRequestDelegate#send(java.nio.ByteBuffer) + */ + public void processSend(ByteBuffer content) { + LOG.entering(CLASS_NAME, "processSend", content); + if (readyState != State.OPENED && readyState != State.HEADERS_RECEIVED) { + throw new IllegalStateException(readyState + " HttpRequest must be in an OPEN state before " + "invocation of the send() method"); + } + try { + if (!async && content != null && content.hasRemaining()) { + connection.setDoOutput(true); + connection.setDoInput(true); + + OutputStream out = connection.getOutputStream(); + out.write(content.array(), content.arrayOffset(), content.remaining()); + out.flush(); + } + + connection.connect(); + reader = new StreamReader(); + Thread t = new Thread(reader, "HttpRequestDelegate stream reader"); + t.setDaemon(true); + t.start(); + } + catch (Exception e) { + LOG.log(Level.FINE, "While processing http request", e); + // e.printStackTrace(); + listener.errorOccurred(new ErrorEvent(e)); + } + } + + /* (non-Javadoc) + * @see org.kaazing.gateway.bridge.HttpRequestDelegate#setRequestHeader(java.lang.String, java.lang.String) + */ + public void setRequestHeader(String header, String value) { + LOG.entering(CLASS_NAME, "setRequestHeader", new Object[] {header, value}); + HttpRequestUtil.validateHeader(header); + connection.addRequestProperty(header, value); + } + + protected void reset() { + LOG.entering(CLASS_NAME, "reset"); + responseBuffer = null; + completedResponseBuffer = null; + setStatus(-1); + setReadyState(State.UNSENT); + } + + private void setReadyState(State state) { + LOG.entering(CLASS_NAME, "setReadyState", state); + this.readyState = state; + } + + private void setStatus(int status) { + LOG.entering(CLASS_NAME, "setStatus", status); + this.httpResponseCode = status; + } + + private final class StreamReader implements Runnable { + private final String CLASS_NAME = StreamReader.class.getName(); + + private AtomicBoolean stopped = new AtomicBoolean(false); + private AtomicBoolean requestCompleted = new AtomicBoolean(false); + + public void run() { + try { + run2(); + } + catch(Exception e) { + LOG.log(Level.INFO, e.getMessage(), e); + } + } + + public void run2() { + LOG.entering(CLASS_NAME, "run"); + InputStream in = null; + try { + httpResponseCode = connection.getResponseCode(); + + // For streaming responses Java returns -1 as the response code + if (httpResponseCode != -1 && (httpResponseCode < 200 || httpResponseCode == 400 || httpResponseCode == 402 || httpResponseCode == 403 || httpResponseCode == 404)) { + Exception ex = new Exception("Unexpected HTTP response code received: code = " + httpResponseCode); + listener.errorOccurred(new ErrorEvent(ex)); + throw ex; + } + Map> headers = connection.getHeaderFields(); + StringBuffer allHeadersBuffer = new StringBuffer(); + int numHeaders = headers.size(); + for (int i = 0; i < numHeaders; i++) { + allHeadersBuffer.append(connection.getHeaderFieldKey(i)); + allHeadersBuffer.append(":"); + allHeadersBuffer.append(connection.getHeaderField(i)); + allHeadersBuffer.append("\n"); + } + String allHeaders = allHeadersBuffer.toString(); + String[] params = new String[] {Integer.toString(State.HEADERS_RECEIVED.ordinal()), Integer.toString(httpResponseCode), connection.getResponseMessage() + "", + allHeaders}; + + setReadyState(State.HEADERS_RECEIVED); + listener.readyStateChanged(new ReadyStateChangedEvent(params)); + + if(httpResponseCode == 401) { + listener.loaded(new LoadEvent(ByteBuffer.allocate(0))); + requestCompleted.compareAndSet(false, true); + connection.disconnect(); + return; + } + in = connection.getInputStream(); + + } + catch (IOException e) { + LOG.severe(e.toString()); + listener.errorOccurred(new ErrorEvent(e)); + return; + } + catch (Exception ex) { + LOG.log(Level.FINE, ex.getMessage(), ex); + listener.errorOccurred(new ErrorEvent(ex)); + return; + } + + try { + if (in == null) { + in = connection.getInputStream(); + if (in == null) { + String s = "Input stream not ready"; + Exception ex = new RuntimeException(s); + listener.errorOccurred(new ErrorEvent(ex)); + LOG.severe(s); + throw ex; + } + } + + + byte[] payloadBuffer = new byte[4096]; // read in chunks + while (!stopped.get()) { + int numberOfBytesRead = in.read(payloadBuffer, 0, payloadBuffer.length); + if (numberOfBytesRead == -1) { + // end of stream, break from loop + break; + } + ByteBuffer payload = ByteBuffer.wrap(payloadBuffer, 0, numberOfBytesRead); + if (!async) { + // build up the buffer for completed response only for + // synchronous requests + // expand the buffer if required + int payloadSize = payload.remaining(); + if (!IoBufferUtil.canAccomodate(responseBuffer, payloadSize)) { + responseBuffer = IoBufferUtil.expandBuffer(responseBuffer, payloadSize); + } + responseBuffer.put(payload); + // the put above resets the position of payload, flip it so we can reuse it + payload.flip(); + } + listener.progressed(new ProgressEvent(payload, 0, 0)); + } + + if (!stopped.get()) { + // We want to fire the load event for complete responses + // only that are + // regular HTTP requests. For streaming request, we expect + // the caller + // to call abort + responseBuffer.flip(); + completedResponseBuffer = responseBuffer.duplicate(); + setReadyState(State.DONE); + + try { + listener.loaded(new LoadEvent(completedResponseBuffer)); + } + finally { + requestCompleted.compareAndSet(false, true); + try { + connection.disconnect(); + } + finally { + listener.closed(new CloseEvent(1000, true, "")); + } + } + } + } + catch (IOException e) { + LOG.severe(e.toString()); + if (!requestCompleted.get()) { + listener.errorOccurred(new ErrorEvent(e)); + } + } + catch (Exception ex) { + LOG.log(Level.FINE, ex.getMessage(), ex); + listener.errorOccurred(new ErrorEvent(ex)); + return; + } + } + + public void stop() { + LOG.entering(CLASS_NAME, "stop"); + this.stopped.set(true); + } + } + + @Override + public void setListener(HttpRequestDelegateListener listener) { + this.listener = listener; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateListener.java b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateListener.java new file mode 100755 index 0000000..850b6be --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestDelegateListener.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.http; + +import org.kaazing.gateway.client.transport.CloseEvent; +import org.kaazing.gateway.client.transport.ErrorEvent; +import org.kaazing.gateway.client.transport.LoadEvent; +import org.kaazing.gateway.client.transport.OpenEvent; +import org.kaazing.gateway.client.transport.ProgressEvent; +import org.kaazing.gateway.client.transport.ReadyStateChangedEvent; + +public interface HttpRequestDelegateListener { + + void opened(OpenEvent event); + void errorOccurred(ErrorEvent event); + void readyStateChanged(ReadyStateChangedEvent event); + void loaded(LoadEvent event); + void progressed(ProgressEvent progressEvent); + void closed(CloseEvent event); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestUtil.java b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestUtil.java new file mode 100755 index 0000000..f18af3e --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/http/HttpRequestUtil.java @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.http; + +import java.util.logging.Logger; + +public class HttpRequestUtil { + private static final String CLASS_NAME = HttpRequestUtil.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + public static void validateHeader(String header) { + LOG.entering(CLASS_NAME, "validateHeader", header); + /* + * From the XMLHttpRequest spec: + * http://www.w3.org/TR/XMLHttpRequest/#setrequestheader + * + * For security reasons, these steps should be terminated if the header + * argument case-insensitively matches one of the following headers: + * + * Accept-Charset Accept-Encoding Connection Content-Length + * Content-Transfer-Encoding Date Expect Host Keep-Alive Referer TE + * Trailer Transfer-Encoding Upgrade Via Proxy-* Sec-* + * + * Also for security reasons, these steps should be terminated if the + * start of the header argument case-insensitively matches Proxy- or Se + */ + if (header == null || (header.length() == 0)) { + LOG.severe("Invalid header in the HTTP request"); + throw new IllegalArgumentException("Invalid header in the HTTP request"); + } + String lowerCaseHeader = header.toLowerCase(); + if (lowerCaseHeader.startsWith("proxy-") || lowerCaseHeader.startsWith("sec-")) { + LOG.severe("Headers starting with Proxy-* or Sec-* are prohibited"); + throw new IllegalArgumentException( + "Headers starting with Proxy-* or Sec-* are prohibited"); + } + for (String prohibited : INVALID_HEADERS) { + if (header.equalsIgnoreCase(prohibited)) { + LOG.severe("Prohibited header"); + throw new IllegalArgumentException( + "Headers starting with Proxy-* or Sec-* are prohibited"); + } + } + } + + private static final String[] INVALID_HEADERS = new String[] { "Accept-Charset", + "Accept-Encoding", "Connection", "Content-Length", "Content-Transfer-Encoding", "Date", + "Expect", "Host", "Keep-Alive", "Referer", "TE", "Trailer", "Transfer-Encoding", + "Upgrade", "Via" }; + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/Base64Util.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/Base64Util.java new file mode 100755 index 0000000..03da7d3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/Base64Util.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.nio.ByteBuffer; +import java.util.logging.Logger; + +/** + * Internal class. This class manages the Base64 encoding and decoding + */ +public class Base64Util { + +// @FlashNative +//import mx.utils.Base64Encoder; +// public String encodeBytes(ByteArray bytes) { +// Base64Encoder encoder=new Base64Encoder(); +// encoder.insertNewLines = false; +// encoder.encodeBytes(bytes); +// return encoder.drain(); +// } + + private static final String CLASS_NAME = Base64Util.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final byte[] INDEXED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes(); + private static final byte PADDING_BYTE = (byte) '='; + + @Deprecated + private Base64Util() { + LOG.entering(CLASS_NAME, ""); + } + + public static String encode(ByteBuffer decoded) { + LOG.entering(CLASS_NAME, "encode", decoded); + + int decodedSize = decoded.remaining(); + int effectiveDecodedSize = ((decodedSize+2) / 3) * 3; + int decodedFragmentSize = decodedSize % 3; + + int encodedArraySize = effectiveDecodedSize / 3 * 4; + byte[] encodedArray = new byte[encodedArraySize]; + int encodedArrayPosition = 0; + + byte[] decodedArray = decoded.array(); + int decodedArrayOffset = decoded.arrayOffset(); + int decodedArrayPosition = decodedArrayOffset + decoded.position(); + int decodedArrayLimit = decodedArrayOffset + decoded.limit() - decodedFragmentSize; + + while (decodedArrayPosition < decodedArrayLimit) { + int byte0 = decodedArray[decodedArrayPosition++] & 0xff; + int byte1 = decodedArray[decodedArrayPosition++] & 0xff; + int byte2 = decodedArray[decodedArrayPosition++] & 0xff; + + encodedArray[encodedArrayPosition++] = INDEXED[(byte0 >> 2) & 0x3f]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte0 << 4) & 0x30) | ((byte1 >> 4) & 0x0f)]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte1 << 2) & 0x3c) | ((byte2 >> 6) & 0x03)]; + encodedArray[encodedArrayPosition++] = INDEXED[byte2 & 0x3f]; + } + + if (decodedFragmentSize == 1) { + int byte0 = decodedArray[decodedArrayPosition++] & 0xff; + + encodedArray[encodedArrayPosition++] = INDEXED[(byte0 >> 2) & 0x3f]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte0 << 4) & 0x30)]; + encodedArray[encodedArrayPosition++] = PADDING_BYTE; + encodedArray[encodedArrayPosition++] = PADDING_BYTE; + } + else if (decodedFragmentSize == 2) { + int byte0 = decodedArray[decodedArrayPosition++] & 0xff; + int byte1 = decodedArray[decodedArrayPosition++] & 0xff; + + encodedArray[encodedArrayPosition++] = INDEXED[(byte0 >> 2) & 0x3f]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte0 << 4) & 0x30) | ((byte1 >> 4) & 0x0f)]; + encodedArray[encodedArrayPosition++] = INDEXED[(byte1 << 2) & 0x3c]; + encodedArray[encodedArrayPosition++] = PADDING_BYTE; + } + + return new String(encodedArray); + } + + public static ByteBuffer decode(String encoded) { + LOG.entering(CLASS_NAME, "decode", encoded); + + if (encoded == null) { + return null; + } + + int length = encoded.length(); + if (length % 4 != 0) { + throw new IllegalArgumentException("Invalid Base64 Encoded String"); + } + + byte[] encodedArray = encoded.getBytes(); + byte[] decodedArray = new byte[(length / 4 * 3)]; + int decodedArrayOffset = 0; + int i = 0; + while (i < length) { + int char0 = encodedArray[i++]; + int char1 = encodedArray[i++]; + int char2 = encodedArray[i++]; + int char3 = encodedArray[i++]; + + int byte0 = mapped(char0); + int byte1 = mapped(char1); + int byte2 = mapped(char2); + int byte3 = mapped(char3); + + decodedArray[decodedArrayOffset++] = (byte) (((byte0 << 2) & 0xfc) | ((byte1 >> 4) & 0x03)); + if (char2 != PADDING_BYTE) { + decodedArray[decodedArrayOffset++] = (byte) (((byte1 << 4) & 0xf0) | ((byte2 >> 2) & 0x0f)); + if (char3 != PADDING_BYTE) { + decodedArray[decodedArrayOffset++] = (byte) (((byte2 << 6) & 0xc0) | (byte3 & 0x3f)); + } + } + } + return ByteBuffer.wrap(decodedArray, 0, decodedArrayOffset); + } + + private static int mapped(int ch) { + if ((ch & 0x40) != 0) { + if ((ch & 0x20) != 0) { + // a(01100001)-z(01111010) -> 26-51 + assert (ch >= 'a'); + assert (ch <= 'z'); + return (ch - 71); + } else { + // A(01000001)-Z(01011010) -> 0-25 + assert (ch >= 'A'); + assert (ch <= 'Z'); + return (ch - 65); + } + } else if ((ch & 0x20) != 0) { + if ((ch & 0x10) != 0) { + if ((ch & 0x08) != 0 && (ch & 0x04) != 0) { + // =(00111101) -> 0 + assert (ch == '='); + return 0; + } else { + // 0(00110000)-9(00111001) -> 52-61 + assert (ch >= '0'); + assert (ch <= '9'); + return (ch + 4); + } + } else { + if ((ch & 0x04) != 0) { + // /(00101111) -> 63 + assert (ch == '/'); + return 63; + } else { + // +(00101011) -> 62 + assert (ch == '+'); + return 62; + } + } + } else { + LOG.warning("Invalid BASE64 string"); + throw new IllegalArgumentException("Invalid BASE64 string"); + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocket.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocket.java new file mode 100755 index 0000000..e6ba62b --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocket.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.SocketException; + +interface BridgeSocket { + + void connect(InetSocketAddress inetSocketAddress, long timeout) throws IOException; + InputStream getInputStream() throws IOException; + OutputStream getOutputStream() throws IOException; + void close() throws IOException; + + void setSoTimeout(int i) throws SocketException; + void setKeepAlive(boolean b) throws SocketException; + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketFactory.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketFactory.java new file mode 100755 index 0000000..25c1a9a --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.io.IOException; + +interface BridgeSocketFactory { + + BridgeSocket createSocket(boolean secure) throws IOException; + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketImpl.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketImpl.java new file mode 100755 index 0000000..06508c3 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/BridgeSocketImpl.java @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; + +import javax.net.ssl.SSLSocketFactory; + +class BridgeSocketImpl implements BridgeSocket { + + boolean secure; + Socket socket; + + BridgeSocketImpl(boolean secure) { + this.secure = secure; + } + + @Override + public void connect(InetSocketAddress inetSocketAddress, long timeout) throws IOException { + if (secure) { + socket = SSLSocketFactory.getDefault().createSocket(); + } + else { + socket = new Socket(); + } + + + assert(timeout >= 0); + socket.connect(inetSocketAddress, (int)timeout); + } + + @Override + public void close() throws IOException { + socket.close(); + } + + @Override + public InputStream getInputStream() throws IOException { + return socket.getInputStream(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return socket.getOutputStream(); + } + + @Override + public void setKeepAlive(boolean val) throws SocketException { + socket.setKeepAlive(val); + } + + @Override + public void setSoTimeout(int timeout) throws SocketException { + socket.setSoTimeout(timeout); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessor.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessor.java new file mode 100755 index 0000000..82cb805 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessor.java @@ -0,0 +1,209 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.logging.Logger; + +public class FrameProcessor { + + private static final String CLASS_NAME = FrameProcessor.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + FrameProcessorListener listener; + DecodingState state; + WsFrameEncodingSupport.Opcode opcode; + int dataLength; + Boolean masked; + int maskkey; + ByteBuffer payLoadLengthBuf; + ByteBuffer maskkeyBuf; + Boolean fin; + ByteBuffer data; + + FrameProcessor(FrameProcessorListener listener) { + this.listener = listener; + this.state = DecodingState.START_OF_FRAME; + } + + /* + * Processing state machine + */ + static enum DecodingState { + START_OF_FRAME, + READING_PAYLOADLENGTH, + READING_PAYLOADLENGTH_EXT, + READING_MASK_KEY, + READING_PAYLOAD, + END_OF_FRAME, + }; + + /** + * Process frames from inbound messages + * @param byteBuffer + */ + boolean process(InputStream inputStream) throws IOException { + LOG.entering(CLASS_NAME, "process"); + int b; + for (;;) { + + switch (state) { + // handle alignment with start-of-frame boundary (after mark) + case START_OF_FRAME: + b = inputStream.read(); + if (b == -1) { + return false; //end of stream + } + fin = (b & 0x80) != 0; + opcode = WsFrameEncodingSupport.Opcode.getById((b & 0x0f)); + state = DecodingState.READING_PAYLOADLENGTH; //start read mask & payload length byte + break; + + case READING_PAYLOADLENGTH: + + //reading mask bit and payload length (7) + b = inputStream.read(); + if (b == -1) { + return false; //end of stream + } + + masked = (b & 0x80) != 0; + if (masked) { + maskkeyBuf = ByteBuffer.allocate(4); //4 byte mask key + } + dataLength = b & 0x7f; + + if (dataLength==126) { + //length is 16 bit long unsigned int - must fill first two bytes with 0,0 to handle unsigned + state = DecodingState.READING_PAYLOADLENGTH_EXT; + payLoadLengthBuf = ByteBuffer.allocate(4); + payLoadLengthBuf.put(new byte[] {0x00, 0x00}); //fill 2 bytes with 0 + } else if (dataLength==127) { + //length is 64 bit long + state = DecodingState.READING_PAYLOADLENGTH_EXT; + payLoadLengthBuf = ByteBuffer.allocate(8); + } + else { + state = DecodingState.READING_MASK_KEY; + + } + break; + + case READING_PAYLOADLENGTH_EXT: + byte[] bytes = new byte[payLoadLengthBuf.remaining()]; + int num = inputStream.read(bytes); + if (num == -1) { + return false; //end of stream + } + payLoadLengthBuf.put(bytes, 0, num); + if (!payLoadLengthBuf.hasRemaining()) { + //payload length bytes has been read + payLoadLengthBuf.flip(); + if (payLoadLengthBuf.capacity() == 4) { + // 16 bit length + dataLength = payLoadLengthBuf.getInt(); + } + else { + dataLength = (int) payLoadLengthBuf.getLong(); + } + state = DecodingState.READING_MASK_KEY; + break; + } + break; + + case READING_MASK_KEY: + if (!masked) { + //unmasked, skip READ_MASK_KEY + data = ByteBuffer.allocate(dataLength); + state = DecodingState.READING_PAYLOAD; + break; + } + bytes = new byte[maskkeyBuf.remaining()]; + num = inputStream.read(bytes); + if (num == -1) { + return false; //end of stream + } + maskkeyBuf.put(bytes, 0, num); + if (!maskkeyBuf.hasRemaining()) { + //4 bytes has been read, done with mask key + maskkeyBuf.flip(); //move postion to 0 + maskkey = maskkeyBuf.getInt(); + data = ByteBuffer.allocate(dataLength); + state = DecodingState.READING_PAYLOAD; + } + break; + + case READING_PAYLOAD: + if (dataLength == 0) { + // empty message + state = DecodingState.END_OF_FRAME; + break; + } + bytes = new byte[data.remaining()]; + num = inputStream.read(bytes); + if (num == -1) { + return false; //end of stream + } + data.put(bytes, 0, num); + if (!data.hasRemaining()) { + //all payload has been read + data.flip(); + state = DecodingState.END_OF_FRAME; + break; + } + break; + + case END_OF_FRAME: + //finished load payload + switch (opcode) { + case BINARY: + if (masked) { + WsFrameEncodingSupport.unmask(data, maskkey); + } + listener.messageReceived(data, "BINARY"); + break; + case TEXT: + if (masked) { + WsFrameEncodingSupport.unmask(data, maskkey); + } + listener.messageReceived(data, "TEXT"); + break; + case PING: + listener.messageReceived(data, "PING"); + break; + case PONG: + //do nothing + break; + case CLOSE: + listener.messageReceived(data, "CLOSE"); + break; + } + state = DecodingState.START_OF_FRAME; + + break; + } + } + + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessorListener.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessorListener.java new file mode 100755 index 0000000..19b8c19 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/FrameProcessorListener.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.nio.ByteBuffer; + +interface FrameProcessorListener { + + void messageReceived(ByteBuffer buffer, String type); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegate.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegate.java new file mode 100755 index 0000000..69656d6 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegate.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.kaazing.gateway.client.transport.BridgeDelegate; + +public interface WebSocketDelegate extends BridgeDelegate { + + void processOpen(); + void processAuthorize(String string); + void processDisconnect() throws IOException; + void processDisconnect(short code, String reason) throws IOException; //add code and reason + void processSend(ByteBuffer byteBuffer); + void processSend(String text); //add this method to send text frame message + + void setListener(WebSocketDelegateListener listener); + void setIdleTimeout(int timeout); //set WebSocket idle timeout in miliseconds +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateFactory.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateFactory.java new file mode 100755 index 0000000..fd47a4e --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateFactory.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.net.URI; + +public interface WebSocketDelegateFactory { + + WebSocketDelegate createWebSocketDelegate(URI xoaUrl, URI originUrl, String wsProtocol); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateImpl.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateImpl.java new file mode 100755 index 0000000..31b18bc --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateImpl.java @@ -0,0 +1,1066 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.transport.AuthenticateEvent; +import org.kaazing.gateway.client.transport.CloseEvent; +import org.kaazing.gateway.client.transport.ErrorEvent; +import org.kaazing.gateway.client.transport.IoBufferUtil; +import org.kaazing.gateway.client.transport.LoadEvent; +import org.kaazing.gateway.client.transport.MessageEvent; +import org.kaazing.gateway.client.transport.OpenEvent; +import org.kaazing.gateway.client.transport.ProgressEvent; +import org.kaazing.gateway.client.transport.ReadyStateChangedEvent; +import org.kaazing.gateway.client.transport.RedirectEvent; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegate; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegateFactory; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegateImpl; +import org.kaazing.gateway.client.transport.http.HttpRequestDelegateListener; +import org.kaazing.gateway.client.transport.ws.WsMessage.Kind; + +public class WebSocketDelegateImpl implements WebSocketDelegate { + private static final String CLASS_NAME = WebSocketDelegateImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final byte[] GET_BYTES = "GET".getBytes(); + private static final String APPLICATION_PREFIX = "Application "; + private static final String WWW_AUTHENTICATE = "WWW-Authenticate: "; + private static final String HTTP_1_1_START = "HTTP/1.1"; + private static final int HTTP_1_1_START_LEN = HTTP_1_1_START.length(); + private static final byte[] HTTP_1_1_START_BYTES = HTTP_1_1_START.getBytes(); + private static final String WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; + public static final int CLOSE_NO_STATUS = 1005; + public static final int CLOSE_ABNORMAL = 1006; + + static enum ConnectionStatus { + START, STATUS_101_READ, CONNECTION_UPGRADE_READ, COMPLETED, ERRORED + } + + public static enum ReadyState { + CONNECTING, OPEN, CLOSING, CLOSED; + } + + private static final byte[] HTTP_1_1_BYTES = "HTTP/1.1".getBytes(); + private static final byte[] COLON_BYTES = ":".getBytes(); + private static final byte[] SPACE_BYTES = " ".getBytes(); + private static final byte[] CRLF_BYTES = "\r\n".getBytes(); + private static final String HEADER_ORIGIN = "Origin"; + private static final String HEADER_CONNECTION = "Connection"; + private static final String HEADER_HOST = "Host"; + private static final String HEADER_UPGRADE = "Upgrade"; + private static final String HEADER_PROTOCOL = "Sec-WebSocket-Protocol"; + private static final String HEADER_WEBSOCKET_KEY = "Sec-WebSocket-Key"; + private static final String HEADER_WEBSOCKET_VERSION = "Sec-WebSocket-Version"; + private static final String HEADER_VERSION = "13"; + private static final String HEADER_AUTHORIZATION = "Authorization"; + private static final String HEADER_LOCATION = "Location"; + private static final String HEADER_WWW_AUTHENTICATE = "WWW-Authenticate"; + private static final String WEB_SOCKET_LOWERCASE = "websocket"; + private static final String HEADER_COOKIE = "Cookie"; + private static final Charset UTF8 = Charset.forName("UTF-8"); + + private BridgeSocket socket; + private boolean stopReaderThread; + private boolean connectionUpgraded = false; + private URI url; + private String origin; + private URI originUri; + private String[] requestedProtocols; + private boolean secure; + private WebSocketDelegateListener listener; + protected String cookies = null; + private String authorize = null; + private AtomicBoolean closed = new AtomicBoolean(false); + String websocketKey; + + private final long connectTimeout; + + //--------Idle Timeout-------------// + private final AtomicInteger idleTimeout = new AtomicInteger(); + private final AtomicLong lastMessageTimestamp = new AtomicLong(); + private Timer idleTimer = null; + + //WebSocket rfc6455 properties + private ReadyState readyState = ReadyState.CONNECTING; + public ReadyState getReadyState(){ + return readyState; + } + + int bufferedAmount; + public int getBufferedAmount() { + return bufferedAmount; + } + + private String secProtocol; + public String getSecProtocol() { + return secProtocol; + } + + private String extensions; + public String getExtensions() { + return extensions; + } + // close event data + private boolean wasClean = false; + private int code = CLOSE_ABNORMAL; + private String reason = ""; + + HttpRequestDelegateFactory HTTP_REQUEST_DELEGATE_FACTORY = new HttpRequestDelegateFactory() { + @Override + public HttpRequestDelegate createHttpRequestDelegate() { + return new HttpRequestDelegateImpl(); + } + }; + + BridgeSocketFactory BRIDGE_SOCKET_FACTORY = new BridgeSocketFactory() { + @Override + public BridgeSocket createSocket(boolean secure) throws IOException { + return new BridgeSocketImpl(secure); + } + }; + + /** + * WebSocket Java API for use in Java Web Start applications + * + * @param url + * WebSocket URL location + * @param origin + * the codebase+hostname from the JNLP of the Java Web Start app + * @param protocol + * WebSocket protocol + * @throws Exception + */ + public WebSocketDelegateImpl(URI url, URI origin, String[] protocols, long connectTimeout) { + LOG.entering(CLASS_NAME, "", new Object[] {url, origin, protocols}); + if (origin == null) { + throw new IllegalArgumentException("Please specify the origin for the WebSocket connection"); + } + + if (url == null) { + throw new IllegalArgumentException("Please specify the target for the WebSocket connection"); + } + this.url = url; + + if ((origin.getScheme() == null) || (origin.getHost() == null)) { + this.origin = "null"; + } + else { + String originScheme = origin.getScheme(); + String originHost = origin.getHost(); + int originPort = origin.getPort(); + if (originPort == -1) { + originPort = (originScheme.equals("https")) ? 443 : 80; + } + this.origin = originScheme + "://" + originHost + ":" + originPort; + } + + this.requestedProtocols = protocols; + secure = url.getScheme().equalsIgnoreCase("wss"); + this.connectTimeout = connectTimeout; + } + + + //------------------------------Idle Timer Start/Stop/Handler---------------------// + + private void startIdleTimer(long delayInMilliseconds) { + LOG.fine("Starting idle timer"); + if (this.idleTimer != null) { + idleTimer.cancel(); + idleTimer = null; + } + + idleTimer = new Timer("IdleTimer", true); + idleTimer.schedule(new TimerTask() { + + @Override + public void run() { + idleTimerHandler(); + } + + }, delayInMilliseconds); + } + + private void idleTimerHandler() { + LOG.fine("Idle timer scheduled"); + long idleDuration = System.currentTimeMillis() - lastMessageTimestamp.get(); + if (idleDuration > idleTimeout.get()) { + String message = "idle duration - " + idleDuration + " exceeded idle timeout - " + idleTimeout; + LOG.fine(message); + handleClose(null); + } + else { + // Reschedule timer + startIdleTimer(idleTimeout.get() - idleDuration); + } + } + + private void stopIdleTimer() { + LOG.fine("Stopping idle timer"); + if (idleTimer != null) { + idleTimer.cancel(); + idleTimer = null; + } + } + + @Override + public void setIdleTimeout(int milliSecond) { + idleTimeout.set(milliSecond); + if (milliSecond > 0) { + // start monitor websocket traffic + lastMessageTimestamp.set(System.currentTimeMillis()); + startIdleTimer(milliSecond); + } + else { + stopIdleTimer(); + } + } + + //-------------------------------------------------------------------------------// + + public void processOpen() { + LOG.entering(CLASS_NAME, "processOpen"); + // pre-flight cookies request + // Lookup the session cookie + String scheme = this.url.getScheme(); + String host = this.url.getHost(); + int port = this.url.getPort(); + String path = this.url.getPath(); + if (port == -1) { + port = (scheme.equals("wss")) ? 443 : 80; + } + LOG.fine("processOpen: Connecting to "+host+":"+port); + + String cookiesUri = scheme.replace("ws", "http") + "://" + host + ":" + port + path + "/;e/cookies?.krn=" + Double.toString(Math.random()).substring(2); + String query = this.url.getQuery(); + if (query != null && query.length() > 0) { + // No need to check to append "?" since a query parameter is added above + cookiesUri += "&" + query; + } + final HttpRequestDelegate cookiesRequest = HTTP_REQUEST_DELEGATE_FACTORY.createHttpRequestDelegate(); + + cookiesRequest.setListener(new HttpRequestDelegateListener() { + + @Override + public void opened(OpenEvent event) { + } + + @Override + public void readyStateChanged(ReadyStateChangedEvent event) { + } + + @Override + public void progressed(ProgressEvent progressEvent) { + } + + @Override + public void loaded(LoadEvent event) { + switch (cookiesRequest.getStatusCode()) { + case 200: + case 201: + ByteBuffer responseBuf = cookiesRequest.getResponseText(); + if (responseBuf != null && responseBuf.hasRemaining()) { + if (isHTTPResponse(responseBuf)) { + try { + handleWrappedHTTPResponse(responseBuf); + return; + } + catch (Exception e1) { + WebSocketDelegateImpl.this.handleClose(e1); + throw new IllegalStateException("Handling wrapped HTTP response failed", e1); + } + } + else { + cookies = new String(responseBuf.array(), responseBuf.position(), responseBuf.remaining()); + } + } + break; + case 301: + case 302: + case 307: + String location = cookiesRequest.getResponseHeader(HEADER_LOCATION); + LOG.finest("Redirect to " + location); + + URI uri; + try { + uri = new URI(location); + String query = uri.getQuery(); + String newQuery = (query != null ? query + "&" : "") + ".kl=Y"; + String redirectLocation = uri.getScheme().replace("http", "ws") + "://" + uri.getHost() + ":" + uri.getPort() + uri.getPath() + "?" + newQuery; + LOG.finest("Redirect as " + redirectLocation); + listener.redirected(new RedirectEvent(redirectLocation)); + } catch (URISyntaxException e) { + LOG.severe("Redirect location invalid: "+location); + } + return; + case 401: + String wwwAuthenticate = cookiesRequest.getResponseHeader(HEADER_WWW_AUTHENTICATE); + listener.authenticationRequested(new AuthenticateEvent(wwwAuthenticate)); + return; + default: + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSED; + String s = "Cookies request: Invalid status code: " + cookiesRequest.getStatusCode(); + listener.errorOccurred(new ErrorEvent(new IllegalStateException(s))); + return; + } + nativeConnect(); + } + + private void handleWrappedHTTPResponse(ByteBuffer responseBody) throws Exception { + LOG.entering(CLASS_NAME, "cookiesRequest.handleWrappedHTTPResponse"); + String[] lines = getLines(responseBody); + int statusCode = Integer.parseInt(lines[0].split(" ")[1]); + switch (statusCode) { + case HttpURLConnection.HTTP_UNAUTHORIZED: // 401 + String wwwAuthenticate = null; + for (int i = 1; i < lines.length; i++) { + if (lines[i].startsWith(WWW_AUTHENTICATE)) { + wwwAuthenticate = lines[i].substring(WWW_AUTHENTICATE.length()); + break; + } + } + LOG.finest("cookiesRequest.handleWrappedHTTPResponse: WWW-Authenticate: " + wwwAuthenticate); + if (wwwAuthenticate == null || "".equals(wwwAuthenticate)) { + LOG.severe("Missing authentication challenge in wrapped HTTP 401 response"); + throw new IllegalStateException("Missing authentication challenge in wrapped HTTP 401 response"); + } + else if (!wwwAuthenticate.startsWith(APPLICATION_PREFIX)) { + LOG.severe("Only Application challenges are supported by the client"); + throw new IllegalStateException("Only Application challenges are supported by the client"); + } + else { + listener.authenticationRequested(new AuthenticateEvent(wwwAuthenticate)); + } + break; + default: + throw new IllegalStateException("Unsupported wrapped response with HTTP status code " + statusCode); + } + } + + @Override + public void closed(CloseEvent event) { + } + + @Override + public void errorOccurred(ErrorEvent event) { + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSED; + listener.errorOccurred(new ErrorEvent(event.getException())); + } + }); + + URL cookiesUrl; + try { + cookiesUrl = new URL(cookiesUri); + cookiesRequest.processOpen("GET", cookiesUrl, this.origin, false, connectTimeout); + if (authorize != null) { + cookiesRequest.setRequestHeader(HEADER_AUTHORIZATION, authorize); + } + postProcessOpen(cookiesRequest); + cookiesRequest.processSend(null); + } + catch (Exception e1) { + LOG.severe(e1.toString()); + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSED; + listener.errorOccurred(new ErrorEvent(e1)); + } + } + + // Hook for subclasses for testing cookies requests. + protected void postProcessOpen(HttpRequestDelegate cookiesRequest) { + } + + private static boolean isHTTPResponse(ByteBuffer buf) { + boolean isHttpResponse = true; + if (buf.remaining() >= HTTP_1_1_START_LEN) { + for (int i = 0; i < HTTP_1_1_START_LEN; i++) { + if (buf.get(i) != HTTP_1_1_START_BYTES[i]) { + isHttpResponse = false; + break; + } + } + } + return isHttpResponse; + } + + private static String[] getLines(ByteBuffer buf) { + List lineList = new ArrayList(); + while (buf.hasRemaining()) { + byte next = buf.get(); + List lineText = new ArrayList(); + while (next != 13) { // CR + lineText.add(next); + if (buf.hasRemaining()) { + next = buf.get(); + } + else { + break; + } + } + if (buf.hasRemaining()) { + next = buf.get(); // should be LF + } + byte[] lineTextBytes = new byte[lineText.size()]; + int i = 0; + for (Byte text : lineText) { + lineTextBytes[i] = text; + i++; + } + try { + lineList.add(new String(lineTextBytes, "UTF-8")); + } + catch (UnsupportedEncodingException e) { + throw new IllegalStateException("Unrecognized Encoding from the server", e); + } + } + String[] lines = new String[lineList.size()]; + lineList.toArray(lines); + return lines; + } + + protected void nativeConnect() { + LOG.entering(CLASS_NAME, "nativeConnect"); + String host = this.url.getHost(); + int port = this.url.getPort(); + String scheme = this.url.getScheme(); + + if (port == -1) { + port = (scheme.equals("wss")) ? 443 : 80; + } + try { + LOG.fine("WebSocketDelegate.nativeConnect(): Connecting to "+host+":"+port); + socket = BRIDGE_SOCKET_FACTORY.createSocket(secure); + socket.connect(new InetSocketAddress(host, port), connectTimeout); + socket.setKeepAlive(true); + socket.setSoTimeout(0); // continuously read from the socket + } + catch (Exception e) { + LOG.log(Level.FINE, "WebSocketDelegateImpl nativeConnect(): "+e.getMessage(), e); + // Fire error listener to request to try to emulate it + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSED; + listener.errorOccurred(new ErrorEvent(e)); + return; + } + negotiateWebSocketConnection(socket); + } + + private void negotiateWebSocketConnection(BridgeSocket socket) { + LOG.entering(CLASS_NAME, "negotiateWebSocketConnection", socket); + try { + int headerCount = 9 + ((cookies == null) ? 0 : 1); + String[] headerNames = new String[headerCount]; + String[] headerValues = new String[headerCount]; + int headerIndex = 0; + headerNames[headerIndex] = HEADER_UPGRADE; + headerValues[headerIndex++] = WEB_SOCKET_LOWERCASE; + headerNames[headerIndex] = HEADER_CONNECTION; + headerValues[headerIndex++] = HEADER_UPGRADE; + headerNames[headerIndex] = HEADER_HOST; + headerValues[headerIndex++] = this.url.getAuthority(); + headerNames[headerIndex] = HEADER_ORIGIN; + headerValues[headerIndex++] = origin; + + headerNames[headerIndex] = HEADER_WEBSOCKET_VERSION; + headerValues[headerIndex++] = HEADER_VERSION; + headerNames[headerIndex] = HEADER_WEBSOCKET_KEY; + if (websocketKey == null) { + websocketKey = base64Encode(randomBytes(16)); + } + headerValues[headerIndex++] = websocketKey; + + if (requestedProtocols != null && requestedProtocols.length > 0) { + headerNames[headerIndex] = HEADER_PROTOCOL; + + String value; + if (requestedProtocols.length == 1) { + value = requestedProtocols[0]; + } + else { + value = ""; + for (int i=0; i0) { + value += ","; + } + value += requestedProtocols[i]; + } + } + + headerValues[headerIndex++] = value; + } + + if (cookies != null) { + headerNames[headerIndex] = HEADER_COOKIE; + headerValues[headerIndex++] = cookies; + } + + if (authorize != null) { + headerNames[headerIndex] = HEADER_AUTHORIZATION; + headerValues[headerIndex] = authorize; + } + + LOG.finer("Origin: " + origin); + + byte[] request = encodeGetRequest(this.url, headerNames, headerValues); + + // SOCKET DEBUGGING: OutputStream out = new LoggingOutputStream(socket.getOutputStream()); + OutputStream out = socket.getOutputStream(); + out.write(request); + out.flush(); + + InputStream in = socket.getInputStream(); + Thread readerThread = new Thread(new SocketReader(in), "WebSocketDelegate socket reader"); + readerThread.setDaemon(true); + readerThread.start(); + + } catch (Exception e) { + LOG.severe(e.toString()); + handleError(e); + } + } + + public byte[] encodeGetRequest(URI requestURI, String[] names, String[] values) { + + // Any changes to this method should result in the getEncodeRequestSize method below + // to get accurate length of the buffer that needs to be allocated. + + LOG.entering(CLASS_NAME, "encodeGetRequest", new Object[] {requestURI, names, values}); + int requestSize = getEncodeRequestSize(requestURI, names, values); + ByteBuffer buf = ByteBuffer.allocate(requestSize); + + // Encode Request line + buf.put(GET_BYTES); + buf.put(SPACE_BYTES); + String path = requestURI.getPath(); + if (requestURI.getQuery() != null) { + path += "?" + requestURI.getQuery(); + } + buf.put(path.getBytes()); + buf.put(SPACE_BYTES); + buf.put(HTTP_1_1_BYTES); + buf.put(CRLF_BYTES); + + // Encode headers + for (int i = 0; i < names.length; i++) { + String headerName = names[i]; + String headerValue = values[i]; + if (headerName != null && headerValue != null) { + buf.put(headerName.getBytes()); + buf.put(COLON_BYTES); + buf.put(SPACE_BYTES); + buf.put(headerValue.getBytes()); + buf.put(CRLF_BYTES); + } + } + + // Encoding cookies, content length and content not done here as we + // don't have it in the initial GET request. + + buf.put(CRLF_BYTES); + buf.flip(); + return buf.array(); + } + + private int getEncodeRequestSize(URI requestURI, String[] names, String[] values) { + int size = 0; + + // Encode Request line + size += GET_BYTES.length; + size += SPACE_BYTES.length; + String path = requestURI.getPath(); + if (requestURI.getQuery() != null) { + path += "?" + requestURI.getQuery(); + } + size += path.getBytes().length; + size += SPACE_BYTES.length; + size += HTTP_1_1_BYTES.length; + size += CRLF_BYTES.length; + + // Encode headers + for (int i = 0; i < names.length; i++) { + String headerName = names[i]; + String headerValue = values[i]; + if (headerName != null && headerValue != null) { + size += headerName.getBytes().length; + size += COLON_BYTES.length; + size += SPACE_BYTES.length; + size += headerValue.getBytes().length; + size += CRLF_BYTES.length; + } + } + + size += CRLF_BYTES.length; + + LOG.fine("Returning a request size of " + size); + return size; + } + + public void processDisconnect() throws IOException { + processDisconnect((short) 0, null); + } + + public void processDisconnect(short code, String reason) throws IOException { + LOG.entering(CLASS_NAME, "disconnect"); + //stopReaderThread = true; --- rfc 6455 donot stop SockectReader, wait for CloseFrame + + //send close frame if webSocket is open + if (this.readyState == ReadyState.OPEN) { + this.readyState = ReadyState.CLOSING; + ByteBuffer data; + if (code == 0) { + data = ByteBuffer.allocate(0); + } + else { + //if code is present, it must equal to 1000 or in range 3000 to 4999 + if (code != 1000 && (code < 3000 || code > 4999)) { + throw new IllegalArgumentException("code must equal to 1000 or in range 3000 to 4999"); + } + ByteBuffer reasonBuf = null; + if (reason != null && reason.length() > 0) { + //UTF 8 encode reason + Charset cs = Charset.forName("UTF-8"); + reasonBuf = cs.encode(reason); + if (reasonBuf.limit() > 123) { + throw new IllegalArgumentException("Reason is longer than 123 bytes"); + } + } + data = ByteBuffer.allocate(2 + (reasonBuf == null ? 0 : reasonBuf.remaining())); + data.putShort(code); + + if (reasonBuf != null) { + data.put(reasonBuf); + } + + data.flip(); + } + this.send(WsFrameEncodingSupport.rfc6455Encode(new WsMessage(data, Kind.CLOSE), new Random().nextInt())); + } + else if (readyState == ReadyState.CONNECTING) { + //websocket not open yet, fire close event + stopReaderThread = true; + handleClose(null); + } + + // Do not close the underlying socket connection immediately. As per the RFC spec - + // After both sending and receiving a Close message, an endpoint considers the WebSocket + // connection closed and MUST close the underlying TCP connection. + // Schedule a timer to close the underlying Socket connection if the CLOSE frame is not + // received from the Gateway within 5 seconds. + Timer t = new Timer("SocketCloseTimer", true); + t.schedule(new TimerTask() { + + @Override + public void run() { + try { + if (WebSocketDelegateImpl.this.readyState != ReadyState.CLOSED) { + stopIdleTimer(); + closeSocket(); + } + } + finally { + cancel(); + } + } + }, 5000); + + //else do nothing for CLOSING and CLOSED + } + + public void processAuthorize(String authorize) { + LOG.entering(CLASS_NAME, "processAuthorize", authorize); + this.authorize = authorize; + processOpen(); + } + + @Override + public void processSend(ByteBuffer data) { + LOG.entering(CLASS_NAME, "processSend", data); + + //move encoder code to core.java.client.internal, here just send data + ByteBuffer frame = WsFrameEncodingSupport.rfc6455Encode(new WsMessage(data, Kind.BINARY), new Random().nextInt()); + send(frame); + // send(data); + } + + @Override + public void processSend(String data) { + LOG.entering(CLASS_NAME, "processSend", data); + ByteBuffer buf = null; + try { + buf = ByteBuffer.wrap(data.getBytes("UTF-8")); + } + catch (UnsupportedEncodingException e) { + // this should not be reached + String s = "The platform should have already been checked to see if UTF-8 encoding is supported"; + throw new IllegalStateException(s); + } + + ByteBuffer frame = WsFrameEncodingSupport.rfc6455Encode(new WsMessage(buf, Kind.TEXT), new Random().nextInt()); + send(frame); + // send(data); + } + + private void send(ByteBuffer frame) { + LOG.entering(CLASS_NAME, "send", frame); + if (socket == null) { + handleError(new IllegalStateException("Socket is null")); + } + + try { + // consolidate the frame complete here and then flush + OutputStream outputStream = socket.getOutputStream(); + int offset = frame.position(); + int len = frame.remaining(); + outputStream.write(frame.array(), offset, len); + outputStream.flush(); + } + catch (Exception e) { + LOG.log(Level.FINE, "While sending: "+e.getMessage(), e); + handleError(e); + } + } + + protected URI getUrl() { + LOG.exiting(CLASS_NAME, "getUrl", url); + return url; + } + + protected URI getOrigin() { + LOG.exiting(CLASS_NAME, "getOrigin", originUri); + return originUri; + } + + private void closeSocket() { + try { + LOG.log(Level.FINE, "Closing socket"); + + // Sleep for a tenth-of-second before closing the socket. + Thread.sleep(100); + + if ((socket != null) && (readyState != ReadyState.CLOSED)) { + socket.close(); + } + } + catch (IOException e) { + LOG.log(Level.FINE, "While closing socket: "+e.getMessage(), e); + } + catch (InterruptedException e) { + LOG.log(Level.FINE, "While closing socket: "+e.getMessage(), e); + } + finally { + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSED; + socket = null; + } + } + + private void handleClose(Exception ex) { + if (closed.compareAndSet(false, true)) { + try { + stopIdleTimer(); + closeSocket(); + } + finally { + if (ex == null) { + listener.closed(new CloseEvent(this.code, this.wasClean, this.reason)); + } + else { + listener.closed(new CloseEvent(ex)); + } + } + } + } + + private void handleError(Exception ex) { + if (closed.compareAndSet(false, true)) { + try { + closeSocket(); + } + finally { + listener.errorOccurred(new ErrorEvent(ex)); + } + } + } + + private byte[] randomBytes(int size) { + byte[] bytes = new byte[size]; + Random r = new Random(); + r.nextBytes(bytes); + return bytes; + } + + private String base64Encode(byte[] bytes) { + return Base64Util.encode(ByteBuffer.wrap(bytes)); + + } + + @Override + public void setListener(WebSocketDelegateListener listener) { + this.listener = listener; + } + + + class SocketReader implements Runnable { + private final String CLASS_NAME = SocketReader.class.getName(); + + private static final String HTTP_101_MESSAGE = "HTTP/1.1 101 Web Socket Protocol Handshake"; + private static final String UPGRADE_HEADER = "Upgrade: "; + private static final int UPGRADE_HEADER_LENGTH = 9; // "Upgrade: ".length(); + private static final String UPGRADE_VALUE = "websocket"; + private static final String CONNECTION_MESSAGE = "Connection: Upgrade"; + private static final String WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol"; + private static final String WEBSOCKET_EXTENSIONS = "Sec-WebSocket-Extensions"; + private static final String WEBSOCKET_ACCEPT = "Sec-WebSocket-Accept"; + + ConnectionStatus state = ConnectionStatus.START; + Boolean upgradeReceived = false; + Boolean connectionReceived = false; + Boolean websocketAcceptReceived = false; + + InputStream inputStream = null; + + public SocketReader(InputStream inputStream) throws IOException { + LOG.entering(CLASS_NAME, ""); + this.inputStream = inputStream; + } + + public void run() { + LOG.entering(CLASS_NAME, "run"); + // TODO: to check for the first 85 bytes of the response instead. + try { + while (!stopReaderThread && !connectionUpgraded) { + if (state == ConnectionStatus.ERRORED) { + throw new IllegalArgumentException("WebSocket Connection upgrade unsuccessful"); + } + String line = readLine(inputStream); + + line = line.trim(); + //get WebSocket-Protocol: header + if (line.startsWith(WEBSOCKET_EXTENSIONS)) { + extensions = line.substring(WEBSOCKET_EXTENSIONS.length() + 1).trim(); + continue; + } + if (line.startsWith(WEBSOCKET_PROTOCOL)) { + secProtocol = line.substring(WEBSOCKET_PROTOCOL.length() + 1).trim(); + continue; + } + + if (state != ConnectionStatus.COMPLETED) { + processLine(line); + } + if (state == ConnectionStatus.COMPLETED) { + //all headers processed, check all required headers for WebSocket handshake + connectionUpgraded = websocketAcceptReceived && upgradeReceived && connectionReceived; + if (connectionUpgraded) { + // Completely read the WebSocket upgraded response. Now + // start doing the WebSocket protocol + readyState = ReadyState.OPEN; + listener.opened(new OpenEvent(secProtocol)); + lastMessageTimestamp.set(System.currentTimeMillis()); + } + else { + throw new IllegalArgumentException("WebSocket Connection upgrade unsuccessful"); + } + break; + } + } //end of while loop + + if (!connectionUpgraded && !stopReaderThread) { + throw new IllegalArgumentException("WebSocket Connection upgrade unsuccessful"); + } + + FrameProcessor frameProcessor = new FrameProcessor(new FrameProcessorListener() { + @Override + public void messageReceived(ByteBuffer buffer, String messageType) { + // update timestamp that is used to record the timestamp of last received message + lastMessageTimestamp.set(System.currentTimeMillis()); + if (messageType == "TEXT" || messageType == "BINARY") { + // fire message event if readyState == OPEN + if (WebSocketDelegateImpl.this.readyState == ReadyState.OPEN) { + listener.messageReceived(new MessageEvent(buffer, null, null, messageType)); + } + } + else if (messageType == "PING") { + //PING received, send PONG + ByteBuffer frame = WsFrameEncodingSupport.rfc6455Encode(new WsMessage(buffer, Kind.PONG), new Random().nextInt()); + WebSocketDelegateImpl.this.send(frame); + + } + else if (messageType == "CLOSE") { + WebSocketDelegateImpl.this.wasClean = true; + if (buffer.remaining() < 2) { + WebSocketDelegateImpl.this.code = CLOSE_NO_STATUS; //no status code was actually present + } + else { + WebSocketDelegateImpl.this.code = buffer.getShort(); + + if (buffer.hasRemaining()) { + WebSocketDelegateImpl.this.reason = UTF8.decode(buffer).toString(); + } + } + if (WebSocketDelegateImpl.this.readyState == ReadyState.OPEN) { + //close frame received, echo close frame + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSING; + buffer.flip(); + WsMessage message = new WsMessage(buffer, Kind.CLOSE); + ByteBuffer frame = WsFrameEncodingSupport.rfc6455Encode(message, new Random().nextInt()); + WebSocketDelegateImpl.this.send(frame); + } + if (WebSocketDelegateImpl.this.readyState == ReadyState.CONNECTING) { + WebSocketDelegateImpl.this.readyState = ReadyState.CLOSING; + } + + } + else { + //unknown type + throw new IllegalArgumentException("Unknown message type: " + messageType); + } + } + }); + + Exception exception = null; + + try { + for (;;) { + if (stopReaderThread) { + LOG.fine("SocketReader: Stopping reader thread; closing socket"); + break; + } + + if (!frameProcessor.process(inputStream)) { + LOG.fine("SocketReader: end of stream"); + break; + } + } + } + catch (Exception ex) { + // ex.printStackTrace(); + exception = ex; + } + finally { + handleClose(exception); + } + } + catch (Exception e) { + LOG.log(Level.FINE, "SocketReader: " + e.getMessage(), e); + listener.errorOccurred(new ErrorEvent(e)); + } + } + + private void handleClose(Exception ex) { + WebSocketDelegateImpl.this.handleClose(ex); + } + + private String readLine(InputStream reader) throws Exception { + ByteBuffer input = ByteBuffer.allocate(512); + int ch; + while ((ch = reader.read()) != -1) { + if (!IoBufferUtil.canAccomodate(input, 1)) { + // expand in 512 bytes chunks + input = IoBufferUtil.expandBuffer(input, 512); + } + if (ch == '\n') { + input.put((byte)0x00); + input.flip(); + return new String(input.array()); + } + input.put((byte)ch); + } + return ""; + } + + private void processLine(String line) throws Exception { + LOG.entering(CLASS_NAME, "processLine", line); + + switch (state) { + case START: + if (line.equals(HTTP_101_MESSAGE)) { + state = ConnectionStatus.STATUS_101_READ; + } + else { + String s = "WebSocket upgrade failed: " + line; + LOG.severe(s); + state = ConnectionStatus.ERRORED; + listener.errorOccurred(new ErrorEvent(new IllegalStateException(s))); + } + break; + case STATUS_101_READ: + if (line == null || (line.length() == 0)) { + //end of header, set to Completed + state = ConnectionStatus.COMPLETED; + } + else if (line.indexOf(UPGRADE_HEADER) == 0) { + upgradeReceived = UPGRADE_VALUE.equalsIgnoreCase(line.substring(UPGRADE_HEADER_LENGTH)); + } + else if (line.equals(CONNECTION_MESSAGE)) { + connectionReceived = true; + } + else if (line.indexOf(WEBSOCKET_ACCEPT) == 0) { + String hashedKey = AcceptHash(websocketKey); + websocketAcceptReceived = hashedKey.equals(line.substring(WEBSOCKET_ACCEPT.length() + 1).trim()); + } + break; + case COMPLETED: + break; + } + } + + /** + * Compute the Sec-WebSocket-Accept key (RFC-6455) + * + * @param key + * @return + * @throws NoSuchAlgorithmException + * @throws Exception + */ + public String AcceptHash(String key) throws NoSuchAlgorithmException { + String input = key + WEBSOCKET_GUID; + + MessageDigest sha1 = MessageDigest.getInstance("SHA-1"); + + byte[] hash = sha1.digest(input.getBytes());; + return Base64Util.encode(ByteBuffer.wrap(hash)); + } + } +} + diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateListener.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateListener.java new file mode 100755 index 0000000..fdfdfa1 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WebSocketDelegateListener.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import org.kaazing.gateway.client.transport.AuthenticateEvent; +import org.kaazing.gateway.client.transport.CloseEvent; +import org.kaazing.gateway.client.transport.ErrorEvent; +import org.kaazing.gateway.client.transport.MessageEvent; +import org.kaazing.gateway.client.transport.OpenEvent; +import org.kaazing.gateway.client.transport.RedirectEvent; + +public interface WebSocketDelegateListener { + + void authenticationRequested(AuthenticateEvent authenticateEvent); + void opened(OpenEvent event); + void redirected(RedirectEvent redirectEvent); + void messageReceived(MessageEvent messageEvent); + void closed(CloseEvent event); + void errorOccurred(ErrorEvent event); + +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/WsFrameEncodingSupport.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WsFrameEncodingSupport.java new file mode 100755 index 0000000..0ba998d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WsFrameEncodingSupport.java @@ -0,0 +1,234 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.nio.ByteBuffer; + + +public class WsFrameEncodingSupport { + + /** + * Encode WebSocket message as a single frame, with the provided masking value applied. + */ + public static ByteBuffer rfc6455Encode(WsMessage message, int maskValue) { + final boolean mask = true; + + boolean fin = true; // TODO continued frames? + + ByteBuffer buf = message.getBytes(); + + int remaining = buf.remaining(); + + int offset = 2 + (mask ? 4 : 0) + calculateLengthSize(remaining); + + ByteBuffer b = ByteBuffer.allocate(offset + remaining); + + int start = b.position(); + + byte b1 = (byte) (fin ? 0x80 : 0x00); + byte b2 = (byte) (mask ? 0x80 : 0x00); + + b1 = doEncodeOpcode(b1, message); + b2 |= lenBits(remaining); + + b.put(b1).put(b2); + + doEncodeLength(b, remaining); + + if (mask) { + b.putInt(maskValue); + } + //put message data + b.put(buf); + + if ( mask ) { + b.position(offset); + mask(b, maskValue); + } + + b.limit(b.position()); + b.position(start); + return b; + } + + protected static enum Opcode { + CONTINUATION(0), + TEXT(1), + BINARY(2), + RESERVED3(3), RESERVED4(4), RESERVED5(5), RESERVED6(6), RESERVED7(7), + CLOSE(8), + PING(9), + PONG(10); + + private int code; + + public int getCode() { + return this.code; + } + + Opcode(int code) { + this.code = code; + } + + static Opcode getById(int id) { + Opcode result = null; + for (Opcode temp : Opcode.values()) + { + if(id == temp.code) + { + result = temp; + break; + } + } + + return result; + } + } + + private static int calculateLengthSize(int length) { + if (length < 126) { + return 0; + } else if (length < 65535) { + return 2; + } else { + return 8; + } + } + + + /** + * Encode a WebSocket opcode onto a byte that might have some high bits set. + * + * @param b + * @param message + * @return + */ + private static byte doEncodeOpcode(byte b, WsMessage message) { + switch (message.getKind()) { + case TEXT: { + b |= Opcode.TEXT.getCode(); + break; + } + case BINARY: { + b |= Opcode.BINARY.getCode(); + break; + } + case PING: { + b |= Opcode.PING.getCode(); + break; + } + case PONG: { + b |= Opcode.PONG.getCode(); + break; + } + case CLOSE: { + b |= Opcode.CLOSE.getCode(); + break; + } + default: + throw new IllegalArgumentException("Unrecognized frame type: " + message.getKind()); + } + return b; + } + + private static byte lenBits(int length) { + if (length < 126) { + return (byte) length; + } else if (length < 65535) { + return (byte) 126; + } else { + return (byte) 127; + } + } + + private static void doEncodeLength(ByteBuffer buf, int length) { + if (length < 126) { + return; + } else if (length < 65535) { + // FIXME? unsigned short + buf.putShort((short) length); + } else { + // Unsigned long (should never have a message that large! really!) + buf.putLong((long) length); + } + } + + /** + * Performs an in-situ masking of the readable buf bytes. + * Preserves the position of the buffer whilst masking all the readable bytes, + * such that the masked bytes will be readable after this invocation. + * + * @param buf the buffer containing readable bytes to be masked. + * @param mask the mask to apply against the readable bytes of buffer. + */ + public static void mask(ByteBuffer buf, int mask) { + // masking is the same as unmasking due to the use of bitwise XOR. + unmask(buf, mask); + } + + + /** + * Performs an in-situ unmasking of the readable buf bytes. + * Preserves the position of the buffer whilst unmasking all the readable bytes, + * such that the unmasked bytes will be readable after this invocation. + * + * @param buf the buffer containing readable bytes to be unmasked. + * @param mask the mask to apply against the readable bytes of buffer. + */ + public static void unmask(ByteBuffer buf, int mask) { + byte b; + int remainder = buf.remaining() % 4; + int remaining = buf.remaining() - remainder; + int end = remaining + buf.position(); + + // xor a 32bit word at a time as long as possible + while (buf.position() < end) { + int plaintext = buf.getInt(buf.position()) ^ mask; + buf.putInt(plaintext); + } + + // xor the remaining 3, 2, or 1 bytes + switch (remainder) { + case 3: + b = (byte) (buf.get(buf.position()) ^ ((mask >> 24) & 0xff)); + buf.put(b); + b = (byte) (buf.get(buf.position()) ^ ((mask >> 16) & 0xff)); + buf.put(b); + b = (byte) (buf.get(buf.position()) ^ ((mask >> 8) & 0xff)); + buf.put(b); + break; + case 2: + b = (byte) (buf.get(buf.position()) ^ ((mask >> 24) & 0xff)); + buf.put(b); + b = (byte) (buf.get(buf.position()) ^ ((mask >> 16) & 0xff)); + buf.put(b); + break; + case 1: + b = (byte) (buf.get(buf.position()) ^ (mask >> 24)); + buf.put(b); + break; + case 0: + default: + break; + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/transport/ws/WsMessage.java b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WsMessage.java new file mode 100755 index 0000000..ec8b995 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/transport/ws/WsMessage.java @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.transport.ws; + +import java.nio.ByteBuffer; + +public class WsMessage { + + public static enum Kind { + BINARY, TEXT, CLOSE, COMMAND, PING, PONG + }; + + private Kind kind; + + public Kind getKind() { + return kind; + } + + private final ByteBuffer buf; + + public WsMessage(ByteBuffer buf, Kind kind) { + this.buf = buf; + this.kind = kind; + } + + public ByteBuffer getBytes() { + return buf; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + builder.append(getKind()); + builder.append(':'); + builder.append(' '); + builder.append(buf.array().toString()); + return builder.toString(); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/Base64Util.java b/android/src/main/java/org/kaazing/gateway/client/util/Base64Util.java new file mode 100755 index 0000000..03801ff --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/Base64Util.java @@ -0,0 +1,179 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +import java.util.logging.Logger; + + +/** + * Internal class. This class manages the Base64 encoding and decoding + */ +public class Base64Util { + +// @FlashNative +//import mx.utils.Base64Encoder; +// public String encodeBytes(ByteArray bytes) { +// Base64Encoder encoder=new Base64Encoder(); +// encoder.insertNewLines = false; +// encoder.encodeBytes(bytes); +// return encoder.drain(); +// } + + private static final String CLASS_NAME = Base64Util.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final byte[] INDEXED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".getBytes(); + private static final byte PADDING_BYTE = (byte) '='; + + @Deprecated + private Base64Util() { + LOG.entering(CLASS_NAME, ""); + } + + public static String encode(WrappedByteBuffer decoded) { + LOG.entering(CLASS_NAME, "encode", decoded); + + int decodedSize = decoded.remaining(); + int effectiveDecodedSize = ((decodedSize+2) / 3) * 3; + int decodedFragmentSize = decodedSize % 3; + + int encodedArraySize = effectiveDecodedSize / 3 * 4; + byte[] encodedArray = new byte[encodedArraySize]; + int encodedArrayPosition = 0; + + byte[] decodedArray = decoded.array(); + int decodedArrayOffset = decoded.arrayOffset(); + int decodedArrayPosition = decodedArrayOffset + decoded.position(); + int decodedArrayLimit = decodedArrayOffset + decoded.limit() - decodedFragmentSize; + + while (decodedArrayPosition < decodedArrayLimit) { + int byte0 = decodedArray[decodedArrayPosition++] & 0xff; + int byte1 = decodedArray[decodedArrayPosition++] & 0xff; + int byte2 = decodedArray[decodedArrayPosition++] & 0xff; + + encodedArray[encodedArrayPosition++] = INDEXED[(byte0 >> 2) & 0x3f]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte0 << 4) & 0x30) | ((byte1 >> 4) & 0x0f)]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte1 << 2) & 0x3c) | ((byte2 >> 6) & 0x03)]; + encodedArray[encodedArrayPosition++] = INDEXED[byte2 & 0x3f]; + } + + if (decodedFragmentSize == 1) { + int byte0 = decodedArray[decodedArrayPosition++] & 0xff; + + encodedArray[encodedArrayPosition++] = INDEXED[(byte0 >> 2) & 0x3f]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte0 << 4) & 0x30)]; + encodedArray[encodedArrayPosition++] = PADDING_BYTE; + encodedArray[encodedArrayPosition++] = PADDING_BYTE; + } + else if (decodedFragmentSize == 2) { + int byte0 = decodedArray[decodedArrayPosition++] & 0xff; + int byte1 = decodedArray[decodedArrayPosition++] & 0xff; + + encodedArray[encodedArrayPosition++] = INDEXED[(byte0 >> 2) & 0x3f]; + encodedArray[encodedArrayPosition++] = INDEXED[((byte0 << 4) & 0x30) | ((byte1 >> 4) & 0x0f)]; + encodedArray[encodedArrayPosition++] = INDEXED[(byte1 << 2) & 0x3c]; + encodedArray[encodedArrayPosition++] = PADDING_BYTE; + } + + return new String(encodedArray); + } + + public static WrappedByteBuffer decode(String encoded) { + LOG.entering(CLASS_NAME, "decode", encoded); + + if (encoded == null) { + return null; + } + + int length = encoded.length(); + if (length % 4 != 0) { + throw new IllegalArgumentException("Invalid Base64 Encoded String"); + } + + byte[] encodedArray = encoded.getBytes(); + byte[] decodedArray = new byte[(length / 4 * 3)]; + int decodedArrayOffset = 0; + int i = 0; + while (i < length) { + int char0 = encodedArray[i++]; + int char1 = encodedArray[i++]; + int char2 = encodedArray[i++]; + int char3 = encodedArray[i++]; + + int byte0 = mapped(char0); + int byte1 = mapped(char1); + int byte2 = mapped(char2); + int byte3 = mapped(char3); + + decodedArray[decodedArrayOffset++] = (byte) (((byte0 << 2) & 0xfc) | ((byte1 >> 4) & 0x03)); + if (char2 != PADDING_BYTE) { + decodedArray[decodedArrayOffset++] = (byte) (((byte1 << 4) & 0xf0) | ((byte2 >> 2) & 0x0f)); + if (char3 != PADDING_BYTE) { + decodedArray[decodedArrayOffset++] = (byte) (((byte2 << 6) & 0xc0) | (byte3 & 0x3f)); + } + } + } + return WrappedByteBuffer.wrap(decodedArray, 0, decodedArrayOffset); + } + + private static int mapped(int ch) { + if ((ch & 0x40) != 0) { + if ((ch & 0x20) != 0) { + // a(01100001)-z(01111010) -> 26-51 + assert (ch >= 'a'); + assert (ch <= 'z'); + return (ch - 71); + } else { + // A(01000001)-Z(01011010) -> 0-25 + assert (ch >= 'A'); + assert (ch <= 'Z'); + return (ch - 65); + } + } else if ((ch & 0x20) != 0) { + if ((ch & 0x10) != 0) { + if ((ch & 0x08) != 0 && (ch & 0x04) != 0) { + // =(00111101) -> 0 + assert (ch == '='); + return 0; + } else { + // 0(00110000)-9(00111001) -> 52-61 + assert (ch >= '0'); + assert (ch <= '9'); + return (ch + 4); + } + } else { + if ((ch & 0x04) != 0) { + // /(00101111) -> 63 + assert (ch == '/'); + return 63; + } else { + // +(00101011) -> 62 + assert (ch == '+'); + return 62; + } + } + } else { + LOG.warning("Invalid BASE64 string"); + throw new IllegalArgumentException("Invalid BASE64 string"); + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/GenericURI.java b/android/src/main/java/org/kaazing/gateway/client/util/GenericURI.java new file mode 100755 index 0000000..e4886ab --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/GenericURI.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +import java.net.URI; +import java.net.URISyntaxException; + +public abstract class GenericURI { + + protected URI uri; + + protected GenericURI(String location) throws URISyntaxException { + this(new URI(location)); + } + + protected GenericURI(URI uri) throws URISyntaxException { + this.uri = uri; + validateScheme(); + } + + abstract protected boolean isValidScheme(String scheme); + + private void validateScheme() throws URISyntaxException { + String scheme = getScheme(); + if (!isValidScheme(scheme)) { + throw new URISyntaxException(uri.toString(), "Invalid scheme"); + } + } + + abstract protected T duplicate(URI uri); + + public T replacePath(String path) { + return duplicate(URIUtils.replacePath(uri, path)); + } + + public T addQueryParameter(String newParam) { + String queryParams = uri.getQuery(); + if (queryParams == null) { + queryParams = newParam; + } + else { + queryParams += "&" + newParam; + } + + return duplicate(URIUtils.replaceQueryParameters(uri, queryParams)); + } + + public URI getURI() { + return uri; + } + + public String getHost() { + return uri.getHost(); + } + + public int getPort() { + return uri.getPort(); + } + + public String getScheme() { + return uri.getScheme(); + } + + public String getPath() { + return uri.getPath(); + } + + public String getQuery() { + return uri.getQuery(); + } + + @Override + public String toString() { + return uri.toString(); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/HexUtil.java b/android/src/main/java/org/kaazing/gateway/client/util/HexUtil.java new file mode 100755 index 0000000..4ddfe39 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/HexUtil.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +public class HexUtil { + + private static final byte[] FROM_HEX = { + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 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, 2, 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, 0, 0, 0, + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 10, 11, 12, 13, 14, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + }; + +// public static final WrappedByteBuffer decode(WrappedByteBuffer encoded) { +// LOG.entering(CLASS_NAME, "decode", encoded); +// if (encoded == null) { +// return null; +// } +// +// byte[] decodedArray = new byte[encoded.remaining() / 2]; +// int decodedArrayOffset = 0; +// +// byte[] encodedArray = encoded.array(); +// int encodedArrayOffset = encoded.arrayOffset(); +// int encodedArrayLimit = encodedArrayOffset + encoded.limit(); +// +// for (int i = encodedArrayOffset + encoded.position(); i < encodedArrayLimit;) { +// decodedArray[decodedArrayOffset++] = (byte)((FROM_HEX[encodedArray[i++]] << 4) | FROM_HEX[encodedArray[i++]]); +// } +// WrappedByteBuffer decoded = WrappedByteBuffer.wrap(decodedArray, 0, decodedArrayOffset); +// LOG.exiting(CLASS_NAME, "decode", decoded); +// return decoded; +// } + + public static byte[] decode(byte[] input ) { + if (input == null) { + return null; + } + byte[] output = new byte[input.length/2+1]; + int decodedArrayOffset = 0; + for(int i = 0; i < input.length; ) { + output[decodedArrayOffset++] = (byte)(FROM_HEX[input[i++]] << 4 | FROM_HEX[input[i++]]); + } + return output; + } + + /** + * Converts a hex string into an array of bytes. + * @param s the hex string with two characters for each byte + * @return the byte array corresponding to the hex string + */ + public static byte[] fromHex(String s) { + int len = s.length(); + byte[] data = new byte[len / 2]; + for (int i = 0; i < len; i += 2) { + data[i / 2] = (byte) ((Character.digit(s.charAt(i), 16) << 4) + Character.digit(s.charAt(i + 1), 16)); + } + return data; + } + + /** + * Convert the byte array to an int starting from the given offset. + * + * @param b The byte array + * @param offset The array offset + * @return The integer + */ + public static int byteArrayToInt(byte[] b, int offset) { + int value = 0; + for (int i = 0; i < 4; i++) { + int shift = (4 - 1 - i) * 8; + value += (b[i + offset] & 0x000000FF) << shift; +} + return value; + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/HttpURI.java b/android/src/main/java/org/kaazing/gateway/client/util/HttpURI.java new file mode 100755 index 0000000..479472c --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/HttpURI.java @@ -0,0 +1,66 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +import java.net.URI; +import java.net.URISyntaxException; + + +/** + * URI with guarantee to be a valid, non-null, http or https URI + */ +public class HttpURI extends GenericURI { + + @Override + protected boolean isValidScheme(String scheme) { + return "http".equals(scheme) || "https".equals(scheme); + } + + public HttpURI(String location) throws URISyntaxException { + this(new URI(location)); + } + + HttpURI(URI uri) throws URISyntaxException { + super(uri); + } + + protected HttpURI duplicate(URI uri) { + try { + return new HttpURI(uri); + } catch (URISyntaxException e) { + throw new IllegalArgumentException(e); + } + } + + public boolean isSecure() { + return "https".equals(uri.getScheme()); + } + + public static HttpURI replaceScheme(GenericURI location, String scheme) throws URISyntaxException { + return HttpURI.replaceScheme(location.getURI(), scheme); + } + + public static HttpURI replaceScheme(URI location, String scheme) throws URISyntaxException { + URI uri = URIUtils.replaceScheme(location, scheme); + return new HttpURI(uri); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/StringUtils.java b/android/src/main/java/org/kaazing/gateway/client/util/StringUtils.java new file mode 100755 index 0000000..a7b2023 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/StringUtils.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +import java.io.UnsupportedEncodingException; +import java.util.HashMap; +import java.util.Map; + +public class StringUtils { + private static final Map BASIC_ESCAPE = new HashMap(); + + static { + BASIC_ESCAPE.put("\"", """); // " - double-quote + BASIC_ESCAPE.put("&", "&"); // & - ampersand + BASIC_ESCAPE.put("<", "<"); // < - less-than + BASIC_ESCAPE.put(">", ">"); // > - greater-than + }; + + public static byte[] getUtf8Bytes(String input) { + if (input == null) + return null; + try { + return input.getBytes("UTF-8"); + } catch (UnsupportedEncodingException e) { + throw new IllegalArgumentException(e); + } + } + + /** + * Strip a String of it's ISO control characters. + * + * @param value The String that should be stripped. + * @return {@code String} A new String instance with its hexadecimal control characters replaced by a space. Or the + * unmodified String if it does not contain any ISO control characters. + */ + public static String stripControlCharacters(String rawValue) { + if (rawValue == null) { + return null; + } + + String value = replaceEntities(rawValue); + + boolean hasControlChars = false; + for (int i = value.length() - 1; i >= 0; i--) { + if (Character.isISOControl(value.charAt(i))) { + hasControlChars = true; + break; + } + } + + if (!hasControlChars) { + return value; + } + + StringBuilder buf = new StringBuilder(value.length()); + int i = 0; + + // Skip initial control characters (i.e. left trim) + for (; i < value.length(); i++) { + if (!Character.isISOControl(value.charAt(i))) { + break; + } + } + + // Copy non control characters and substitute control characters with + // a space. The last control characters are trimmed. + boolean suppressingControlChars = false; + for (; i < value.length(); i++) { + if (Character.isISOControl(value.charAt(i))) { + suppressingControlChars = true; + continue; + } else { + if (suppressingControlChars) { + suppressingControlChars = false; + buf.append(' '); + } + buf.append(value.charAt(i)); + } + } + + return buf.toString(); + } + + private static String replaceEntities(String value) { + if (value == null) { + return null; + } + + StringBuilder sb = new StringBuilder(value.length()); + for (int i = 0; i < value.length(); i++) { + String c = String.valueOf(Character.toChars(Character.codePointAt(value, i))); + CharSequence escapedEntity = BASIC_ESCAPE.get(c); + + if (escapedEntity == null) { + sb.append(c); + } else { + sb.append(escapedEntity); + } + } + + return sb.toString(); + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/URIUtils.java b/android/src/main/java/org/kaazing/gateway/client/util/URIUtils.java new file mode 100755 index 0000000..f4f577d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/URIUtils.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +import java.net.URI; +import java.net.URISyntaxException; + +public class URIUtils { + + public static URI replaceScheme(String location, String scheme) { + try { + return replaceScheme(new URI(location), scheme); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI/Scheme: replacing scheme with "+scheme+" for "+location); + } + } + + public static URI replaceScheme(URI uri, String scheme) { + try { + String location = uri.toString(); + int index = location.indexOf("://"); + return new URI(scheme + location.substring(index)); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI/Scheme: replacing scheme with "+scheme+" for "+uri); + } + } + + public static URI replacePath(URI uri, String path) { + try { + return new URI(uri.getScheme(), uri.getAuthority(), path, uri.getQuery(), uri.getFragment()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI/Scheme: replacing path with '"+path+"' for "+uri); + } + } + + public static URI replaceQueryParameters(URI uri, String queryParams) { + try { + return new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), queryParams, uri.getFragment()); + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Invalid URI/Scheme: replacing query parameters with '"+queryParams+"' for "+uri); + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/WrappedByteBuffer.java b/android/src/main/java/org/kaazing/gateway/client/util/WrappedByteBuffer.java new file mode 100755 index 0000000..7d01c66 --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/WrappedByteBuffer.java @@ -0,0 +1,1247 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util; + +import java.nio.BufferUnderflowException; +import java.nio.ByteOrder; +import java.nio.charset.Charset; + +/** + * WrappedByteBuffer provides an automatically expanding byte buffer for ByteSocket. + */ +public class WrappedByteBuffer { + /* Invariant: 0 <= mark <= position <= limit <= capacity */ + + static int INITIAL_CAPACITY = 128; + + java.nio.ByteBuffer _buf; + private final int _minimumCapacity; + private boolean autoExpand = true; + private boolean _isBigEndian = true; + + /** + * The order property indicates the endianness of multibyte integer types in the buffer. + * Defaults to ByteOrder.BIG_ENDIAN; + */ + public WrappedByteBuffer() { + this(INITIAL_CAPACITY); + } + + WrappedByteBuffer(int capacity) { + this(java.nio.ByteBuffer.allocate(capacity)); + } + + public WrappedByteBuffer(byte[] bytes) { + this(bytes, 0, bytes.length); + } + + WrappedByteBuffer(byte[] bytes, int offset, int length) { + this(java.nio.ByteBuffer.wrap(bytes, offset, length)); + } + + public WrappedByteBuffer(java.nio.ByteBuffer buf) { + _buf = buf; + _minimumCapacity = buf.capacity(); + } + + public java.nio.ByteBuffer getNioByteBuffer() { + return _buf; + } + + /** + * Sets whether the buffer is auto-expanded when bytes are added at the limit. + */ + public final WrappedByteBuffer setAutoExpand(boolean autoExpand) { + this.autoExpand = autoExpand; + return this; + } + + /** + * Return true if the buffer is auto-expanded when bytes are added at the limit. + * + * @return whether the buffer is auto-expanded. + */ + public final boolean isAutoExpand() { + return autoExpand; + } + + /** + * Return underlying byte array offset + * + * @return underlying byte array offset + */ + public final int arrayOffset() { + int ao = _buf.arrayOffset(); + return ao; + } + + /** + * Return underlying byte array + * + * @return returns the contents of the buffer as a byte array + */ + public final byte[] array() { + byte[] a = _buf.array(); + return a; + } + + /** + * Allocates a new WrappedByteBuffer instance. + * + * @param capacity + * the maximum buffer capacity + * + * @return the allocated WrappedByteBuffer + */ + public static WrappedByteBuffer allocate(int capacity) { + return new WrappedByteBuffer(capacity); + } + + /** + * Wraps a byte array as a new WrappedByteBuffer instance. + * + * @param bytes + * an array of byte-sized numbers + * + * @return the bytes wrapped as a WrappedByteBuffer + */ + public static WrappedByteBuffer wrap(byte[] bytes) { + return new WrappedByteBuffer(bytes); + } + + /** + * Wraps a byte array as a new WrappedByteBuffer instance. + * + * @param bytes + * an array of byte-sized numbers + * + * @return the bytes wrapped as a WrappedByteBuffer + */ + public static WrappedByteBuffer wrap(byte[] bytes, int offset, int length) { + return new WrappedByteBuffer(bytes, offset, length); + } + + /** + * Wraps a java.nio.ByteBuffer as a new WrappedByteBuffer instance. + * + * @param bytes + * an array of byte-sized numbers + * + * @return the bytes wrapped as a WrappedByteBuffer + */ + public static WrappedByteBuffer wrap(java.nio.ByteBuffer in) { + return new WrappedByteBuffer(in); + } + + /** + * Returns the ByteOrder of the buffer + * + * @return + */ + public ByteOrder order() { + ByteOrder bo = _buf.order(); + return bo; + } + + /** + * Set the byte order for this buffer + * + * @param o ByteOrder + * + * @return the ByteOrder specified + * + * @deprecated Will return WrappedByteBuffer in future releases to match java.nio.ByteBuffer + */ + public ByteOrder order(ByteOrder o) { + // TODO: In the next release, return WrappedByteBuffer - use return _buf.order(o) + _isBigEndian = "BIG_ENDIAN".equals(o.toString()); + _buf.order(o); + return o; + } + + /** + * Marks a position in the buffer. + * + * @return the buffer + * + * @see WrappedByteBuffer#reset + */ + public WrappedByteBuffer mark() { + _buf.mark(); + return this; + } + + /** + * Resets the buffer position using the mark. + * + * @throws Exception + * if the mark is invalid + * + * @return the buffer + * + * @see WrappedByteBuffer#mark + */ + public WrappedByteBuffer reset() { + _buf = (java.nio.ByteBuffer) _buf.reset(); + return this; + } + + private static final int max(int a, int b) { + return a > b ? a : b; + } + + /** + * Compacts the buffer by removing leading bytes up to the buffer position, and decrements the limit and position values + * accordingly. + * + * @return the buffer + */ + public WrappedByteBuffer compact() { + int remaining = remaining(); + int capacity = capacity(); + + if (capacity == 0) { + return this; + } + + if (remaining <= capacity >>> 2 && capacity > _minimumCapacity) { + int newCapacity = capacity; + int minCapacity = max(_minimumCapacity, remaining << 1); + for (;;) { + if (newCapacity >>> 1 < minCapacity) { + break; + } + newCapacity >>>= 1; + } + + newCapacity = max(minCapacity, newCapacity); + + if (newCapacity == capacity) { + if (_buf.remaining() == 0) { + _buf.position(0); + _buf.limit(_buf.capacity()); + } + else { + java.nio.ByteBuffer dup = _buf.duplicate(); + _buf.position(0); + _buf.limit(_buf.capacity()); + _buf.put(dup); + } + return this; + } + + // Shrink and compact: + // // Save the state. + ByteOrder bo = order(); + + // // Sanity check. + if (remaining > newCapacity) { + throw new IllegalStateException("The amount of the remaining bytes is greater than " + "the new capacity."); + } + + // // Reallocate. + java.nio.ByteBuffer oldBuf = _buf; + java.nio.ByteBuffer newBuf = java.nio.ByteBuffer.allocate(newCapacity); + newBuf.put(oldBuf); + _buf = newBuf; + + // // Restore the state. + _buf.order(bo); + } else { + _buf.compact(); + } + + return this; + } + + /** + * Duplicates the buffer by reusing the underlying byte array but with independent position, limit and capacity. + * + * @return the duplicated buffer + */ + public WrappedByteBuffer duplicate() { + return WrappedByteBuffer.wrap(_buf.duplicate()); + } + + /** + * Fills the buffer with a repeated number of zeros. + * + * @param size + * the number of zeros to repeat + * + * @return the buffer + */ + public WrappedByteBuffer fill(int size) { + _autoExpand(size); + while (size-- > 0) { + _buf.put((byte) 0); + } + return this; + } + + /** + * Fills the buffer with a specific number of repeated bytes. + * + * @param b + * the byte to repeat + * @param size + * the number of times to repeat + * + * @return the buffer + */ + public WrappedByteBuffer fillWith(byte b, int size) { + _autoExpand(size); + while (size-- > 0) { + _buf.put(b); + } + return this; + } + + /** + * Returns the index of the specified byte in the buffer. + * + * @param b + * the byte to find + * + * @return the index of the byte in the buffer, or -1 if not found + */ + public int indexOf(byte b) { + if (_buf.hasArray()) { + byte[] array = _buf.array(); + int arrayOffset = _buf.arrayOffset(); + int startAt = arrayOffset + position(); + int endAt = arrayOffset + limit(); + + for (int i = startAt; i < endAt; i++) { + if (array[i] == b) { + return i - arrayOffset; + } + } + return -1; + } else { + int startAt = _buf.position(); + int endAt = _buf.limit(); + + for (int i = startAt; i < endAt; i++) { + if (_buf.get(i) == b) { + return i; + } + } + return -1; + } + } + + /** + * Puts a single byte number into the buffer at the current position. + * + * @param v + * the single-byte number + * + * @return the buffer + */ + public WrappedByteBuffer put(byte v) { + _autoExpand(1); + _buf.put(v); + return this; + } + + /** + * Puts segment of a single-byte array into the buffer at the current position. + * + * @param v + * the source single-byte array + * @offset + * the start position of source array + * @length + * the length to put into WrappedByteBuffer + * @return the buffer + */ + public WrappedByteBuffer put(byte[] v, int offset, int length) { + _autoExpand(length); + for (int i = 0; i < length; i++) { + _buf.put(v[offset + i]); + } + return this; + } + + /** + * Puts a single byte number into the buffer at the specified index. + * + * @param index the index + * @param v the byte + * + * @return the buffer + */ + public WrappedByteBuffer putAt(int index, byte v) { + _checkForWriteAt(index, 1); + _buf.put(index, v); + return this; + } + + /** + * Puts a unsigned single-byte number into the buffer at the current position. + * + * @param v the unsigned byte as an int + * + * @return the buffer + */ + public WrappedByteBuffer putUnsigned(int v) { + byte b = (byte)(v & 0xFF); + return this.put(b); + } + + /** + * Puts an unsigned single byte into the buffer at the specified position. + * + * @param v the unsigned byte as an int + * + * @return the buffer + */ + public WrappedByteBuffer putUnsignedAt(int index, int v) { + _checkForWriteAt(index, 1); + byte b = (byte)(v & 0xFF); + return this.putAt(index, b); + } + + /** + * Puts a two-byte short into the buffer at the current position. + * + * @param v the two-byte short value + * + * @return the buffer + */ + public WrappedByteBuffer putShort(short v) { + _autoExpand(2); + _buf.putShort(v); + return this; + } + + /** + * Puts a two-byte short into the buffer at the specified index. + * + * @param index the index + * @param v the two-byte short + * + * @return the buffer + */ + public WrappedByteBuffer putShortAt(int index, short v) { + _checkForWriteAt(index, 2); + _buf.putShort(index, v); + return this; + } + + /** + * Puts a two-byte unsigned short into the buffer at the current position. + * + * @param v the two-byte short value + * + * @return the buffer + */ + public WrappedByteBuffer putUnsignedShort(int v) { + this.putShort((short)(v & 0xFFFF)); + return this; + } + + /** + * Puts an unsigned two-byte unsigned short into the buffer at the position specified. + * + * @param index the index + * @param v the short value + * + * @return the buffer + */ + public WrappedByteBuffer putUnsignedShortAt(int index, int v) { + _checkForWriteAt(index, 2); + this.putShortAt(index, (short)(v & 0xFFFF)); + return this; + } + + /** + * Puts a four-byte int into the buffer at the current position. + * + * @param v the four-byte int + * + * @return the buffer + */ + public WrappedByteBuffer putInt(int v) { + _autoExpand(4); + _buf.putInt(v); + return this; + } + + /** + * Puts a four-byte int into the buffer at the specified index. + * + * @param index the index + * @param v the four-byte int + * + * @return the buffer + */ + public WrappedByteBuffer putIntAt(int index, int v) { + _checkForWriteAt(index, 4); + _buf.putInt(index, v); + return this; + } + + /** + * Puts a four-byte array into the buffer at the current position. + * + * @param v the four-byte int + * + * @return the buffer + */ + public WrappedByteBuffer putUnsignedInt(long value) { + this.putInt((int)value & 0xFFFFFFFF); + return this; + } + + /** + * Puts an unsigned four-byte array into the buffer at the specified index. + * + * @param index the index + * @param v the four-byte int + * + * @return the buffer + */ + public WrappedByteBuffer putUnsignedIntAt(int index, long value) { + _checkForWriteAt(index, 4); + this.putIntAt(index, (int)value & 0xFFFFFFFF); + return this; + } + + /** + * Puts an eight-byte long into the buffer at the current position. + * + * @param v the eight-byte long + * + * @return the buffer + */ + public WrappedByteBuffer putLong(long v) { + _autoExpand(8); + _buf.putLong(v); + return this; + } + + /** + * Puts an eight-byte long into the buffer at the specified index. + * + * @param index the index + * @param v the eight-byte long + * + * @return the buffer + */ + public WrappedByteBuffer putLongAt(int index, long v) { + _checkForWriteAt(index, 8); + _buf.putLong(index, v); + return this; + } + + /** + * Puts a string into the buffer at the current position, using the character set to encode the string as bytes. + * + * @param v + * the string + * @param cs + * the character set + * + * @return the buffer + */ + public WrappedByteBuffer putString(String v, Charset cs) { + java.nio.ByteBuffer strBuf = cs.encode(v); + _autoExpand(strBuf.limit()); + _buf.put(strBuf); + return this; + } + + /** + * Puts a string into the buffer at the specified index, using the character set to encode the string as bytes. + * + * @param fieldSize + * the width in bytes of the prefixed length field + * @param v + * the string + * @param cs + * the character set + * + * @return the buffer + */ + public WrappedByteBuffer putPrefixedString(int fieldSize, String v, Charset cs) { + if (fieldSize == 0) { + return this; + } + boolean utf16 = cs.name().startsWith("UTF-16"); + + if (utf16 && (fieldSize == 1)) { + throw new IllegalArgumentException("fieldSize is not even for UTF-16 character set"); + } + + java.nio.ByteBuffer strBuf = cs.encode(v); + _autoExpand(fieldSize + strBuf.limit()); + + int len = strBuf.remaining(); + switch (fieldSize) { + case 1: + put((byte) len); + break; + case 2: + putShort((short) len); + break; + case 4: + putInt(len); + break; + default: + throw new IllegalArgumentException("Illegal argument, field size should be 1,2 or 4 and fieldSize is: " + fieldSize); + } + + _buf.put(strBuf); + return this; + } + + /** + * Puts a single-byte array into the buffer at the current position. + * + * @param v + * the single-byte array + * + * @return the buffer + */ + public WrappedByteBuffer putBytes(byte[] b) { + _autoExpand(b.length); + _buf.put(b); + return this; + } + + /** + * Puts a single-byte array into the buffer at the specified index. + * + * @param index the index + * @param v the single-byte array + * + * @return the buffer + */ + public WrappedByteBuffer putBytesAt(int index, byte[] b) { + _checkForWriteAt(index, b.length); + int pos = _buf.position(); + _buf.position(index); + _buf.put(b, 0, b.length); + _buf.position(pos); + return this; + } + + /** + * Puts a buffer into the buffer at the current position. + * + * @param v the WrappedByteBuffer + * + * @return the buffer + */ + public WrappedByteBuffer putBuffer(WrappedByteBuffer v) { + _autoExpand(v.remaining()); + _buf.put(v._buf); + return this; + } + + /** + * Puts a buffer into the buffer at the specified index. + * + * @param index the index + * @param v the WrappedByteBuffer + * + * @return the buffer + */ + public WrappedByteBuffer putBufferAt(int index, WrappedByteBuffer v) { + // TODO: I believe this method incorrectly moves the position! + // Can't change it without a code analysis - and this is a public API! + int pos = _buf.position(); + _buf.position(index); + _buf.put(v._buf); + _buf.position(pos); + return this; + } + + /** + * Returns a single byte from the buffer at the current position. + * + * @return the byte + */ + public byte get() { + _checkForRead(1); + byte v = _buf.get(); + return v; + } + + /** + * @see java.nio.ByteBuffer#get(byte[], int, int) + */ + public WrappedByteBuffer get(byte[] dst, int offset, int length) { + _checkForRead(length); + for (int i = 0; i < length; i++) { + dst[offset + i] = _buf.get(); + } + return this; + } + + /** + * @see java.nio.ByteBuffer#get(byte[], int, int) + */ + public WrappedByteBuffer get(byte[] dst) { + return get(dst, 0, dst.length); + } + + /** + * Returns a byte from the buffer at the specified index. + * + * @param index the index + * + * @return the byte + */ + public byte getAt(int index) { + _checkForReadAt(index,1); + byte v = _buf.get(index); + return v; + } + + /** + * Returns an unsigned byte from the buffer at the current position. + * + * @return the unsigned byte as an int + */ + public int getUnsigned() { + _checkForRead(1); + int val = ((int) (_buf.get() & 0xff)); + return val; + } + /** + * Returns an unsigned byte from the buffer at the specified index. + * + * @param index the index + * + * @return the unsigned byte as an int + */ + public int getUnsignedAt(int index) { + _checkForReadAt(index, 1); + int val = ((int) (_buf.get(index) & 0xff)); + return val; + } + /** + * Returns a byte array of length size from the buffer from current position + * @param size + * @return a new byte array with bytes read from the buffer + */ + public byte[] getBytes(int size){ + _checkForRead(size); + byte[] dst = new byte[size]; + _buf.get(dst, 0, size); + return dst; + } + /** + * Returns a byte array of length size from the buffer starting from the specified position. + * @param index the index + * @param size the size of the buffer to be returned + * @return a new byte array with bytes read from the buffer + */ + public byte[] getBytesAt(int index,int size){ + _checkForReadAt(index,size); + byte[] dst = new byte[size]; + int i=0; + int j=index; + while (i INITIAL_CAPACITY) ? expectedRemaining : INITIAL_CAPACITY); + java.nio.ByteBuffer newBuffer = java.nio.ByteBuffer.allocate(newCapacity); + _buf.flip(); + newBuffer.put(_buf); + _buf = newBuffer; + } + } + + return this; + } + + private void _autoExpandAt(int i, int expectedRemaining) { + if (autoExpand) { + expandAt(i, expectedRemaining); + } + } + + private void _autoExpand(int expectedRemaining) { + if (autoExpand) { + expand(expectedRemaining); + } + } + + private void _checkForRead(int expected) { + int end = _buf.position() + expected; + if (end > _buf.limit()) { + throw new BufferUnderflowException(); + } + } + + private void _checkForReadAt(int index, int expected) { + int end = index + expected; + if (index < 0 || end > _buf.limit()) { + throw new IndexOutOfBoundsException(); + } + } + + private void _checkForWriteAt(int index, int expected) { + int end = index + expected; + if (index < 0 || end > _buf.limit()) { + throw new IndexOutOfBoundsException(); + } + } +} diff --git a/android/src/main/java/org/kaazing/gateway/client/util/auth/LoginHandlerProvider.java b/android/src/main/java/org/kaazing/gateway/client/util/auth/LoginHandlerProvider.java new file mode 100755 index 0000000..51abf8d --- /dev/null +++ b/android/src/main/java/org/kaazing/gateway/client/util/auth/LoginHandlerProvider.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.gateway.client.util.auth; + +import org.kaazing.net.auth.LoginHandler; + +/** + * An internal marker interface to mark implementations of challenge handlers + * that may in fact provide access to Login Handlers. + * + */ +public interface LoginHandlerProvider { + + /** + * Get the login handler associated with this challenge handler. + * A login handler is used to assist in obtaining credentials to respond to challenge requests. + * + * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet. + */ + public LoginHandler getLoginHandler(); +} diff --git a/android/src/main/java/org/kaazing/net/URLFactory.java b/android/src/main/java/org/kaazing/net/URLFactory.java new file mode 100755 index 0000000..2c8891b --- /dev/null +++ b/android/src/main/java/org/kaazing/net/URLFactory.java @@ -0,0 +1,206 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLStreamHandler; +import java.net.URLStreamHandlerFactory; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +/** + * {@link URLFactory} supports static methods to instantiate URL objects that + * support custom protocols/schemes. Since {@link URL} by default only + * guarantees protocol handlers for + *

+ * {@code + * http, https, ftp, file, and jar + * } + *

+ * and the {@link URLStreamHandlerFactory} registration is not extensible, + * URLFactory will allow application developers to create URL objects for + * protocols such as + *

+ * {@code + * ws, wse, wsn, wss, wse+ssl + * } + *

+ * like this: + *

+ * {@code + * URL url = URLFactory.createURL("ws://:/"); + * } + */ +public final class URLFactory { + private static final Map _factories; + + private URLFactory() { + + } + + static { + Class clazz = URLStreamHandlerFactorySpi.class; + ServiceLoader loader = ServiceLoader.load(clazz); + _factories = new HashMap(); + + for (URLStreamHandlerFactorySpi factory : loader) { + Collection protocols = factory.getSupportedProtocols(); + + if (protocols != null && !protocols.isEmpty()) { + for (String protocol : protocols) { + _factories.put(protocol, factory); + } + } + } + } + + /** + * Creates a URL object from the String representation. + * + * @param spec the String to parse as a URL. + * @return URL representing the passed in String + * @throws MalformedURLException if no protocol is specified, or an + * unknown protocol is found, or spec is null + */ + public static URL createURL(String spec) throws MalformedURLException { + return createURL(null, spec); + } + + /** + * Creates a URL by parsing the given spec within a specified context. The + * new URL is created from the given context URL and the spec argument as + * described in RFC2396 "Uniform Resource Identifiers : Generic Syntax" : + *

+ * {@code + * ://?# + * } + *

+ * The reference is parsed into the scheme, authority, path, query and + * fragment parts. If the path component is empty and the scheme, authority, + * and query components are undefined, then the new URL is a reference to + * the current document. Otherwise, the fragment and query parts present in + * the spec are used in the new URL. + *

+ * If the scheme component is defined in the given spec and does not match + * the scheme of the context, then the new URL is created as an absolute URL + * based on the spec alone. Otherwise the scheme component is inherited from + * the context URL. + *

+ * If the authority component is present in the spec then the spec is + * treated as absolute and the spec authority and path will replace the + * context authority and path. If the authority component is absent in the + * spec then the authority of the new URL will be inherited from the context. + *

+ * If the spec's path component begins with a slash character "/" then the + * path is treated as absolute and the spec path replaces the context path. + *

+ * Otherwise, the path is treated as a relative path and is appended to the + * context path, as described in RFC2396. Also, in this case, the path is + * canonicalized through the removal of directory changes made by + * occurrences of ".." and ".". + *

+ * For a more detailed description of URL parsing, refer to RFC2396. + * + * @param context the context in which to parse the specification + * @param spec the String to parse as a URL + * @return URL created using the spec within the specified context + * @throws MalformedURLException if no protocol is specified, or an unknown + * protocol is found, or spec is null. + */ + public static URL createURL(URL context, String spec) + throws MalformedURLException { + if ((spec == null) || (spec.trim().length() == 0)) { + return new URL(context, spec); + } + + String protocol = URI.create(spec).getScheme(); + URLStreamHandlerFactory factory = _factories.get(protocol); + + // If there is no URLStreamHandlerFactory registered for the + // scheme/protocol, then we just use the regular URL constructor. + if (factory == null) { + return new URL(context, spec); + } + + // If there is a URLStreamHandlerFactory associated for the + // scheme/protocol, then we create a URLStreamHandler. And, then use + // then use the URLStreamHandler to create a URL. + URLStreamHandler handler = factory.createURLStreamHandler(protocol); + return new URL(context, spec, handler); + } + + /** + * Creates a URL from the specified protocol name, host name, and file name. + * The default port for the specified protocol is used. + *

+ * This method is equivalent to calling the four-argument method with + * the arguments being protocol, host, -1, and file. No validation of the + * inputs is performed by this method. + * + * @param protocol the name of the protocol to use + * @param host the name of the host + * @param file the file on the host + * @return URL created using specified protocol, host, and file + * @throws MalformedURLException if an unknown protocol is specified + */ + public static URL createURL(String protocol, String host, String file) + throws MalformedURLException { + return createURL(protocol, host, -1, file); + + } + + /** + * Creates a URL from the specified protocol name, host name, port number, + * and file name. + *

+ * No validation of the inputs is performed by this method. + * + * @param protocol the name of the protocol to use + * @param host the name of the host + * @param port the port number + * @param file the file on the host + * @return URL created using specified protocol, host, and file + * @throws MalformedURLException if an unknown protocol is specified + */ + public static URL createURL(String protocol, + String host, + int port, + String file) throws MalformedURLException { + URLStreamHandlerFactory factory = _factories.get(protocol); + + // If there is no URLStreamHandlerFactory registered for the + // scheme/protocol, then we just use the regular URL constructor. + if (factory == null) { + return new URL(protocol, host, port, file); + } + + // If there is a URLStreamHandlerFactory associated for the + // scheme/protocol, then we create a URLStreamHandler. And, then use + // then use the URLStreamHandler to create a URL. + URLStreamHandler handler = factory.createURLStreamHandler(protocol); + return new URL(protocol, host, port, file, handler); + } +} + diff --git a/android/src/main/java/org/kaazing/net/URLStreamHandlerFactorySpi.java b/android/src/main/java/org/kaazing/net/URLStreamHandlerFactorySpi.java new file mode 100755 index 0000000..4d5ce9d --- /dev/null +++ b/android/src/main/java/org/kaazing/net/URLStreamHandlerFactorySpi.java @@ -0,0 +1,54 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net; + +import java.net.URLStreamHandlerFactory; +import java.util.Collection; + +/** + * This a Service Provider Interface (SPI) class. Implementors + * can create extensions of this class. At runtime, the extensions will be + * instantiated using the {@link ServiceLoader} APIs using the META-INF/services + * mechanism in the {@link URLFactory} implementation. + *

+ * {@link URLStreamHandlerFactory} is a singleton that is registered using the + * static method + * {@link URL#setURLStreamHandlerFactory(URLStreamHandlerFactory)}. Also, + * the {@link URL} objects can only be created for the following protocols: + * -- http, https, file, ftp, and jar. In order to install protocol handlers + * for other protocols, one has to hijack or override the system's singleton + * {@link URLStreamHandlerFactory} instance with a custom implementation. The + * objective of this class is to make the {@link URLStreamHandler} registration + * for other protocols such as ws, wss, etc. feasible without hijacking the + * system's {@link URLStreamHandlerFactory}. + *

+ */ +public abstract class URLStreamHandlerFactorySpi implements URLStreamHandlerFactory { + + /** + * Returns a list of supported protocols. This can be used to instantiate + * appropriate {@link URLStreamHandler} objects based on the protocol. + * + * @return list of supported protocols + */ + public abstract Collection getSupportedProtocols(); +} + diff --git a/android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java new file mode 100755 index 0000000..21ee8a9 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/BasicChallengeHandler.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + + +/** + * Challenge handler for Basic authentication as defined in RFC 2617. + *

+ * This BasicChallengeHandler can be loaded and instantiated using + * {@link BasicChallengeHandler#create()}, and registered + * at a location using {@link DispatchChallengeHandler#register(String, ChallengeHandler)}. + *

+ * In addition, one can install general and realm-specific {@link LoginHandler} objects onto this + * {@link BasicChallengeHandler} to assist in handling challenges associated + * with any or specific realms. This can be achieved using {@link #setLoginHandler(LoginHandler)} and + * {@link #setRealmLoginHandler(String, LoginHandler)} methods. + *

+ * The following example loads an instance of a {@link BasicChallengeHandler}, sets a login + * handler onto it and registers the basic handler at a URI location. In this way, all attempts to access + * that URI for which the server issues "Basic" challenges are handled by the registered {@link BasicChallengeHandler}. + *

+ * {@code
+ * LoginHandler loginHandler = new LoginHandler() {
+ *    public PasswordAuthentication getCredentials() {
+ *        return new PasswordAuthentication("global", "credentials".toCharArray());
+ *    }
+ * };
+ * BasicChallengeHandler    bch = BasicChallengeHandler.create();
+ * DispatchChallengeHandler dch = DispatchChallengeHandler.create();
+ * WebSocketFactory     wsFactory = WebSocketFactory.createWebSocketFactory();
+ * wsFactory.setDefaultChallengeHandler(dch.register("ws://localhost:8001", bch.setLoginHandler(loginHandler)));
+ * }
+ * 
+ * + * @see RFC 2616 - HTTP 1.1 + * @see RFC 2617 Section 2 - Basic Authentication + */ +public abstract class BasicChallengeHandler extends ChallengeHandler { + + /** + * Creates a new instance of {@link BasicChallengeHandler} using the + * {@link ServiceLoader} API with the implementation specified under + * META-INF/services. + * + * @return BasicChallengeHandler + */ + public static BasicChallengeHandler create() { + return create(BasicChallengeHandler.class); + } + + /** + * Creates a new instance of {@link BasicChallengeHandler} with the + * specified {@link ClassLoader} using the {@link ServiceLoader} API with + * the implementation specified under META-INF/services. + * + * @param classLoader ClassLoader to be used to instantiate + * @return BasicChallengeHandler + */ + public static BasicChallengeHandler create(ClassLoader classLoader) { + return create(BasicChallengeHandler.class, classLoader); + } + + /** + * Set a Login Handler to be used if and only if a challenge request has + * a realm parameter matching the provided realm. + * + * @param realm the realm upon which to apply the {@code loginHandler}. + * @param loginHandler the login handler to use for the provided realm. + */ + public abstract void setRealmLoginHandler(String realm, LoginHandler loginHandler); + + + /** + * Provide a general (non-realm-specific) login handler to be used in association with this challenge handler. + * The login handler is used to assist in obtaining credentials to respond to any Basic + * challenge requests when no realm-specific login handler matches the realm parameter of the request (if any). + * + * @param loginHandler a login handler for credentials. + */ + public abstract BasicChallengeHandler setLoginHandler(LoginHandler loginHandler); + + /** + * Get the general (non-realm-specific) login handler associated with this challenge handler. + * A login handler is used to assist in obtaining credentials to respond to challenge requests. + * + * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet. + */ + public abstract LoginHandler getLoginHandler() ; +} diff --git a/android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java new file mode 100755 index 0000000..b57578b --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/ChallengeHandler.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + +import java.util.ServiceLoader; + +/** + * A ChallengeHandler is responsible for producing responses to authentication challenges. + *

+ * When an attempt to access a protected URI is made, the server responsible for serving the resource + * may respond with a challenge, indicating that credentials need be provided before access to the + * resource is granted. The specific type of challenge is indicated in a HTTP header called "WWW-Authenticate". + * This response and that header are converted into a {@link ChallengeRequest} and sent to a + * registered ChallengeHandler for authentication challenge responses. The {@link ChallengeResponse} credentials + * generated by a registered challenge handler are included in a replay of the original HTTP request to the server, which + * (assuming the credentials are sufficient) allows access to the resource. + *

+ * Public subclasses of ChallengeHandler can be loaded and instantiated using {@link ChallengeHandlers}, + * and registered to handle server challenges for specific URI locations + * using {@link DispatchChallengeHandler#register(String, ChallengeHandler)}. + *

+ * Any challenge responses to requests matching the registered location may be handled by the registered {@link ChallengeHandler} + * as long as {@link #canHandle(ChallengeRequest)} returns true. In the case where multiple registered challenge handlers + * could respond to a challenge request, the earliest challenge handler registered at the most specific location matching + * the protected URI is selected. + * + */ +public abstract class ChallengeHandler { + /** + * Creates a new instance of the sub-class of {@link ChallengeHandler} using + * the {@link ServiceLoader} API with the implementation specified under + * META-INF/services. + * + * @param T sub-class of ChallengeHandler + * @param clazz Class object of the sub-type + * @return ChallengeHandler + */ + protected static T create(Class clazz) { + return load0(clazz, ServiceLoader.load(clazz)); + } + + /** + * Creates a new instance of the sub-class of {@link ChallengeHandler} with + * specified {@link ClassLoader} using the {@link ServiceLoader} API with + * the implementation specified under META-INF/services. + * + * @param T sub-type of ChallengeHandler + * @param clazz Class object of the sub-type + * @param classLoader ClassLoader to be used to instantiate + * @return ChallengeHandler + */ + protected static T create(Class clazz, + ClassLoader clazzLoader) { + return load0(clazz, ServiceLoader.load(clazz, clazzLoader)); + } + + /** + * Can the presented challenge be potentially handled by this challenge handler? + * + * @param challengeRequest a challenge request object containing a challenge + * @return true iff this challenge handler could potentially respond meaningfully to the challenge. + */ + public abstract boolean canHandle(ChallengeRequest challengeRequest); + + /** + * Handle the presented challenge by creating a challenge response or returning {@code null}. + * This responsibility is usually achieved + * by using the associated {@link LoginHandler} to obtain user credentials, and transforming those credentials + * into a {@link ChallengeResponse}. + *

+ * When it is not possible to create a {@link ChallengeResponse}, this method MUST return {@code null}. + * + * @param challengeRequest a challenge object + * @return a challenge response object or {@code null} if no response is possible. + */ + public abstract ChallengeResponse handle(ChallengeRequest challengeRequest); + + // ----------------------- Private Methods ------------------------------- + + private static T load0(Class clazz, + ServiceLoader serviceLoader) { + for (ChallengeHandler challengeHandler: serviceLoader) { + Class c = challengeHandler.getClass(); + if ((clazz != null) && clazz.isAssignableFrom(c)) { + try { + return clazz.cast(c.newInstance()); + } catch (InstantiationException e) { + String s = "Failed to instantiate class " + c; + throw new RuntimeException(s,e); + } catch (IllegalAccessException e) { + String s = "Failed to access and instantiate class " + c; + throw new RuntimeException(s, e); + } + } + } + return null; + } + +} diff --git a/android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java b/android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java new file mode 100755 index 0000000..077a9e7 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/ChallengeRequest.java @@ -0,0 +1,118 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + +/** + * An immutable object representing the challenge presented by the server when the client accessed + * the URI represented by a location. + *

+ * According to RFC 2617, + *

+ *     challenge   = auth-scheme 1*SP 1#auth-param
+ * 
+ * so we model the authentication scheme and parameters in this class. + *

+ * This class is also responsible for detecting and adapting the {@code Application Basic} + * and {@code Application Negotiate} authentication schemes into their {@code Basic} and {@code Negotiate} + * counterpart authentication schemes. + */ +public class ChallengeRequest { + + private static final String APPLICATION_PREFIX = "Application "; + + + String location; + String authenticationScheme; + String authenticationParameters; + + + /** + * Constructor from the protected URI location triggering the challenge, + * and an entire server-provided 'WWW-Authenticate:' string. + * + * @param location the protected URI location triggering the challenge + * @param challenge an entire server-provided 'WWW-Authenticate:' string + */ + public ChallengeRequest(String location, String challenge) { + if ( location == null ) { + throw new NullPointerException("location"); + } + if ( challenge == null ) { + return; + } + + if (challenge.startsWith(APPLICATION_PREFIX)) { + challenge = challenge.substring(APPLICATION_PREFIX.length()); + } + + this.location = location; + this.authenticationParameters = null; + + int space = challenge.indexOf(' '); + if ( space == -1 ) { + this.authenticationScheme = challenge; + } else { + this.authenticationScheme = challenge.substring(0, space); + if ( challenge.length() > (space+1)) { + this.authenticationParameters = challenge.substring(space+1); + } + } + } + + /** + * Return the protected URI the access of which triggered this challenge as a {@code String}. + *

+ * For some authentication schemes, the production of a response to the challenge may require + * access to the location of the URI triggering the challenge. + * + * @return the protected URI the access of which triggered this challenge as a {@code String} + */ + public String getLocation() { + return location; + } + + /** + * Return the authentication scheme with which the server is challenging. + * + * @return the authentication scheme with which the server is challenging. + */ + public String getAuthenticationScheme() { + return authenticationScheme; + } + + /** + * Return the string after the space separator, not including the authentication scheme nor the space itself, + * or {@code null} if no such string exists. + * + * @return the string after the space separator, not including the authentication scheme nor the space itself, + * or {@code null} if no such string exists. + */ + public String getAuthenticationParameters() { + return authenticationParameters; + } + + + @Override + public String toString() { + return String.format("%s: %s: %s %s", super.toString(), location, authenticationScheme, authenticationParameters); + } +} diff --git a/android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java b/android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java new file mode 100755 index 0000000..354a272 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/ChallengeResponse.java @@ -0,0 +1,95 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + +import java.util.Arrays; + +/** + * A challenge response contains a character array representing the response to the server, + * and a reference to the next challenge handler to handle any further challenges for the request. + * + */ +public class ChallengeResponse { + + private char[] credentials; + private ChallengeHandler nextChallengeHandler; + + /** + * Constructor from a set of credentials to send to the server in an 'Authorization:' header + * and the next challenge handler responsible for handling any further challenges for the request. + * + * @param credentials a set of credentials to send to the server in an 'Authorization:' header + * @param nextChallengeHandler the next challenge handler responsible for handling any further challenges for the request. + */ + public ChallengeResponse(char[] credentials, ChallengeHandler nextChallengeHandler) { + this.credentials = credentials; + this.nextChallengeHandler = nextChallengeHandler; + } + + /** + * Return the next challenge handler responsible for handling any further challenges for the request. + * + * @return the next challenge handler responsible for handling any further challenges for the request. + */ + public ChallengeHandler getNextChallengeHandler() { + return nextChallengeHandler; + } + + /** + * Return a set of credentials to send to the server in an 'Authorization:' header. + * + * @return a set of credentials to send to the server in an 'Authorization:' header. + */ + public char[] getCredentials() { + return credentials; + } + + /** + * Establish the credentials for this response. + * + * @param credentials the credentials to be used for this challenge response. + */ + public void setCredentials(char[] credentials) { + this.credentials = credentials; + } + + /** + * Establish the next challenge handler responsible for handling any further challenges for the request. + * + * @param nextChallengeHandler the next challenge handler responsible for handling any further challenges for the request. + */ + public void setNextChallengeHandler(ChallengeHandler nextChallengeHandler) { + this.nextChallengeHandler = nextChallengeHandler; + } + + /** + * Clear the credentials of this response. + *

+ * Calling this method once the credentials have been communicated to the network layer + * protects credentials in memory. + */ + public void clearCredentials() { + if (getCredentials() != null) { + Arrays.fill(getCredentials(), (char) 0); + } + } +} diff --git a/android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java new file mode 100755 index 0000000..c319343 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/DispatchChallengeHandler.java @@ -0,0 +1,116 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + +/** + * A DispatchChallengeHandler is responsible for dispatching challenge requests + * to appropriate challenge handlers when challenges + * arrive from specific URI locations in challenge responses. + *

+ * This allows clients to use specific challenge handlers to handle specific + * types of challenges at different URI locations. + *

+ */ +public abstract class DispatchChallengeHandler extends ChallengeHandler { + + /** + * Creates a new instance of {@link DispatchChallengeHandler} using the + * {@link ServiceLoader} API with the implementation specified under + * META-INF/services. + * + * @return DispatchChallengeHandler + */ + public static DispatchChallengeHandler create() { + return create(DispatchChallengeHandler.class); + } + + /** + * Creates a new instance of {@link DispatchChallengeHandler} with the + * specified {@link ClassLoader} using the {@link ServiceLoader} API with + * the implementation specified under META-INF/services. + * + * @param classLoader ClassLoader to be used to instantiate + * @return DispatchChallengeHandler + */ + public static DispatchChallengeHandler create(ClassLoader classLoader) { + return create(DispatchChallengeHandler.class, classLoader); + } + + @Override + public abstract boolean canHandle(ChallengeRequest challengeRequest) ; + + @Override + public abstract ChallengeResponse handle(ChallengeRequest challengeRequest) ; + + /** + * Register a challenge handler to respond to challenges at one or more locations. + *

+ * When a challenge response is received for a protected URI, the {@code locationDescription} + * matches against elements of the protected URI; if a match is found, one + * consults the challenge handler(s) registered at that {@code locationDescription} to find + * a challenge handler suitable to respond to the challenge. + *

+ * A {@code locationDescription} comprises a username, password, host, port and paths, + * any of which can be wild-carded with the "*" character to match any number of request URIs. + * If no port is explicitly mentioned in a {@code locationDescription}, a default port will be inferred + * based on the scheme mentioned in the location description, according to the following table: + * + * + * + * + * + * + *
schemedefault portSample locationDescription
http80foo.example.com or http://foo.example.com
ws80foo.example.com or ws://foo.example.com
https443https://foo.example.com
wss443wss://foo.example.com
+ *

+ * The protocol scheme (e.g. http or ws) if present in {@code locationDescription} will not be used to + * match {@code locationDescription} with the protected URI, because authentication challenges are + * implemented on top of one of the HTTP/s protocols always, whether one is initiating web socket + * connections or regular HTTP connections. That is to say for example, the locationDescription {@code "foo.example.com"} + * matches both URIs {@code http://foo.example.com} and {@code ws://foo.example.com}. + *

+ * Some examples of {@code locationDescription} values with wildcards are: + *

    + *
  1. {@code *}/ -- matches all requests to any host on port 80 (default port), with no user info or path specified.
  2. + *
  3. {@code *.hostname.com:8000} -- matches all requests to port 8000 on any sub-domain of {@code hostname.com}, + * but not {@code hostname.com} itself.
  4. + *
  5. {@code server.hostname.com:*}/{@code *} -- matches all requests to a particular server on any port on any path but not the empty path.
  6. + *
+ * @param locationDescription the (possibly wild-carded) location(s) at which to register a handler. + * @param challengeHandler the challenge handler to register at the location(s). + * + * @return a reference to this challenge handler for chained calls + */ + public abstract DispatchChallengeHandler register(String locationDescription, ChallengeHandler challengeHandler) ; + + /** + * If the provided challengeHandler is registered at the provided location, clear that + * association such that any future challenge requests matching the location will never + * be handled by the provided challenge handler. + *

+ * If no such location or challengeHandler registration exists, this method silently succeeds. + * @param locationDescription the exact location description at which the challenge handler was originally registered + * @param challengeHandler the challenge handler to de-register. + * + * @return a reference to this object for chained call support + */ + public abstract DispatchChallengeHandler unregister(String locationDescription, ChallengeHandler challengeHandler) ; +} diff --git a/android/src/main/java/org/kaazing/net/auth/LoginHandler.java b/android/src/main/java/org/kaazing/net/auth/LoginHandler.java new file mode 100755 index 0000000..c5aae4c --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/LoginHandler.java @@ -0,0 +1,63 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + +import java.net.PasswordAuthentication; + +/** + * A login handler is responsible for obtaining credentials from an arbitrary + * source. + *

+ * Login Handlers can be associated with one or more {@link ChallengeHandler} + * objects, to ensure that when a Challenge Handler requires credentials for a {@link ChallengeResponse}, + * the work is delegated to a {@link LoginHandler}. + *

+ * At client configuration time, a {@link LoginHandler} can be associated with a {@link ChallengeHandler} as follows: + *

+ * {@code
+ *
+ * BasicChallengeHandler basicChallengeHandler = ChallengeHandlerLoader.load(BasicChallengeHandler.class);
+ * LoginHandler loginHandler = new LoginHandler() {
+ *    public PasswordAuthentication getCredentials() {
+ *        // Obtain credentials in an application-specific manner
+ *    }
+ * }
+ * basicChallengeHandler.setLoginHandler(loginHandler);
+ *    
+ * }
+ * 
+ */ +public abstract class LoginHandler { + + /** + * Default constructor. + */ + protected LoginHandler() { + } + + /** + * Gets the password authentication credentials from an arbitrary source. + * @return the password authentication obtained. + */ + public abstract PasswordAuthentication getCredentials(); + +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java new file mode 100755 index 0000000..a7ea216 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/NegotiableChallengeHandler.java @@ -0,0 +1,98 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + + +import java.util.Collection; + +/** + * A NegotiableChallengeHandler can be used to directly respond to + * "Negotiate" challenges, and in addition, can be used indirectly in conjunction + * with a {@link NegotiateChallengeHandler} + * to assist in the construction of a challenge response using object identifiers. + * + * @see RFC 4178 Section 4.2.1 for details + * about how the supported object identifiers contribute towards the initial context token in the challenge response. + * + *

+ * + */ +public abstract class NegotiableChallengeHandler extends ChallengeHandler { + + /** + * Creates a new instance of {@link NegotiableChallengeHandler} using the + * {@link ServiceLoader} API with the implementation specified under + * META-INF/services. + * + * @return NegotiableChallengeHandler + */ + public static NegotiableChallengeHandler create() { + return create(NegotiableChallengeHandler.class); + } + + /** + * Creates a new instance of {@link NegotiableChallengeHandler} with the + * specified {@link ClassLoader} using the {@link ServiceLoader} API with + * the implementation specified under META-INF/services. + * + * @param classLoader ClassLoader to be used to instantiate + * @return NegotiableChallengeHandler + */ + public static NegotiableChallengeHandler create(ClassLoader classLoader) { + return create(NegotiableChallengeHandler.class, classLoader); + } + + /** + * Default constructor. + */ + protected NegotiableChallengeHandler() { + } + + /** + * Return a collection of string representations of object identifiers + * supported by this challenge handler implementation, in dot-separated notation. + * For example, {@literal 1.3.5.1.5.2}. + * + * @return a collection of string representations of object identifiers + * supported by this challenge handler implementation. + */ + public abstract Collection getSupportedOids(); + + /** + * Provide a general login handler to be used in association with this challenge handler. + * The login handler is used to assist in obtaining credentials to respond to any + * challenge requests when this challenge handler handles the request. + * + * @param loginHandler a login handler for credentials. + * + * @return this challenge handler object, to support chained calls + */ + public abstract NegotiableChallengeHandler setLoginHandler(LoginHandler loginHandler); + + /** + * Get the general login handler associated with this challenge handler. + * A login handler is used to assist in obtaining credentials to respond to challenge requests. + * + * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet. + */ + public abstract LoginHandler getLoginHandler() ; +} diff --git a/android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java b/android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java new file mode 100755 index 0000000..cdbe45e --- /dev/null +++ b/android/src/main/java/org/kaazing/net/auth/NegotiateChallengeHandler.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.auth; + +/** + * A Negotiate Challenge Handler handles initial empty "Negotiate" challenges from the + * server. It uses other "candidate" challenger handlers to assemble an initial context token + * to send to the server, and is responsible for creating a challenge response that can delegate + * to the winning candidate. + *

+ * This NegotiateChallengeHandler can be loaded and instantiated using + * {@link #create()}, and registered at a location using + * {@link DispatchChallengeHandler#register(String, ChallengeHandler)}. + *

+ * In addition, one can register more specific {@link NegotiableChallengeHandler} objects with + * this initial {@link NegotiateChallengeHandler} to handle initial Negotiate challenges and subsequent challenges associated + * with specific Negotiation mechanism types / object identifiers. + *

+ * The following example establishes a Negotiation strategy at a specific URL location. + * We show the use of a {@link DispatchChallengeHandler} to register a {@link NegotiateChallengeHandler} at + * a specific location. The {@link NegotiateChallengeHandler} has a {@link NegotiableChallengeHandler} + * instance registered as one of the potential negotiable alternative challenge handlers. + *

+ * {@code
+ * LoginHandler someServerLoginHandler = ...
+ * NegotiateChallengeHandler  nch = NegotiateChallengeHandler.create();
+ * NegotiableChallengeHandler nblch = NegotiableChallengeHandler.create();
+ * DispatchChallengeHandler   dch = DispatchChallengeHandler.create();
+ * WebSocketFactory       wsFactory = WebSocketFactory.createWebSocketFactory();
+ * wsFactory.setDefaultChallengeHandler(dch.register("ws://some.server.com",
+ *                                     nch.register(nblch).setLoginHandler(someServerLoginHandler)
+ *                                     );
+ *             // could register more alternatives to negotiate amongst here.
+ *         )
+ * );
+ * }
+ * 
+ * + * @see RFC 2616 - HTTP 1.1 + * @see RFC 2617 - HTTP Authentication + */ +public abstract class NegotiateChallengeHandler extends ChallengeHandler { + + /** + * Creates a new instance of {@link NegotiateChallengeHandler} using the + * {@link ServiceLoader} API with the implementation specified under + * META-INF/services. + * + * @return NegotiateChallengeHandler + */ + public static NegotiateChallengeHandler create() { + return create(NegotiateChallengeHandler.class); + } + + /** + * Creates a new instance of {@link NegotiateChallengeHandler} with the + * specified {@link ClassLoader} using the {@link ServiceLoader} API with + * the implementation specified under META-INF/services. + * + * @param classLoader ClassLoader to be used to instantiate + * @return NegotiateChallengeHandler + */ + public static NegotiateChallengeHandler create(ClassLoader classLoader) { + return create(NegotiateChallengeHandler.class, classLoader); + } + + /** + * Register a candidate negotiable challenge handler that will be used to respond + * to an initial "Negotiate" server challenge and can then potentially be + * a winning candidate in the race to handle the subsequent server challenge. + * + * @param handler the mechanism-type-specific challenge handler. + * + * @return a reference to this handler, to support chained calls + */ + public abstract NegotiateChallengeHandler register(NegotiableChallengeHandler handler); +} diff --git a/android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java b/android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java new file mode 100755 index 0000000..e8b6df8 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/http/HttpRedirectPolicy.java @@ -0,0 +1,317 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.http; + +import java.net.URI; +import java.util.Comparator; + +/** + * Options for following HTTP redirect requests with response code 3xx. + */ +public enum HttpRedirectPolicy implements Comparator { + /** + * Do not follow HTTP redirects. + */ + NEVER() { + @Override + public int compare(URI current, URI redirect) { + if ((current == null) || (redirect == null)) { + String s = "Null URI passed in to compare()"; + throw new IllegalArgumentException(s); + } + + // URIs don't matter as this option indicates never to follow + // redirects. + return -1; + } + + @Override + public String toString() { + return "HttpRedirectOption.NEVER"; + } + }, + + /** + * Follow HTTP redirect requests always regardless of the origin, domain, etc. + */ + ALWAYS() { + @Override + public int compare(URI current, URI redirect) { + if ((current == null) || (redirect == null)) { + String s = "Null URI passed in to compare()"; + throw new IllegalArgumentException(s); + } + + // URIs don't matter as this option indicates always to follow + // redirects. + return 0; + } + + @Override + public String toString() { + return "HttpRedirectOption.ALWAYS"; + } + }, + + /** + * Follow HTTP redirect only if the redirected request is for the same + * origin. This implies that both the scheme/protocol and the + * authority should match between the current and the redirect URIs. + * Note that authority includes the hostname and the port. + */ + SAME_ORIGIN() { + @Override + public int compare(URI current, URI redirect) { + if ((current == null) || (redirect == null)) { + String s = "Null URI passed in to compare()"; + throw new IllegalArgumentException(s); + } + + // Two URIs have same origin if the protocol and authority + // are the same. + if (current.getScheme().equalsIgnoreCase(redirect.getScheme()) && + current.getAuthority().equalsIgnoreCase(redirect.getAuthority())) { + return 0; + } + + return -1; + } + + @Override + public String toString() { + return "HttpRedirectOption.SAME_ORIGIN"; + } + }, + + /** + * Follow HTTP redirect only if the redirected request is for the same + * domain. This implies that both the scheme/protocol and the + * hostname should match between the current and the redirect URIs. + *

+ * URIs that satisfy HttpRedirectPolicy.SAME_ORIGIN policy will implicitly + * satisfy HttpRedirectPolicy.SAME_DOMAIN policy. + *

+ * URIs with identical domains would be ws://production.example.com:8001 and + * ws://production.example.com:8002. + */ + SAME_DOMAIN() { + @Override + public int compare(URI current, URI redirect) { + if (HttpRedirectPolicy.SAME_ORIGIN.compare(current, redirect) == 0) { + // If the URIs have same origin, then they implicitly have same + // domain. + return 0; + } + + if ((current == null) || (redirect == null)) { + String s = "Null URI passed in to compare()"; + throw new IllegalArgumentException(s); + } + + // We should allow redirecting to a more secure scheme from a less + // secure scheme. For example, we should allow redirecting from + // ws -> wss. + String currScheme = current.getScheme(); + String newScheme = redirect.getScheme(); + if (newScheme.equalsIgnoreCase(currScheme) || + newScheme.contains(currScheme)) { + // Check if the host names are the same between the two URIs. + String currHost = current.getHost(); + String newHost = redirect.getHost(); + + if (currHost.equalsIgnoreCase(newHost)) { + return 0; + } + } + + return -1; + } + + @Override + public String toString() { + return "HttpRedirectOption.SAME_DOMAIN"; + } + }, + + /** + * Follow HTTP redirect only if the redirected request is for a peer-domain. + * This implies that both the scheme/protocol and the domain should + * match between the current and the redirect URIs. + *

+ * URIs that satisfy HttpRedirectPolicy.SAME_DOMAIN policy will implicitly + * satisfy HttpRedirectPolicy.PEER_DOMAIN policy. + *

+ * To determine if the two URIs have peer-domains, we do the following: + *

    + *
  • compute base-domain by removing the token before the first '.' in + * the hostname of the original URI and check if the hostname of the + * redirected URI ends with the computed base-domain + *
  • compute base-domain by removing the token before the first '.' in + * the hostname of the redirected URI and check if the hostname of the + * original URI ends with the computed base-domain + *
+ *

+ * If both the conditions are satisfied, then we conclude that the URIs are + * for peer-domains. However, if the host in the URI has no '.'(for eg., + * ws://localhost:8000), then we just use the entire hostname as the + * computed base-domain. + *

+ * If you are using this policy, it is recommended that the number of tokens + * in the hostname be atleast 2 + number_of_tokens(top-level-domain). For + * example, if the top-level-domain(TLD) is "com", then the URIs should have + * atleast 3 tokens in the hostname. So, ws://marketing.example.com:8001 and + * ws://sales.example.com:8002 are examples of URIs with peer-domains. Similarly, + * if the TLD is "co.uk", then the URIs should have atleast 4 tokens in the + * hostname. So, ws://marketing.example.co.uk:8001 and + * ws://sales.example.co.uk:8002 are examples of URIs with peer-domains. + */ + PEER_DOMAIN() { + @Override + public int compare(URI current, URI redirect) { + if (HttpRedirectPolicy.SAME_DOMAIN.compare(current, redirect) == 0) { + // If the domains are the same, then they are peers. + return 0; + } + + if ((current == null) || (redirect == null)) { + String s = "Null URI passed in to compare()"; + throw new IllegalArgumentException(s); + } + + // We should allow redirecting to a more secure scheme from a less + // secure scheme. For example, we should allow redirecting from + // ws -> wss. + String currScheme = current.getScheme(); + String newScheme = redirect.getScheme(); + if (newScheme.equalsIgnoreCase(currScheme) || + newScheme.contains(currScheme)) { + String currHost = current.getHost(); + String redirectHost = redirect.getHost(); + String currBaseDomain = getBaseDomain(currHost); + String redirectBaseDomain = getBaseDomain(redirectHost); + + if (currHost.endsWith(redirectBaseDomain) && + redirectHost.endsWith(currBaseDomain)) { + return 0; + } + } + + return -1; + } + + @Override + public String toString() { + return "HttpRedirectOption.PEER_DOMAIN"; + } + + // Get the base domain for a given hostname. For example, jms.kaazing.com + // will return kaazing.com. + private String getBaseDomain(String hostname) { + String[] tokens = hostname.split("\\."); + + if (tokens.length <= 2) { + return hostname; + } + + String baseDomain = ""; + for (int i = 1; i < tokens.length; i++) { + baseDomain += "." + tokens[i]; + } + + return baseDomain; + } + }, + + /** + * Follow HTTP redirect only if the redirected request is for child-domain + * or sub-domain of the original request. + *

+ * URIs that satisfy HttpRedirectPolicy.SAME_DOMAIN policy will implicitly + * satisfy HttpRedirectPolicy.SUB_DOMAIN policy. + *

+ * To determine if the domain of the redirected URI is sub-domain/child-domain + * of the domain of the original URI, we check if the hostname of the + * redirected URI ends with the hostname of the original URI. + *

+ * Domain of the redirected URI ws://benefits.hr.example.com:8002 is a + * sub-domain/child-domain of the domain of the original URI + * ws://hr.example.com:8001. Note that domain in ws://example.com:9001 is a + * sub-domain of the domain in ws://example.com:9001. + */ + SUB_DOMAIN() { + @Override + public int compare(URI current, URI redirect) { + if (HttpRedirectPolicy.SAME_DOMAIN.compare(current, redirect) == 0) { + // If the domains are the same, then one can be a sub-domain + // of the other. + return 0; + } + + if ((current == null) || (redirect == null)) { + String s = "Null URI passed in to compare()"; + throw new IllegalArgumentException(s); + } + + // We should allow redirecting to a more secure scheme from a less + // secure scheme. For example, we should allow redirecting from + // ws -> wss. + String currScheme = current.getScheme(); + String newScheme = redirect.getScheme(); + if (newScheme.equalsIgnoreCase(currScheme) || + newScheme.contains(currScheme)) { + // If the current host is gateway.example.com, and the new + // is child.gateway.example.com, then allow redirect. + String currHost = current.getHost(); + String newHost = redirect.getHost(); + + if (newHost.length() < currHost.length()) { + return -1; + } + + if (newHost.endsWith("." + currHost)) { + return 0; + } + } + + return -1; + } + + @Override + public String toString() { + return "HttpRedirectOption.SUB_DOMAIN"; + } + }; + + /** + * Returns 0, if the aspects of current and the redirected URIs match as per + * the option. Otherwise, -1 is returned. + * + * @param current URI of the current request + * @param redirect URI of the redirected request + * @return 0, for a successful match; otherwise -1 + */ + @Override + public abstract int compare(URI current, URI redirect); + + @Override + public abstract String toString(); +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java b/android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java new file mode 100755 index 0000000..765920f --- /dev/null +++ b/android/src/main/java/org/kaazing/net/impl/auth/BasicChallengeResponseFactory.java @@ -0,0 +1,40 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.impl.auth; + +import org.kaazing.gateway.client.util.Base64Util; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.auth.ChallengeResponse; + +import java.net.PasswordAuthentication; +import java.util.Arrays; + +public class BasicChallengeResponseFactory { + + public static ChallengeResponse create(PasswordAuthentication creds, ChallengeHandler nextChallengeHandler) { + String unencoded = String.format("%s:%s", creds.getUserName(), new String(creds.getPassword())); + String response = String.format("Basic %s", Base64Util.encode(WrappedByteBuffer.wrap(unencoded.getBytes()))); + Arrays.fill(creds.getPassword(), (char) 0); + return new ChallengeResponse(response.toCharArray(), nextChallengeHandler); + } +} diff --git a/android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java b/android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java new file mode 100755 index 0000000..3dfd062 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/impl/auth/DefaultBasicChallengeHandler.java @@ -0,0 +1,122 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.impl.auth; + + +import java.net.PasswordAuthentication; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.util.auth.LoginHandlerProvider; +import org.kaazing.net.auth.BasicChallengeHandler; +import org.kaazing.net.auth.ChallengeRequest; +import org.kaazing.net.auth.ChallengeResponse; +import org.kaazing.net.auth.LoginHandler; + +/** + * Challenge handler for Basic authentication. See RFC 2617. + */ +public class DefaultBasicChallengeHandler extends BasicChallengeHandler implements LoginHandlerProvider { + +// ------------------------------ FIELDS ------------------------------ + + private static final String CLASS_NAME = DefaultBasicChallengeHandler.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private Map loginHandlersByRealm = new ConcurrentHashMap(); + + @Override + public void setRealmLoginHandler(String realm, LoginHandler loginHandler) { + if ( realm == null) { + throw new NullPointerException("realm"); + } + if ( loginHandler == null ) { + throw new NullPointerException("loginHandler"); + } + + loginHandlersByRealm.put(realm, loginHandler); + } + + /** + * If specified, this login handler is responsible for assisting in the + * production of challenge responses. + */ + private LoginHandler loginHandler; + + /** + * Provide a login handler to be used in association with this challenge handler. + * The login handler is used to assist in obtaining credentials to respond to challenge requests. + * + * @param loginHandler a login handler for credentials. + */ + public BasicChallengeHandler setLoginHandler(LoginHandler loginHandler) { + this.loginHandler = loginHandler; + return this; + } + + /** + * Get the login handler associated with this challenge handler. + * A login handler is used to assist in obtaining credentials to respond to challenge requests. + * + * @return a login handler to assist in providing credentials, or {@code null} if none has been established yet. + */ + public LoginHandler getLoginHandler() { + return loginHandler; + } + + @Override + public boolean canHandle(ChallengeRequest challengeRequest) { + return challengeRequest != null && + "Basic".equals(challengeRequest.getAuthenticationScheme()); + } + + @Override + public ChallengeResponse handle(ChallengeRequest challengeRequest) { + + LOG.entering(CLASS_NAME, "handle", new String[]{challengeRequest.getLocation(), + challengeRequest.getAuthenticationParameters()}); + + if (challengeRequest.getLocation() != null) { + + + + // Start by using this generic Basic handler + LoginHandler loginHandler = getLoginHandler(); + + // Try to delegate to a realm-specific login handler if we can + String realm = RealmUtils.getRealm(challengeRequest); + if ( realm != null && loginHandlersByRealm.get(realm) != null) { + loginHandler = loginHandlersByRealm.get(realm); + } + LOG.finest("BasicChallengeHandler.getResponse: login handler = " + loginHandler); + if (loginHandler != null) { + PasswordAuthentication creds = loginHandler.getCredentials(); + if (creds != null && creds.getUserName() != null && creds.getPassword() != null) { + return BasicChallengeResponseFactory.create(creds, this); + } + } + } + return null; + } + +} diff --git a/android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java b/android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java new file mode 100755 index 0000000..fff10e2 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/impl/auth/DefaultDispatchChallengeHandler.java @@ -0,0 +1,778 @@ +/** + * Copyright 2007-2015, Kaazing Corporation. All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.kaazing.net.impl.auth; + +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.auth.ChallengeRequest; +import org.kaazing.net.auth.ChallengeResponse; +import org.kaazing.net.auth.DispatchChallengeHandler; + +import java.net.URI; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +/** + * The DefaultDispatchChallengeHandler is responsible for defining and using appropriate challenge handlers when challenges + * arrive from specific URI locations. This allows clients to use specific challenge handlers to handle specific + * types of challenges at different URI locations. + *

+ */ +public class DefaultDispatchChallengeHandler extends DispatchChallengeHandler { +// ------------------------------ FIELDS ------------------------------ + + static final String SCHEME_URI = "^(.*)://(.*)"; + static final Pattern SCHEME_URI_PATTERN = Pattern.compile(SCHEME_URI); + + enum UriElement { + HOST, + USERINFO, + PORT, + PATH + } + private Node rootNode; + + public void clear() { + rootNode = new Node(); + } + + @Override + public boolean canHandle(ChallengeRequest challengeRequest) { + return lookup(challengeRequest) != null; + } + + @Override + public ChallengeResponse handle(ChallengeRequest challengeRequest) { + ChallengeHandler challengeHandler = lookup(challengeRequest); + if (challengeHandler == null) { + return null; + } + return challengeHandler.handle(challengeRequest); + } + + + + +// --------------------------- CONSTRUCTORS --------------------------- + + public DefaultDispatchChallengeHandler() { + rootNode = new Node(); + } + + @Override + public DispatchChallengeHandler register(String locationDescription, ChallengeHandler challengeHandler) { + if (locationDescription == null || locationDescription.length() == 0) { + throw new IllegalArgumentException("Must specify a location to handle challenges upon."); + } + + if (challengeHandler == null) { + throw new IllegalArgumentException("Must specify a handler to handle challenges."); + } + + addChallengeHandlerAtLocation(locationDescription, challengeHandler); + return this; + } + + @Override + public DispatchChallengeHandler unregister(String locationDescription, ChallengeHandler challengeHandler) { + if (locationDescription == null || locationDescription.length() == 0) { + throw new IllegalArgumentException("Must specify a location to un-register challenge handlers upon."); + } + + if (challengeHandler == null) { + throw new IllegalArgumentException("Must specify a handler to un-register."); + } + + delChallengeHandlerAtLocation(locationDescription, challengeHandler); + + return this; + } + + private void delChallengeHandlerAtLocation(String locationDescription, ChallengeHandler challengeHandler) { + List> tokens = tokenize(locationDescription); + Node cursor = rootNode; + for (Token token : tokens) { + if (!cursor.hasChild(token.getName(), token.getKind())) { + return; // silently remove nothing + } else { + cursor = cursor.getChild(token.getName()); + } + } + cursor.removeValue(challengeHandler); + } + + private void addChallengeHandlerAtLocation(String locationDescription, ChallengeHandler challengeHandler) { + List> tokens = tokenize(locationDescription); + Node cursor = rootNode; + + for (Token token : tokens) { + if (!cursor.hasChild(token.getName(), token.getKind())) { + cursor = cursor.addChild(token.getName(), token.getKind()); + } else { + cursor = cursor.getChild(token.getName()); + } + } + cursor.appendValues(challengeHandler); + } + + + +// -------------------------- OTHER METHODS -------------------------- + /** + * Locate all challenge handlers to serve the given location. + * + * @param location a location + * @return a collection of {@link ChallengeHandler}s if found registered at a matching location or an empty list if none are found. + */ + public List lookup(String location) { + List result = Collections.emptyList(); + if (location != null) { + Node resultNode = findBestMatchingNode(location); + if (resultNode != null) { + return resultNode.getValues(); + } + } + return result; + } + + /** + * Locate a challenge handler factory to serve the given location and challenge type. + * + * + * @param challengeRequest A challenge string from the server. + * @return a challenge handler registered to handle the challenge at the location, + * or null if none could be found. + */ + ChallengeHandler lookup(ChallengeRequest challengeRequest) { + ChallengeHandler result = null; + String location = challengeRequest.getLocation(); + if (location != null) { + Node resultNode = findBestMatchingNode(location); + + // + // If we found an exact or wildcard match, try to find a handler + // for the requested challenge. + // + if (resultNode != null) { + List handlers = resultNode.getValues(); + if (handlers != null) { + for (ChallengeHandler challengeHandler : handlers) { + if (challengeHandler.canHandle(challengeRequest)) { + result = challengeHandler; + break; + } + } + } + } + } + return result; + } + + /** + * Return the Node corresponding to ("matching") a location, or null if none can be found. + * + * @param location the location at which to find a Node + * @return the Node corresponding to ("matching") a location, or null if none can be found. + */ + private Node findBestMatchingNode(String location) { + List> tokens = tokenize(location); + int tokenIdx = 0; + + return rootNode.findBestMatchingNode(tokens, tokenIdx); + } + + /** + * Tokenize a given string assuming it is like a URL. + * + * @param s the string to be parsed as a wildcard-able URI + * @return the array of tokens of URI parts. + * @throws IllegalArgumentException when the string cannot be parsed as a wildcard-able URI + */ + List> tokenize(String s) throws IllegalArgumentException { + if (s == null || s.length() == 0) { + return new ArrayList>(); + } + + // + // Make sure if a scheme is not specified, we default one before we parse as a URI. + // + if ( !SCHEME_URI_PATTERN.matcher(s).matches()) { + s = ("http://")+s; + } + + // + // Parse as a URI + // + URI uri = URI.create(s); + + // + // Detect what the scheme is, if any. + // + List> result = new ArrayList>(10); + String scheme = "http"; + if (uri.getScheme() != null) { + scheme = uri.getScheme(); + } + + + // + // A wildcard-ed hostname is parsed as an authority. + // + String host = uri.getHost(); + String parsedPortFromAuthority = null; + String parsedUserInfoFromAuthority = null; + String userFromAuthority = null; + String passwordFromAuthority = null; + if (host == null) { + String authority = uri.getAuthority(); + if (authority != null) { + host = authority; + int asteriskIdx = host.indexOf("@"); + if ( asteriskIdx >= 0) { + parsedUserInfoFromAuthority = host.substring(0, asteriskIdx); + host = host.substring(asteriskIdx+1); + int colonIdx = parsedUserInfoFromAuthority.indexOf(":"); + if ( colonIdx >= 0) { + userFromAuthority = parsedUserInfoFromAuthority.substring(0, colonIdx); + passwordFromAuthority = parsedUserInfoFromAuthority.substring(colonIdx+1); + } + } + int colonIdx = host.indexOf(":"); + if ( colonIdx >=0 ) { + parsedPortFromAuthority = host.substring(colonIdx + 1); + host = host.substring(0, colonIdx); + } + } else { + throw new IllegalArgumentException("Hostname is required."); + } + } + + // + // Split the host and reverse it for the tokenization. + // + List hostParts = Arrays.asList(host.split("\\.")); + Collections.reverse(hostParts); + for (String hostPart: hostParts) { + result.add(new Token(hostPart, UriElement.HOST)); + } + + if (parsedPortFromAuthority != null) { + result.add(new Token(parsedPortFromAuthority, UriElement.PORT)); + } else if (uri.getPort() > 0) { + result.add(new Token(String.valueOf(uri.getPort()), UriElement.PORT)); + } else if (getDefaultPort(scheme) > 0) { + result.add(new Token(String.valueOf(getDefaultPort(scheme)), UriElement.PORT)); + } + + + if ( parsedUserInfoFromAuthority != null ) { + if ( userFromAuthority != null) { + result.add(new Token(userFromAuthority, UriElement.USERINFO)); + } + if ( passwordFromAuthority != null ) { + result.add(new Token(passwordFromAuthority, UriElement.USERINFO)); + } + if ( userFromAuthority == null && passwordFromAuthority == null) { + result.add(new Token(parsedUserInfoFromAuthority, UriElement.USERINFO)); + } + } else if (uri.getUserInfo() != null) { + String userInfo = uri.getUserInfo(); + int colonIdx = userInfo.indexOf(":"); + if ( colonIdx >= 0) { + result.add(new Token(userInfo.substring(0, colonIdx), UriElement.USERINFO)); + result.add(new Token(userInfo.substring(colonIdx+1), UriElement.USERINFO)); + } else { + result.add(new Token(uri.getUserInfo(), UriElement.USERINFO)); + } + } + + if (isNotBlank(uri.getPath())) { + String path = uri.getPath(); + if (path.startsWith("/")) { + path = path.substring(1); + } + if (isNotBlank(path)) { + for (String p: path.split("/")) { + result.add(new Token(p, UriElement.PATH)); + } + } + } + return result; + } + + int getDefaultPort(String scheme) { + if ( defaultPortsByScheme.containsKey(scheme.toLowerCase())) { + return defaultPortsByScheme.get(scheme); + } else { + return -1; + } + } + + static Map defaultPortsByScheme = new HashMap(); + static { + defaultPortsByScheme.put("http", 80); + defaultPortsByScheme.put("ws", 80); + defaultPortsByScheme.put("wss", 443); + defaultPortsByScheme.put("https", 443); + } + + + private boolean isNotBlank(String s) { + return s != null && s.length() > 0; + } + + /** + * A Node instance has a kind, holds a list of {@link #values} (parameterized type instances), + * and a sub-tree of nodes called {@link #children}. It is used as a model for + * holding typed {@link org.kaazing.net.auth.ChallengeHandler} instances at "locations". + *

+ * {@link org.kaazing.net.auth.impl.DefaultDispatchChallengeHandler.Node} instances are mutable. Nodes have {@link #name}s. One can add children + * with distinct names, add {@link #values} and recall them. + *

+ * {@link org.kaazing.net.auth.impl.DefaultDispatchChallengeHandler.Node} instances are considered to be "wildcard" nodes when their name is equal + * to {@link #getWildcardChar()}. Nodes are considered to have a wildcard defined if they or + * any of their children are named {@link #getWildcardChar()}. Wildcard nodes are treated + * as matching one or multiple elements during {@link org.kaazing.net.auth.impl.DefaultDispatchChallengeHandler.Node searches}. + *

+ * The concept of a Node has the following restrictions in the following cases. + * An {@link IllegalArgumentException} will result in each case. + *

    + *
  1. One is not permitted to add values to the root node. Use a wildcard node instead.
  2. + *
  3. One is not permitted to create Node instances with null or empty names.
  4. + *
+ * + * @private + */ + static class Node> { + + /** + * The name of this Node instance. + * Must not be null or empty. + */ + private String name; + + /** + * The parameterized type instances. + * Optimized for fewer values per node. + */ + private List values = new ArrayList(3); + + /** + * An up-link to this Node instance's parent. + */ + private Node parent; + + /** + * An enumerated value representing the "kind" of this node. + */ + private E kind; + + /** + * The down-links to children sub-nodes of this node. + * Each link is accessed through the child Node's name. + * This means that child names must be unique. + */ + private Map> children = new LinkedHashMap>(); + + /** + * A method to access the wildcard character. + * Making this a method rather than a constant will allow + * this value to change without recompilation of client classes. + * @return the character assumed to be the wildcard character for this tree. + */ + public static String getWildcardChar() { + return "*"; + } + + /** + * Create a new root node, with a null name and parent. + */ + Node() { + this.name=null; + this.parent=null; + this.kind = null; + } + + + /** + * Create a new node with the provided name and parent node. + * @param name the name of the node instance to create. + * @param parent the parent of the new node to establish the new node's place in the tree. + * @param kind the kind of the node instance to create. + */ + private Node(String name, Node parent, E kind) { + this.name = name; + this.parent = parent; + this.kind = kind; + } + + /** + * Add a new node with the given name of the given kind to this node. + *

+ * If an existing child has that name, replace the existing named sub-tree with + * a new single node with the provided name. + * + * + * @param name the name of the new node + * @param kind the kind of the new node + * @return the freshly added Node, for chained calls if needed. + * @throws IllegalArgumentException if the name is null or empty + */ + public Node addChild(String name, E kind) { + if ( name == null || name.length() == 0) { + throw new IllegalArgumentException("A node may not have a null name."); + } + + Node result = new Node(name, this, kind); + children.put(name, result); + return result; + } + + /** + * Return whether this node has a child with the name and kind provided. + * + * + * @param name the name of the child node sought + * @param kind the kind of the child node sought + * @return true iff this instance has a child with that name and kind + */ + public boolean hasChild(String name, E kind) { + return null != getChild(name) && kind == getChild(name).getKind(); + } + + /** + * Return the child node instance corresponding to the provided name, + * or null if no such node can be found. + * + * + * @param name the name of the node sought + * @return the child node instance corresponding to the provided name, + * or null if no such node can be found. + */ + public Node getChild(String name) { + return children.get(name); + } + + /** + * Return the distance that this token is away from the root. + * @return 0 if this node is the root node, + * otherwise the number of nodes from this node to the root node including this node. + */ + public int getDistanceFromRoot() { + int result = 0; + Node cursor = this; + while (!cursor.isRootNode()) { + result++; + cursor = cursor.getParent(); + } + return result; + } + + /** + * Add the provided values to the current node. + * + * @param values the values to add to this node instance + * @throws IllegalArgumentException when attempting to add values to the root node. + */ + public void appendValues(T... values) { + if ( isRootNode() ) { + throw new IllegalArgumentException("Cannot set a values on the root node."); + } + if ( values != null ) { + this.values.addAll(Arrays.asList(values)); + } + } + + /** + * Remove the provided value from the current node. + * @param value the value to remove from this node instance. + */ + public void removeValue(T value) { + if ( isRootNode() ) { + return; + } + this.values.remove(value); + } + + /** + * Return the collection of stored values for this node instance. + * If no values have been stored, we return an empty list. + * @return the collection of stored values for this node instance; an empty list if none have been stored. + */ + public List getValues() { + return values; + } + + /** + * Returns whether this node instance contains any values. + * @return true iff values have been stored in this node. + */ + public boolean hasValues() { + return values != null && values.size()>0; + } + + /** + * Return a link to the parent instance of this node, or null + * when invoked on the root node. + * + * @return a link to the parent instance of this node, or null + * when invoked on the root node. + */ + public Node getParent() { + return parent; + } + + /** + * Return the enumerated value from E for this kind of this node, or null + * when invoked on the root node. + * + * @return the enumerated value from E for this kind of this node, or null + * when invoked on the root node. + */ + public E getKind() { + return this.kind; + } + + /** + * Is this node the root node? + * @return true iff this node instance is the root node. + */ + public boolean isRootNode() { + return this.parent == null; + } + + /** + * Return the name of this node. + * @return the name of this node. null iff this node is the root node. + */ + public String getName() { + return name; + } + + /** + * Has at least one child been added to this node instance explicitly? + * @return true iff one child been added to this node instance explicitly. + */ + public boolean hasChildren() { + return children != null && children.size() > 0; + } + + /** + * Return whether this node instance's name is equal to {@link #getWildcardChar()}. + * + * @return true iff this node instance's name is equal to {@link #getWildcardChar()}. + */ + public boolean isWildcard() { + return name!=null && name.equals(getWildcardChar()); + } + + + + boolean hasWildcardChild() { + return hasChildren() && children.keySet().contains(getWildcardChar()); + } + + /** + * Get a fully qualified name from the root node down to this node. + *

+ * Implemented as: Walk up to the root, gathering names, then emit a dot-separated list of names. + * Useful for debugging. + * + * @return a fully qualified name from the root node down to this node. + */ + String getFullyQualifiedName() { + StringBuilder b = new StringBuilder(); + List name = new ArrayList(); + Node cursor = this; + while (!cursor.isRootNode()) { + name.add(cursor.name); + cursor = cursor.parent; + } + Collections.reverse(name); + for(String s: name) { + b.append(s).append('.'); + } + + if ( b.length() >= 1 && b.charAt(b.length()-1) == '.') { + b.deleteCharAt(b.length()-1); + } + + return b.toString(); + } + + public List> getChildrenAsList() { + return new ArrayList>(children.values()); + } + + /** + * Find the best matching node with respect to the tokens underneath this node. + * @param tokens the tokenized location to query. + * @param tokenIdx the index into the tokens to commence matching at. + * @return the best matching node or {@code null} if no matching node could be found. + */ + Node findBestMatchingNode(List> tokens, int tokenIdx) { + List> matches = findAllMatchingNodes(tokens, tokenIdx); + + Node resultNode = null; + int score = 0; + for (Node node : matches) { + if (node.getDistanceFromRoot() > score) { + score = node.getDistanceFromRoot(); + resultNode = node; + } + } + return resultNode; + } + + /** + * Find all matching nodes with respect to the tokens underneath this node. + * @param tokens the tokenized location to query. + * @param tokenIdx the index into the tokens to commence matching at. + * @return a collection of all matching nodes, which may be empty if no matching nodes were found. + */ + private List> findAllMatchingNodes(List> tokens, int tokenIdx) { + List> result = new ArrayList>(); + + // + // Iterate over this node's children. + // + List> nodes = this.getChildrenAsList(); + for (Node node : nodes) { + + // + // Do any tokens match the child node? + // + int matchResult = node.matches(tokens, tokenIdx); + if (matchResult < 0) { + // The node matched no tokens. + continue; + } + if (matchResult >= tokens.size()) { + // This node matched all remaining tokens. + + // + // Make sure we walk down further wildcard node(s) of the same kind + // as the node that matched all tokens and gather all values. + // + do { + if (node.hasValues()) { + result.add(node); + } + if ( node.hasWildcardChild()) { + Node child = node.getChild(getWildcardChar()); + if (child.getKind() != getKind()) { + node = null; + } else { + node = child; + } + + } else { + node = null; + } + } while (node != null); + + } else { + // + // This node matched some of the remaining tokens. + // So continue to find matching nodes for the remaining tokens (from matchResult onwards). + // + result.addAll(node.findAllMatchingNodes(tokens, matchResult)); + } + } + return result; + } + + /** + * Does this node match one or more tokens starting at tokenIdx? + *

+ * If not, returns {@code -1}. + * If so, return the index of the first non-matching token in the provided + * tokens, or {@code tokens.length} when all tokens in the array match the node. + * + * @param tokens the tokenized lcoation query + * @param tokenIdx the index of interest into the tokens + * @return the index of the first non-matching token starting at tokenIdx, + * {@code -1} when this node does not match any tokens starting at tokenIdx, + * {@code tokens.length} when this node matches all tokens starting at tokenIdx. + */ + private int matches(List> tokens, int tokenIdx) { + // Return no match (-1) for bad token indices + if (tokenIdx < 0 || tokenIdx >= tokens.size()) { + return -1; + } + + // For exact name matches return the next token index + if (matchesToken(tokens.get(tokenIdx))) { + return tokenIdx + 1; + } + + // Return no match (-1) since we are not a wildcard and not an exact match + if (!this.isWildcard()) { + return -1; + } else { + + // Return no match because wildcards match within Node kinds + if ( this.kind != tokens.get(tokenIdx).getKind()) { + return -1; + } + + do { + tokenIdx++; + } while ( tokenIdx < tokens.size() && this.kind == tokens.get(tokenIdx).getKind()); + return tokenIdx; + } + } + + private boolean matchesToken(Token token) { + return this.getName().equals(token.getName()) && + this.kind == token.getKind(); + } + + + } + + + static class Token> { + E kind; + String name; + + Token(String name, E element) { + this.kind = element; + this.name = name; + } + + public E getKind() { + return kind; + } + + public void setKind(E kind) { + this.kind = kind; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java b/android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java new file mode 100755 index 0000000..a1cfc5f --- /dev/null +++ b/android/src/main/java/org/kaazing/net/impl/auth/RealmUtils.java @@ -0,0 +1,65 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.impl.auth; + +import org.kaazing.net.auth.ChallengeRequest; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public final class RealmUtils { + + private RealmUtils() { + // prevent object creation + } + + private static final String REALM_REGEX = "(.*)\\s?(?i:realm=)(\"(.*)\")(.*)"; + private static final Pattern REALM_PATTERN= Pattern.compile(REALM_REGEX); + + /** + * A realm parameter is a valid authentication parameter for all authentication schemes + * according to RFC 2617 Section 2.1". + * + * + * The realm directive (case-insensitive) is required for all + * authentication schemes that issue a challenge. The realm value + * (case-sensitive), in combination with the canonical root URL (the + * absoluteURI for the server whose abs_path is empty) of the server + * being accessed, defines the protection space. + * + * + * @param challengeRequest the challenge request to extract a realm from + * + * @return the unquoted realm parameter value if present, or {@code null} if no such parameter exists. + */ + public static String getRealm(ChallengeRequest challengeRequest) { + String authenticationParameters = challengeRequest.getAuthenticationParameters(); + if ( authenticationParameters == null) { + return null; + } + Matcher m = REALM_PATTERN.matcher(authenticationParameters); + if ( m.matches() && m.groupCount()>=3) { + return m.group(3); + } + return null; + } +} diff --git a/android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java b/android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java new file mode 100755 index 0000000..1e6faa7 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/impl/util/BlockingQueueImpl.java @@ -0,0 +1,137 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.impl.util; + +import java.util.concurrent.ArrayBlockingQueue; + +/** + * ArrayBlockingQueue extension with ability to interrupt or end-of-stream. + * This will be used by producer(ie. the listener) and the consumer(ie. the + * WebSocketMessageReader). To match the 3.X event-listener behavior, the + * capacity of the queue can be set to 1. + * + * @param element type + */ +public class BlockingQueueImpl extends ArrayBlockingQueue { + private static final long serialVersionUID = 1L; + + // ### TODO: Maybe expose an API on WebSocket/WsURLConnection for developers + // to specify the number of incoming messages that can be held + // before we start pushing on the network. + private static final int _QUEUE_CAPACITY = 32; + + private boolean _done = false; + + public BlockingQueueImpl() { + super(_QUEUE_CAPACITY, true); + } + + public synchronized void done() { + _done = true; + notifyAll(); + } + + public boolean isDone() { + return _done; + } + + public synchronized void reset() { + // Wake up threads that maybe blocked to retrieve data. + notifyAll(); + clear(); + _done = false; + } + + // Override to make peek() a blocking call. + @Override + public E peek() { + E el; + + while (((el = super.peek()) == null) && !isDone()) { + synchronized (this) { + try { + wait(); + } catch (InterruptedException e) { + String s = "Reader has been interrupted maybe the connection is closed"; + throw new RuntimeException(s); + } + } + } + + if ((el == null) && isDone()) { + String s = "Reader has been interrupted maybe the connection is closed"; + throw new RuntimeException(s); + } + + return el; + } + + @Override + public void put(E el) throws InterruptedException { + synchronized (this) { + while ((size() == _QUEUE_CAPACITY) && !isDone()) { + // Push on the network as the messages are not being retrieved. + wait(); + } + + if (isDone()) { + notifyAll(); + return; + } + } + + super.put(el); + + synchronized (this) { + notifyAll(); + } + } + + @Override + public E take() throws InterruptedException { + E el = null; + + synchronized (this) { + while (isEmpty() && !isDone()) { + wait(); + } + + if (isDone()) { + notifyAll(); + + if (size() == 0) { + String s = "Reader has been interrupted maybe the connection is closed"; + throw new InterruptedException(s); + } + } + } + + el = super.take(); + + synchronized (this) { + notifyAll(); + } + + return el; + } +} + diff --git a/android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java b/android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java new file mode 100755 index 0000000..cb1547d --- /dev/null +++ b/android/src/main/java/org/kaazing/net/impl/util/ResumableTimer.java @@ -0,0 +1,133 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.impl.util; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicLong; + +public class ResumableTimer { + public enum PauseStrategy { UPDATE_DELAY, DO_NOT_UPDATE_DELAY }; + + private final Runnable runnable; + private volatile boolean taskExecuted = false; + + private AtomicLong delay; // milliseconds + private AtomicLong startTime; // milliseconds since epoch + private Timer timer; + private boolean updateDelayWhenPaused; + + public ResumableTimer(Runnable runnable, long delay, boolean updateDelayWhenPaused) { + if (runnable == null) { + throw new IllegalArgumentException("runnable is null"); + } + + if (delay < 0) { + throw new IllegalArgumentException("Timer delay cannot be negative"); + } + + this.delay = new AtomicLong(delay); + this.startTime = new AtomicLong(0L); + this.runnable = runnable; + this.updateDelayWhenPaused = updateDelayWhenPaused; + } + + public synchronized void cancel() { + if (timer != null) { + timer.cancel(); + } + + timer = null; + delay.set(-1L); + startTime.set(-1L); + taskExecuted = false; + } + + public boolean didTaskExecute() { + return taskExecuted; + } + + public synchronized long getDelay() { + return delay.get(); + } + + public synchronized void pause() { + long elapsedTime = System.currentTimeMillis() - startTime.get(); + + if (timer == null) { + // throw new IllegalStateException("Timer is not running"); + return; + } + + timer.cancel(); + timer = null; + + // If updateDelayWhenPaused is true, then update this.delay by + // subtracting the elapsed time. Otherwise, this.delay is not modified. + if (this.updateDelayWhenPaused) { + assert(elapsedTime < delay.get()); + delay.compareAndSet(delay.get(), (delay.get() - elapsedTime)); + } + } + + public synchronized void resume() { + if (timer != null) { + // throw new IllegalStateException("Timer is already running"); + return; + } + + if (delay.get() < 0) { + throw new IllegalStateException("Timer delay cannot be negative"); + } + + timer = new Timer("ResumableTimer", true); + startTime.compareAndSet(startTime.get(), System.currentTimeMillis()); + timer.schedule(new RunnableTask(runnable), delay.get()); + } + + public synchronized void start() { + resume(); + } + + private synchronized void cleanup() { + taskExecuted = true; + startTime.set(-1L); + timer = null; + } + + private class RunnableTask extends TimerTask { + private final Runnable runnable; + + public RunnableTask(Runnable runnable) { + if (runnable == null) { + throw new NullPointerException("runnable is null"); + } + + this.runnable = runnable; + } + + public void run() { + runnable.run(); + ResumableTimer.this.cleanup(); + } + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventReader.java b/android/src/main/java/org/kaazing/net/sse/SseEventReader.java new file mode 100755 index 0000000..3627456 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/SseEventReader.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse; + +import java.io.IOException; + +public abstract class SseEventReader { + /** + * Returns the payload of the last received event. This method returns a + * null until the first event is received. Note that this is not a blocking + * call. Typically, this method should be invoked after {@link #next()} ONLY + * if the returned type is {@link SseEventType#DATA}. Otherwise, an + * IOException is thrown. + *

+ * If {@link #next()} returns any other event type, then subsequent + * invocation of this method returns a null. + *

+ * @return String event payload; a null is returned if + * invoked when not connected or before the first + * event is received + * @throws IOException if the event type is not SseEventType.DATA + */ + public abstract CharSequence getData() throws IOException; + + /** + * Returns the name of the last received event. This method returns a null + * until the first event is received. Note that this is not a blocking + * call. Typically, this method should be invoked after {@link #next()}. + * It's perfectly legal for an event name to be null even if it contains + * data. Similarly, it is perfectly legal for an event of type + * {@link SseEvent#EMPTY} to have an event name. + *

+ * @return String event name; a null is returned if invoked + * when not connected or before the first event is + * received + */ + public abstract String getName(); + + /** + * Returns the {@link SseEventType} of the already received event. This + * method returns a null until the first event is received. Note that + * this is not a blocking call. When connected, if this method is invoked + * immediately after {@link #next()}, then they will return the same value. + *

+ * Based on the returned {@link SseEventType}, the application developer can + * decide whether to read the data. This method will continue to return the + * same {@link SseEventType} till the next event arrives. When the next + * event arrives, this method will return the the {@link SseEventType} + * associated with that event. + *

+ * @return SseEventType SseEventType.DATA for an event that contain + * data; SseEventType.EMPTY for an event that + * is empty with no data; WebSocketMessageType.EOS + * if the connection is closed; a null is + * returned if not connected or before the first + * event is received + */ + public abstract SseEventType getType(); + + /** + * Invoking this method will cause the thread to block until an event is + * received. When the event is received, it will return the type of the + * newly received event. Based on {@link SseEventType}, the application + * developer can decide whether to invoke the {@link #readData()} method. + * When the connection is closed, this method returns + * {@link SseEventType#EOS}. + *

+ * An IOException is thrown if this method is invoked before the connection + * has been established. + *

+ * @return SseEventType SseEventType.DATA for an event that contain + * data; SseEventType.EMPTY for an event that + * is empty with no data; WebSocketMessageType.EOS + * if the connection is closed + * @throws IOException if invoked before the connection is established + */ + public abstract SseEventType next() throws IOException; +} diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventSource.java b/android/src/main/java/org/kaazing/net/sse/SseEventSource.java new file mode 100755 index 0000000..0b00dcb --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/SseEventSource.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse; + +import java.io.IOException; + +import org.kaazing.net.http.HttpRedirectPolicy; + +/** + * SseEventSource provides an implementation of HTML5 Server-sent Events. + * Refer to HTML5 EventSource at + * {@link http://www.whatwg.org/specs/web-apps/current-work/#server-sent-events} + * {@link http://www.whatwg.org/specs/web-apps/current-work/#the-event-source} + */ +public abstract class SseEventSource { + /** + * Disconnects with the server. This is a blocking call that returns only + * when the shutdown is complete. + * + * @throws IOException if the disconnect did not succeed + */ + public abstract void close() throws IOException; + + /** + * Connects with the server using an end-point. This is a blocking call. The + * thread invoking this method will be blocked till a successful connection + * is established. If the connection cannot be established, then an + * IOException is thrown and the thread is unblocked. + * + * @throws IOException if the connection cannot be established + */ + public abstract void connect() throws IOException; + + /** + * Returns a {@link SseEventReader} that can be used to receive + * events based on the {@link SseEventType}. + *

+ * If this method is invoked before a connection is established successfully, + * then an IOException is thrown. + * + * @return SseEventReader to receive events + * @throws IOException if invoked before the connection is opened + */ + public abstract SseEventReader getEventReader() throws IOException; + + /** + * Returns {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). The default option is + * {@link HttpRedirectPolicy#NONE}. + * + * @return HttpRedirectOption indicating the + */ + public abstract HttpRedirectPolicy getFollowRedirect(); + + /** + * Returns the retry timeout in milliseconds. The default is 3000ms. + * + * @return retry timeout in milliseconds + */ + public abstract long getRetryTimeout(); + + /** + * Sets {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). + * + * @param option HttpRedirectOption to used for following the + * redirects + */ + public abstract void setFollowRedirect(HttpRedirectPolicy option); + + /** + * Sets the retry timeout specified in milliseconds. + * + * @param millis retry timeout + */ + public abstract void setRetryTimeout(long millis); +} diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java b/android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java new file mode 100755 index 0000000..bffcb45 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/SseEventSourceFactory.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ServiceLoader; + +import org.kaazing.net.http.HttpRedirectPolicy; + +/** + * {@link SseEventSourceFactory} is an abstract class that can be used to create + * {@link SseEventSource}s by specifying the end-point. It may be extended to + * instantiate particular subclasses of {@link SseEventSource} and thus provide + * a general framework for the addition of public SSE-level functionality. + */ +public abstract class SseEventSourceFactory { + + protected SseEventSourceFactory() { + + } + + /** + * Creates and returns a new instance of the default implementation of the + * {@link SseEventSourceFactory}. + * + * @return SseEventSourceFactory + */ + public static SseEventSourceFactory createEventSourceFactory() { + Class clazz = SseEventSourceFactory.class; + ServiceLoader loader = ServiceLoader.load(clazz); + return loader.iterator().next(); + } + + /** + * Creates a {@link SseEventSource} to connect to the target location. + *

+ * + * @param location URI of the SSE provider for the connection + * @throws URISyntaxException + */ + public abstract SseEventSource createEventSource(URI location) + throws URISyntaxException; + + /** + * Returns the default {@link HttpRedirectPolicy} that was specified at + * on the factory. + * + * ### TODO: If this wasn't set, should we return HttpRedirectOption.NONE or + * null. + * + * @return HttpRedirectOption + */ + public abstract HttpRedirectPolicy getDefaultFollowRedirect(); + + /** + * Returns the default retry timeout. The default is 3000 milliseconds. + * + * @return retry timeout in milliseconds + */ + public abstract long getDefaultRetryTimeout(); + + /** + * Sets the default {@link HttpRedirectPolicy} that is to be inherited by + * all the {@link EventSource}s created using this factory instance. + * + * @param option HttpRedirectOption + */ + public abstract void setDefaultFollowRedirect(HttpRedirectPolicy option); + + /** + * Sets the default retry timeout that is to be inherited by all the + * {@link EventSource}s created using this factory instance. + * + * @param millis retry timeout + */ + public abstract void setDefaultRetryTimeout(long millis); +} diff --git a/android/src/main/java/org/kaazing/net/sse/SseEventType.java b/android/src/main/java/org/kaazing/net/sse/SseEventType.java new file mode 100755 index 0000000..9f9c177 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/SseEventType.java @@ -0,0 +1,26 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse; + +public enum SseEventType { + EOS, EMPTY, DATA; +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/sse/SseException.java b/android/src/main/java/org/kaazing/net/sse/SseException.java new file mode 100755 index 0000000..11fce00 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/SseException.java @@ -0,0 +1,37 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse; + +import java.io.IOException; + +public class SseException extends IOException { + + private static final long serialVersionUID = 1L; + + public SseException(String reason) { + super(reason); + } + + public SseException(Exception ex) { + super(ex); + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java b/android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java new file mode 100755 index 0000000..513c477 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/DefaultEventSourceFactory.java @@ -0,0 +1,85 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +import java.net.URI; +import java.net.URISyntaxException; + +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.sse.SseEventSource; +import org.kaazing.net.sse.SseEventSourceFactory; + +public class DefaultEventSourceFactory extends SseEventSourceFactory { + + private long _retryTimeout; + private HttpRedirectPolicy _redirectOption; + + public DefaultEventSourceFactory() { + _retryTimeout = 3000; + + // ### TODO: Should _redirectOption be null or + // HttpRedirectOption.ALWAYS by default. Note that in + // HttpURLConnection, followRedirects is true by default. + _redirectOption = HttpRedirectPolicy.ALWAYS; + } + + @Override + public SseEventSource createEventSource(URI location) + throws URISyntaxException { + + String scheme = location.getScheme(); + if (!scheme.toLowerCase().equals("sse") && + !scheme.toLowerCase().equals("http") && + !scheme.toLowerCase().equals("https")) { + String s = String.format("Incorrect scheme or protocol '%s'", scheme); + throw new URISyntaxException(location.toString(), s); + } + + SseEventSourceImpl eventSource = new SseEventSourceImpl(location); + + // Set up the defaults from the factory. + eventSource.setFollowRedirect(_redirectOption); + eventSource.setRetryTimeout(_retryTimeout); + + return eventSource; + } + + @Override + public HttpRedirectPolicy getDefaultFollowRedirect() { + return _redirectOption; + } + + @Override + public long getDefaultRetryTimeout() { + return _retryTimeout; + } + + @Override + public void setDefaultFollowRedirect(HttpRedirectPolicy redirectOption) { + _redirectOption = redirectOption; + } + + @Override + public void setDefaultRetryTimeout(long millis) { + _retryTimeout = millis; + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java new file mode 100755 index 0000000..2bd5c13 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SseEventReaderImpl.java @@ -0,0 +1,205 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +import java.io.IOException; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.net.impl.util.BlockingQueueImpl; +import org.kaazing.net.sse.SseEventReader; +import org.kaazing.net.sse.SseEventType; +import org.kaazing.net.sse.SseException; + +public class SseEventReaderImpl extends SseEventReader { + private static final String _CLASS_NAME = SseEventReaderImpl.class.getName(); + private static final Logger _LOG = Logger.getLogger(_CLASS_NAME); + + private final BlockingQueueImpl _sharedQueue; + private final SseEventSourceImpl _eventSource; + + private SsePayload _payload; + private SseEventType _eventType; + private String _eventName; + private String _data; + + public SseEventReaderImpl(SseEventSourceImpl eventSource, + BlockingQueueImpl sharedQueue) { + _eventSource = eventSource; + _sharedQueue = sharedQueue; + + _payload = null; + _eventType = null; + _eventName = null; + _data = null; + } + + @Override + public CharSequence getData() throws IOException { + if (_payload == null) { + return null; + } + + if (_eventType != SseEventType.DATA) { + String s = "readData() can only be used to read events " + + "of type SseEventType.DATA"; + throw new SseException(s); + } + + return _data; + } + + @Override + public String getName() { + return _eventName; + } + + @Override + public SseEventType getType() { + return _eventType; + } + + @Override + public SseEventType next() throws IOException { + if (_sharedQueue.isDone()) { + _eventType = SseEventType.EOS; + return _eventType; + } + + synchronized (this) { + if (!_eventSource.isConnected()) { + _eventType = SseEventType.EOS; + return _eventType; + } + + try { + _payload = null; + _payload = (SsePayload) _sharedQueue.take(); + } + catch (InterruptedException ex) { + _LOG.log(Level.FINE, ex.getMessage()); + } + + if (_payload == null) { + String s = "Reader has been interrupted maybe the connection " + + "is closed"; + // throw new SseException(s); + _LOG.log(Level.FINE, _CLASS_NAME, s); + + _eventType = SseEventType.EOS; + return _eventType; + } + + _data = _payload.getData(); + _eventName = _payload.getEventName(); + _eventType = (_payload.getData() == null) ? SseEventType.EMPTY : + SseEventType.DATA; + } + + return _eventType; + } + + // ------------------ Package-Private Implementation ---------------------- + // These methods are called from other classes in this package. They are + // not part of the public API. + void close() throws IOException { + _sharedQueue.done(); + _payload = null; + _eventType = null; + _data = null; + _eventName = null; + } + + void reset() throws IOException { + _sharedQueue.reset(); + _payload = null; + _eventType = null; + _data = null; + _eventName = null; + } + + // ------------- Currently not being used methods ------------------------- + // This was earlier part of our public API. It's no longer being exposed. + /** + * Returns the payload of the event. Use this method to retrieve the + * payload only if the event's type is {@link SseEventType.DATA}. + *

+ * If this method is invoked AFTER {@link #next()} method, then it will not + * block. If this method is invoked before {@link #next()} method, then it + * will block till a message is received. If the type of received event is + * {@link SseEventType#EMPTY}, then this method will throw an IOException. + *

+ * An IOException is thrown if the connection is closed while blocked. An + * IOException is thrown if this method is invoked before the connection + * has been established. + *

+ * @return CharSequence event's payload + * @throws IOException if the connection is closed; if the received + * event's type is SseEventType.EMPTY; if invoked + * before connection is established + */ + @SuppressWarnings("unused") + private CharSequence readData() throws IOException { + if (!_eventSource.isConnected()) { + String s = "Can't read using the MessageReader if the event " + + "source is not connected"; + throw new SseException(s); + } + + synchronized (this) { + if (_payload != null) { + // If we are here, then it means that readData() was invoked + // after next(). So, the _payload is already setup and we just + // have to return the data. + if (_eventType != SseEventType.DATA) { + String s = "readData() can only be used to read events " + + "of type SseEventType.DATA"; + throw new SseException(s); + } + + // Clear the _payload member variable for the internal state + // machine. + _payload = null; + return _data; + } + + // This will block the thread. If we are here, this means that + // readData() was invoked without a previous invocation of next(). + // So, we invoke next() and ensure that the next message is a text + // message. Otherwise, throw an exception. + SseEventType type = next(); + + if (type != SseEventType.DATA) { + String s = "readData() can only be used to read events " + + "of type SseEventType.DATA"; + throw new SseException(s); + } + + _data = _payload.getData(); + _eventName = _payload.getEventName(); + + // Clear the _payload member variable for the internal state machine. + _payload = null; + return _data; + } + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseEventSourceImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/SseEventSourceImpl.java new file mode 100755 index 0000000..b85f7e9 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SseEventSourceImpl.java @@ -0,0 +1,328 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +import java.io.IOException; +import java.net.URI; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.impl.util.BlockingQueueImpl; +import org.kaazing.net.sse.SseEventReader; +import org.kaazing.net.sse.SseEventSource; +import org.kaazing.net.sse.SseException; + +public class SseEventSourceImpl extends SseEventSource { + private static final String _CLASS_NAME = SseEventSourceImpl.class.getName(); + private static final Logger _LOG = Logger.getLogger(_CLASS_NAME); + + /** + * Values are CONNECTING = 0, OPEN = 1, CLOSING = 2, and CLOSED = 3; + */ + enum ReadyState { + CONNECTING, OPEN, CLOSING, CLOSED; + } + + private SseEventStream _eventStream; + private ReadyState _readyState; + private SseException _exception; + private SseEventReaderImpl _eventReader; + private BlockingQueueImpl _sharedQueue; + private URI _location; + private HttpRedirectPolicy _redirectOption; + private long _retryTimeout = 3000; + + public SseEventSourceImpl(URI location) { + URI loc = location; + + // Map "sse" to "http". + if (location.getScheme().equalsIgnoreCase("sse")) { + String fragment = location.getFragment(); + String schemeSpecificPart = location.getSchemeSpecificPart(); + + if (fragment == null) { + fragment = ""; + } + loc = URI.create("http:" + schemeSpecificPart + fragment); + } + + _location = loc; + _readyState = ReadyState.CLOSED; + + // Used by the producer(i.e. the eventSourceListener) and the + // consumer(i.e. the SseEventReader). + _sharedQueue = new BlockingQueueImpl(); + + } + + @Override + public synchronized void close() throws IOException { + _LOG.entering(_CLASS_NAME, "close"); + + if ((_readyState == ReadyState.CLOSED) || + (_readyState == ReadyState.CLOSING)) { + // Since the WebSocket is already closed/closing, we just bail. + _LOG.log(Level.FINE, "Event source is not connected"); + return; + } + + setException(null); + _readyState = ReadyState.CLOSING; + + _eventStream.stop(); + + _readyState = ReadyState.CLOSED; + + cleanupAfterClose(); + } + + @Override + public synchronized void connect() throws IOException { + _LOG.entering(_CLASS_NAME, "connect"); + + if (_readyState != ReadyState.CLOSED) { + String s = "Event source must be closed before connecting"; + throw new SseException(s); + } + + _eventStream = new SseEventStream(_location.toString()); + _eventStream.setListener(_eventStreamListener); + _eventStream.setRetryTimeout(_retryTimeout); + + if (_eventReader != null) { + // Ensure that the reader is reset and ready to block the consumer + // if no data has been produced. + _eventReader.reset(); + } + + // Prepare the state for connection. + _readyState = ReadyState.CONNECTING; + setException(null); + + // Connect to the event source. Note that it all happens on the same + // thread. The registered SseEventStreamListener is also invoked as part + // of this call. In WebSocket, we have to block to synchronize with the + // other thread that invokes the listener. Here, we don't have to. + _eventStream.connect(); + + // Check if there is any exception that needs to be reported. + SseException exception = getException(); + if (exception != null) { + throw exception; + } + } + + @Override + public SseEventReader getEventReader() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot get the SseEventReader as the event source is not yet connected"; + throw new IOException(s); + } + + synchronized (this) { + if (_eventReader != null) { + return _eventReader; + } + + _eventReader = new SseEventReaderImpl(this, _sharedQueue); + } + + return _eventReader; + } + + + @Override + public HttpRedirectPolicy getFollowRedirect() { + return _redirectOption; + } + + @Override + public long getRetryTimeout() { + return _retryTimeout; + } + + @Override + public void setFollowRedirect(HttpRedirectPolicy redirectOption) { + _redirectOption = redirectOption; + } + + @Override + public void setRetryTimeout(long millis) { + _retryTimeout = millis; + + if (_eventStream != null) { + _eventStream.setRetryTimeout(millis); + } + } + + // ---------------------- Internal Implementation ------------------------ + public SseException getException() { + return _exception; + } + + public void setException(SseException exception) { + _exception = exception; + } + + public boolean isConnected() { + return (_readyState == ReadyState.OPEN); + } + + public boolean isDisconnected() { + return (_readyState == ReadyState.CLOSED); + } + + public BlockingQueueImpl getSharedQueue() { + return _sharedQueue; + } + + // --------------------- Private Methods --------------------------------- + private synchronized void connectionOpened() { + _readyState = ReadyState.OPEN; + + // Unblock the connect() call so that it can proceed. + notifyAll(); + } + + private void messageArrived(String eventName, String data) { + if (_readyState != ReadyState.OPEN) { + // If the connection is closed, then we should be queuing the + // events/payload. + return; + } + + synchronized (_sharedQueue) { + try { + _sharedQueue.put(new SsePayload(eventName, data)); + } + catch (InterruptedException ex) { + _LOG.log(Level.INFO, ex.getMessage(), ex); + } + } + } + + @SuppressWarnings("unused") + private synchronized void connectionClosed(String reason) { + _readyState = ReadyState.CLOSED; + + if (reason != null) { + setException(new SseException(reason)); + } + + cleanupAfterClose(); + + // Unblock the close() call so that it can proceed. + notifyAll(); + } + + private synchronized void connectionFailed(Exception exception) { + SseException ex = null; + + if (exception == null) { + ex = new SseException("Connection Failed"); + } + else { + ex = new SseException(exception); + } + + setException(ex); + + _readyState = ReadyState.CLOSED; + + cleanupAfterClose(); + + // Unblock threads so that they can proceed. + notifyAll(); + } + + private synchronized void cleanupAfterClose() { + if (_eventReader != null) { + // Notify the waiting consumers that the connection is closing. + try { + _eventReader.close(); + } + catch (IOException ex) { + _LOG.log(Level.FINE, ex.getMessage(), ex); + } + } + else { + getSharedQueue().done(); + } + } + + + private SseEventStreamListener _eventStreamListener = new SseEventStreamListener() { + + @Override + public void streamOpened() { + _LOG.entering(_CLASS_NAME, "streamOpened"); + connectionOpened(); + + /* + EventSourceEvent event = new EventSourceEvent(this, EventSourceEvent.Type.OPEN); + for (EventSourceListener listener : listeners) { + try { + listener.onOpen(event); + } catch (RuntimeException e) { + String s = "Application threw an exception during onOpen: "+e.getMessage(); + _LOG.logp(Level.WARNING, _CLASS_NAME, "onOpen", s, e); + } + } + */ + } + + @Override + public void messageReceived(String eventName, String message) { + _LOG.entering(_CLASS_NAME, "messageReceived", message); + messageArrived(eventName, message); + + /* + EventSourceEvent event = new EventSourceEvent(this, EventSourceEvent.Type.MESSAGE, message); + for (EventSourceListener listener : listeners) { + try { + listener.onMessage(event); + } catch (RuntimeException e) { + LOG.logp(Level.WARNING, CLASS_NAME, "onMessage", "Application threw an exception during onMessage: "+e.getMessage(), e); + } + } + */ + } + + @Override + public void streamErrored(Exception exception) { + _LOG.entering(_CLASS_NAME, "streamErrored"); + connectionFailed(exception); + + /* + EventSourceEvent event = new EventSourceEvent(this, EventSourceEvent.Type.ERROR); + for (EventSourceListener listener : listeners) { + try { + listener.onError(event); + } catch (RuntimeException e) { + LOG.logp(Level.WARNING, CLASS_NAME, "onError", "Application threw an exception during onError: "+e.getMessage(), e); + } + } + */ + } + }; +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseEventStream.java b/android/src/main/java/org/kaazing/net/sse/impl/SseEventStream.java new file mode 100755 index 0000000..d08b743 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SseEventStream.java @@ -0,0 +1,362 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +import org.kaazing.gateway.client.impl.http.HttpRequest; +import org.kaazing.gateway.client.impl.http.HttpRequest.Method; +import org.kaazing.gateway.client.impl.http.HttpRequestAuthenticationHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestHandlerFactory; +import org.kaazing.gateway.client.impl.http.HttpRequestListener; +import org.kaazing.gateway.client.impl.http.HttpRequestRedirectHandler; +import org.kaazing.gateway.client.impl.http.HttpRequestTransportHandler; +import org.kaazing.gateway.client.impl.http.HttpResponse; +import org.kaazing.gateway.client.impl.ws.ReadyState; +import org.kaazing.gateway.client.util.HttpURI; +import org.kaazing.gateway.client.util.WrappedByteBuffer; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * ServerSentEvent stream implementation. + */ +public class SseEventStream { + private static final String MESSAGE = "message"; + private static final String CLASS_NAME = SseEventStream.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + private static final Charset UTF_8 = Charset.forName("UTF-8"); + private final StringBuffer dataBuffer = new StringBuffer(); + + private transient static final Timer timer = new Timer("reconnect", true); + + static final HttpRequestHandlerFactory SSE_HANDLER_FACTORY = new HttpRequestHandlerFactory() { + + @Override + public HttpRequestHandler createHandler() { + HttpRequestAuthenticationHandler authHandler = new HttpRequestAuthenticationHandler(); + HttpRequestRedirectHandler redirectHandler = new HttpRequestRedirectHandler(); + HttpRequestHandler transportHandler = HttpRequestTransportHandler.DEFAULT_FACTORY.createHandler(); + + authHandler.setNextHandler(redirectHandler); + redirectHandler.setNextHandler(transportHandler); + + return authHandler; + } + }; + + private ReadyState readyState = ReadyState.CONNECTING; + private String lastEventId = ""; + private boolean aborted = false; + private boolean errored = false; + private String sseLocation; + private long retry = 3000; // same as actionscript implementation + private boolean immediateReconnect = false; + private String name = MESSAGE; // event name stores here between data receiving progress events + private String messageBuffer = ""; + private HttpRequest sseSource; + private AtomicBoolean progressEventReceived = new AtomicBoolean(false); + private AtomicBoolean reconnected = new AtomicBoolean(false); + private HttpRequestHandler sseHandler; + private SseEventStreamListener listener; + + + public SseEventStream(String sseLoc) throws IOException { + LOG.entering(CLASS_NAME, "", sseLoc); + + // Validate the URI. + URI.create(sseLoc); + + this.sseLocation = sseLoc; + + sseHandler = SSE_HANDLER_FACTORY.createHandler(); + + sseHandler.setListener(new EventStreamHttpRequestListener()); + } + + public ReadyState getReadyState() { + return readyState; + } + + public void stop() { + LOG.entering(CLASS_NAME, "stop"); + readyState = ReadyState.CLOSED; + sseHandler.processAbort(sseSource); + aborted = true; + } + + public void connect() throws IOException { + LOG.entering(CLASS_NAME, "connect"); + if (lastEventId != null && (lastEventId.length() > 0)) { + sseLocation += (sseLocation.indexOf("?") == -1 ? "?" : "&") + ".ka=" + lastEventId; + } + + try { + HttpURI uri = new HttpURI(this.sseLocation); + sseSource = new HttpRequest(Method.GET, uri, true); + sseHandler.processOpen(sseSource); + + if (!reconnected.get()) { + TimerTask timerTask = new TimerTask() { + @Override + public void run() { + // TODO: Why is this commented out? - no fallback to long polling? + + // if (!SseEventStream.this.progressEventReceived.get() && readyState != ReadyState.CLOSED) { + // if (sseLocation.indexOf("?") == -1) { + // sseLocation += "?.ki=p"; + // } + // else { + // sseLocation += "&.ki=p"; + // } + // listener.reconnectScheduled = true; + // reconnected.set(true); + // retry = 0; + // try { + // connect(); + // } + // catch (IOException e) { + // // TODO Auto-generated catch block + // e.printStackTrace(); + // } + // } + } + }; + Timer timer = new Timer(); + timer.schedule(timerTask, 3000); + } + } catch (Exception e) { + LOG.log(Level.INFO, e.getMessage(), e); + doError(e); + } + } + + public long getRetryTimeout() { + return retry; + } + + public void setRetryTimeout(long millis) { + retry = millis; + } + + private synchronized void reconnect() { + LOG.entering(CLASS_NAME, "reconnect"); + if (readyState != ReadyState.CLOSED) { + TimerTask task = new TimerTask() { + @Override + public void run() { + try { + connect(); + } catch (IOException e) { + LOG.log(Level.INFO, e.getMessage(), e); + throw new RuntimeException(e); + } + } + }; + timer.schedule(task, retry); + } + } + + private synchronized void processProgressEvent(String message) { + LOG.entering(CLASS_NAME, "processProgressEvent", message); + String line; + try { + messageBuffer = messageBuffer + message; + String field = null; + String value = null; + name = name != null ? name : MESSAGE; + String data = ""; + immediateReconnect = false; + while (!aborted && !errored) { + line = fetchLineFromBuffer(); + if (line == null) { + break; + } + + if (line.length() == 0 && dataBuffer.length() > 0) { + synchronized (dataBuffer) { + int dataBufferlength = dataBuffer.length(); + if (dataBuffer.charAt(dataBufferlength - 1) == '\n') { + dataBuffer.replace(dataBufferlength - 1, dataBufferlength, ""); + } + doMessage(name, dataBuffer.toString()); + name = null; + dataBuffer.setLength(0); + } + } + + int colonAt = line.indexOf(':'); + if (colonAt == -1) { + // no colon, line is field name with empty value + field = line; + value = ""; + } else if (colonAt == 0) { + // leading colon indicates comment line + continue; + } else { + field = line.substring(0, colonAt); + int valueAt = colonAt + 1; + if (line.length() > valueAt && line.charAt(valueAt) == ' ') { + valueAt++; + } + value = line.substring(valueAt); + } + // process the field of completed event + if (field.equals("event")) { + name = value; + } else if (field.equals("id")) { + this.lastEventId = value; + } else if (field.equals("retry")) { + retry = Integer.parseInt(value); + } else if (field.equals("data")) { + // deliver event if data is specified and non-empty, or name is specified and not "message" + if (value != null || (name != null && name.length() > 0 && !MESSAGE.equals(name))) { + dataBuffer.append(value).append("\n"); + } + } else if (field.equals("location")) { + if (value != null && value.length() > 0) { + this.sseLocation = value; + } + } else if (field.equals("reconnect")) { + immediateReconnect = true; + } + } + + if (immediateReconnect) { + retry = 0; + // this will be done on the load + } + } catch (Exception e) { + LOG.log(Level.INFO, e.getMessage(), e); + doError(e); + } + } + + private String fetchLineFromBuffer() { + LOG.entering(CLASS_NAME, "fetchLineFromBuffer"); + int lf = this.messageBuffer.indexOf("\n"); + if (lf == -1) { + lf = this.messageBuffer.indexOf("\r"); + } + if (lf != -1) { + String ret = messageBuffer.substring(0, lf); + messageBuffer = messageBuffer.substring(lf + 1); + return ret; + } + return null; + } + + private class EventStreamHttpRequestListener implements HttpRequestListener { + private final String CLASS_NAME = EventStreamHttpRequestListener.class.getName(); + private final Logger LOG = Logger.getLogger(CLASS_NAME); + + boolean reconnectScheduled = false; + + EventStreamHttpRequestListener() { + LOG.entering(CLASS_NAME, ""); + } + + @Override + public void requestReady(HttpRequest request) { + } + + @Override + public void requestOpened(HttpRequest request) { + doOpen(); + } + + @Override + public void requestProgressed(HttpRequest request, WrappedByteBuffer payload) { + progressEventReceived.set(true); + String response = payload.getString(UTF_8); + processProgressEvent(response); + } + + @Override + public void requestLoaded(HttpRequest request, HttpResponse response) { + // for Long polling. If we get an onload we have to + // reconnect. + if (readyState != ReadyState.CLOSED) { + if (immediateReconnect) { + retry = 0; + if (!reconnectScheduled) { + reconnect(); + } + } + } + } + + @Override + public void requestAborted(HttpRequest request) { + } + + @Override + public void requestClosed(HttpRequest request) { + } + + @Override + public void errorOccurred(HttpRequest request, Exception exception) { + doError(exception); + } + } + + private void doOpen() { + /** + * Only file the event once in the case its already opened, + * Currently, this is being called twice, once when the SSE + * gets connected and then again when the ready state changes. + */ + if(readyState == ReadyState.CONNECTING){ + readyState = ReadyState.OPEN; + listener.streamOpened(); + } + } + + private void doMessage(String eventName, String data) { + // messages before OPEN and after CLOSE should not be delivered. + if (getReadyState() != ReadyState.OPEN) { + return; + } + + listener.messageReceived(eventName, data); + } + + private void doError(Exception exception) { + if (getReadyState() == ReadyState.CLOSED) { + return; + } + + // TODO: Set readyState to CLOSED? + errored = true; + listener.streamErrored(exception); + } + + public void setListener(SseEventStreamListener listener) { + this.listener = listener; + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseEventStreamListener.java b/android/src/main/java/org/kaazing/net/sse/impl/SseEventStreamListener.java new file mode 100755 index 0000000..a9fba75 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SseEventStreamListener.java @@ -0,0 +1,30 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +public interface SseEventStreamListener { + + void streamOpened(); + void messageReceived(String eventName, String data); + void streamErrored(Exception exception); + +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SsePayload.java b/android/src/main/java/org/kaazing/net/sse/impl/SsePayload.java new file mode 100755 index 0000000..ecd3c5a --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SsePayload.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +public class SsePayload { + + private String _eventName; + private String _data; + + public SsePayload(String eventName, String data) { + _eventName = eventName; + _data = data; + } + + public String getData() { + return _data; + } + + public String getEventName() { + return _eventName; + } + + public void setData(String data) { + _data = data; + } + + public void setEventName(String eventName) { + if ((eventName == null) || (eventName.trim().length() == 0)) { + eventName = "message"; + } + + _eventName = eventName; + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseURLConnection.java b/android/src/main/java/org/kaazing/net/sse/impl/SseURLConnection.java new file mode 100755 index 0000000..564e029 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SseURLConnection.java @@ -0,0 +1,100 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +import java.io.IOException; +import java.net.URL; +import java.net.URLConnection; + +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.sse.SseEventReader; +import org.kaazing.net.sse.SseEventType; + +public abstract class SseURLConnection extends URLConnection { + + protected SseURLConnection(URL url) { + super(url); + } + + /** + * Disconnects with the server. This is a blocking call that returns only + * when the shutdown is complete. + * + * @throws IOException if the disconnect did not succeed + */ + public abstract void close() throws IOException; + + /** + * Connects with the server using an end-point. This is a blocking call. The + * thread invoking this method will be blocked till a successful connection + * is established. If the connection cannot be established, then an + * IOException is thrown and the thread is unblocked. + * + * @throws IOException if the connection cannot be established + */ + @Override + public abstract void connect() throws IOException; + + /** + * Returns a {@link SseEventReader} that can be used to receive + * events based on the {@link SseEventType}. + *

+ * If this method is invoked before a connection is established successfully, + * then an IOException is thrown. + * + * @return SseEventReader to receive events + * @throws IOException if invoked before the connection is opened + */ + public abstract SseEventReader getEventReader() throws IOException; + + /** + * Returns {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). The default option is + * {@link HttpRedirectPolicy#NONE}. + * + * @return HttpRedirectOption indicating the + */ + public abstract HttpRedirectPolicy getFollowRedirect(); + + /** + * Returns the retry timeout in milliseconds. The default is 3000ms. + * + * @return timeout interval in milliseconds + */ + public abstract long getRetryTimeout(); + + /** + * Sets {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). + * + * @param option HttpRedirectOption to used for following the + * redirects + */ + public abstract void setFollowRedirect(HttpRedirectPolicy option); + + /** + * Sets the retry timeout interval specified in milliseconds. + * + * @param millis retry timeout + */ + public abstract void setRetryTimeout(long millis); +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/SseURLConnectionImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/SseURLConnectionImpl.java new file mode 100755 index 0000000..937344e --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/SseURLConnectionImpl.java @@ -0,0 +1,281 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.URI; +import java.net.URL; +import java.security.Permission; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; + +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.sse.SseEventReader; + +public class SseURLConnectionImpl extends SseURLConnection { + private static final String _CLASS_NAME = SseURLConnectionImpl.class.getName(); + private static final Logger _LOG = Logger.getLogger(_CLASS_NAME); + + private SseEventSourceImpl _eventSource; + + public SseURLConnectionImpl(URL url) { + super(url); + _eventSource = new SseEventSourceImpl(URI.create(url.toString())); + } + + @Override + public void close() throws IOException { + _eventSource.close(); + } + + @Override + public void connect() throws IOException { + _LOG.entering(_CLASS_NAME, "connect"); + _eventSource.connect(); + } + + @Override + public SseEventReader getEventReader() throws IOException { + return _eventSource.getEventReader(); + } + + + @Override + public HttpRedirectPolicy getFollowRedirect() { + return _eventSource.getFollowRedirect(); + } + + @Override + public long getRetryTimeout() { + return _eventSource.getRetryTimeout(); + } + + @Override + public void setFollowRedirect(HttpRedirectPolicy option) { + _eventSource.setFollowRedirect(option); + } + + @Override + public void setRetryTimeout(long millis) { + _eventSource.setRetryTimeout(millis); + } + + // --------------- Unsupported URLConnection Methods ---------------------- + @Override + public void addRequestProperty(String key, String value) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getConnectTimeout() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setConnectTimeout(int timeout) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getReadTimeout() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setReadTimeout(int timeout) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + // @Override -- Not available in JDK 6. + public long getContentLengthLong() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getContentLength() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getContentType() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getContentEncoding() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getExpiration() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getDate() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getLastModified() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getHeaderField(String name) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Map> getHeaderFields() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getHeaderFieldInt(String name, int Default) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + // @Override -- Not available in JDK 6. + public long getHeaderFieldLong(String name, long Default) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getHeaderFieldDate(String name, long Default) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getHeaderFieldKey(int n) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getHeaderField(int n) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Object getContent() throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @SuppressWarnings("rawtypes") + @Override + public Object getContent(Class[] classes) throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Permission getPermission() throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getDoInput() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setDoInput(boolean doinput) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getDoOutput() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setDoOutput(boolean dooutput) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getAllowUserInteraction() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setAllowUserInteraction(boolean allowuserinteraction) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getUseCaches() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setUseCaches(boolean usecaches) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getIfModifiedSince() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setIfModifiedSince(long ifmodifiedsince) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getDefaultUseCaches() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setDefaultUseCaches(boolean defaultusecaches) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getRequestProperty(String key) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Map> getRequestProperties() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setRequestProperty(String key, String value) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public InputStream getInputStream() throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public OutputStream getOutputStream() throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSource.java b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSource.java new file mode 100755 index 0000000..4a37cf5 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSource.java @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.legacy; + +import java.io.IOException; +import java.util.logging.Logger; + + + +/* +import org.kaazing.gateway.client.impl.sse.EventStream; +import org.kaazing.gateway.client.impl.sse.EventStreamListener; +*/ +/** + * EventSource provides an implementation of HTML5 Server-sent Events. Refer to HTML5 EventSource at {@link http + * ://www.whatwg.org/specs/web-apps/current-work/#server-sent-events} {@link http + * ://www.whatwg.org/specs/web-apps/current-work/#the-event-source} + */ +public class EventSource { + private static final String CLASS_NAME = EventSource.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private EventSource _delegate; + + /** + * State of the Event Source. CONNECTING = 0, OPEN = 1 and CLOSED = 2 + */ + public enum ReadyState { + CONNECTING, OPEN, CLOSED + }; + + /** + * EventSource provides a text-based stream abstraction for Java + */ + public EventSource() { + LOG.entering(CLASS_NAME, ""); + } + + /** + * The ready state indicates the stream status, Possible values are 0 (CONNECTING), 1 (OPEN) and 2 (CLOSED) + * + * @return current state + */ + public ReadyState getReadyState() { + return _getDelegate().getReadyState(); + } + + + /** + * Connects the EventSource instance to the stream location. + * + * @param eventSourceUrl + * stream location + * @throws IOException + * on error + */ + public void connect(String eventSourceUrl) throws IOException { + LOG.entering(CLASS_NAME, "connect", eventSourceUrl); + _getDelegate().connect(eventSourceUrl); + } + + /** + * Disconnects the stream. + */ + public void disconnect() { + LOG.entering(CLASS_NAME, "disconnect"); + _getDelegate().disconnect(); + } + + /** + * Register a listener for EventSource events + * + * @param listener + */ + public void addEventSourceListener(EventSourceListener listener) { + LOG.entering(CLASS_NAME, "addEventSourceListener", listener); + _getDelegate().addEventSourceListener(listener); + } + + /** + * Removes the given EventSource listener from the listener list. + * + * @param listener + * EventSourceListener to be unregistered + */ + public void removeEventSourceListener(EventSourceListener listener) { + LOG.entering(CLASS_NAME, "removeEventSourceListener", listener); + _getDelegate().removeEventSourceListener(listener); + } + + private EventSource _getDelegate() { + if (_delegate != null) { + return _delegate; + } + + try { + _delegate = (EventSource)Class.forName("org.kaazing.net.sse.impl.legacy.EventSourceImpl").newInstance(); + } catch (Exception e) { + throw new Error("Cannot instantiate default EventSourceImpl"); + } + + return _delegate; + } +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceAdapter.java b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceAdapter.java new file mode 100755 index 0000000..8fd4fed --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceAdapter.java @@ -0,0 +1,47 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.legacy; + + +/** + * An adapter class for {@link EventSourceListener}. Use this as a base class to + * override selected methods only. + * + */ +public class EventSourceAdapter implements EventSourceListener { + + @Override + public void onError(EventSourceEvent error) { + + } + + @Override + public void onMessage(EventSourceEvent message) { + + } + + @Override + public void onOpen(EventSourceEvent open) { + + } + +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceEvent.java b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceEvent.java new file mode 100755 index 0000000..186ef94 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceEvent.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.legacy; + +import java.util.EventObject; +import java.util.logging.Logger; + +/** + * This class represents events generated by the EventSource object + * + */ +public class EventSourceEvent extends EventObject { + private static final String CLASS_NAME = EventSourceEvent.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private static final long serialVersionUID = -3654347840399311101L; + private final Type type; + private final String data; + + /** + * Type of the EventSourceEvent. + */ + public enum Type { + OPEN, MESSAGE, ERROR + } + + /** + * + * @param source + * @param type + */ + public EventSourceEvent(Object source, Type type) { + this(source, type, null); + } + + public EventSourceEvent(Object source, Type type, String message) { + super(source); + LOG.entering(CLASS_NAME, "", new Object[] { source, type, message }); + this.type = type; + this.data = message; + } + + /** + * Get type of the EventSourceEvent OPEN, MESSAGE or ERROR + * + * @return the EventSourceEvent type + */ + public Type getType() { + LOG.exiting(CLASS_NAME, "getType", type); + return type; + } + + /** + * Returns the message data delivered by the event source + * + * @return the message data + */ + public String getData() { + LOG.exiting(CLASS_NAME, "getData", data); + return data; + } + +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceImpl.java new file mode 100755 index 0000000..2213fbb --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceImpl.java @@ -0,0 +1,180 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.legacy; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.net.sse.impl.SseEventStream; +import org.kaazing.net.sse.impl.SseEventStreamListener; + + + +/** + * EventSourceImpl provides an implementation of HTML5 Server-sent Events. Refer + * to HTML5 EventSource at {@link http + * ://www.whatwg.org/specs/web-apps/current-work/#server-sent-events} {@link http + * ://www.whatwg.org/specs/web-apps/current-work/#the-event-source} + */ +public class EventSourceImpl extends EventSource { + private static final String CLASS_NAME = EventSourceImpl.class.getName(); + private static final Logger LOG = Logger.getLogger(CLASS_NAME); + + private SseEventStream stream = null; + private final List listeners = new ArrayList(); + + /** + * EventSource provides a text-based stream abstraction for Java + */ + public EventSourceImpl() { + LOG.entering(CLASS_NAME, ""); + } + + /** + * The ready state indicates the stream status, Possible values are 0 (CONNECTING), 1 (OPEN) and 2 (CLOSED) + * + * @return current state + */ + public ReadyState getReadyState() { + if (stream == null) { + return EventSource.ReadyState.CONNECTING; + } + else { + switch (stream.getReadyState()) { + case CONNECTING: + return EventSource.ReadyState.CONNECTING; + + case OPEN: + return EventSource.ReadyState.OPEN; + + case CLOSING: + case CLOSED: + default: + return EventSource.ReadyState.CLOSED; + } + } + } + + /** + * Connects the EventSource instance to the stream location. + * + * @param eventSourceUrl + * stream location + * @throws IOException + * on error + */ + public void connect(String eventSourceUrl) throws IOException { + LOG.entering(CLASS_NAME, "connect", eventSourceUrl); + if (stream != null) { + LOG.warning("Reusing the same event source for a differnt URL, please create a new EventSource object"); + throw new IllegalArgumentException( + "Reusing the same event source for a differnt URL, please create a new EventSource object"); + } + stream = new SseEventStream(eventSourceUrl); + stream.setListener(eventStreamListener); + stream.connect(); + } + + /** + * Disconnects the stream. + */ + public void disconnect() { + LOG.entering(CLASS_NAME, "disconnect"); + stream.stop(); + stream = null; + } + + /** + * Register a listener for EventSource events + * + * @param listener + */ + public void addEventSourceListener(EventSourceListener listener) { + LOG.entering(CLASS_NAME, "addEventSourceListener", listener); + if (listener == null) { + throw new NullPointerException("listener"); + } + listeners.add(listener); + } + + /** + * Removes the given EventSource listener from the listener list. + * + * @param listener + * EventSourceListener to be unregistered + */ + public void removeEventSourceListener(EventSourceListener listener) { + LOG.entering(CLASS_NAME, "removeEventSourceListener", listener); + if (listener == null) { + throw new NullPointerException("listener"); + } + listeners.remove(listener); + } + + private SseEventStreamListener eventStreamListener = new SseEventStreamListener() { + + @Override + public void streamOpened() { + LOG.entering(CLASS_NAME, "streamOpened"); + + EventSourceEvent event = new EventSourceEvent(this, EventSourceEvent.Type.OPEN); + for (EventSourceListener listener : listeners) { + try { + listener.onOpen(event); + } catch (RuntimeException e) { + LOG.logp(Level.WARNING, CLASS_NAME, "onOpen", "Application threw an exception during onOpen: "+e.getMessage(), e); + } + } + } + + @Override + public void messageReceived(String eventName, String message) { + LOG.entering(CLASS_NAME, "fireMessageListeners", message); + + EventSourceEvent event = new EventSourceEvent(this, EventSourceEvent.Type.MESSAGE, message); + for (EventSourceListener listener : listeners) { + try { + listener.onMessage(event); + } catch (RuntimeException e) { + LOG.logp(Level.WARNING, CLASS_NAME, "onMessage", "Application threw an exception during onMessage: "+e.getMessage(), e); + } + } + } + + @Override + public void streamErrored(Exception exception) { + LOG.entering(CLASS_NAME, "fireErrorListeners"); + + EventSourceEvent event = new EventSourceEvent(this, EventSourceEvent.Type.ERROR); + for (EventSourceListener listener : listeners) { + try { + listener.onError(event); + } catch (RuntimeException e) { + LOG.logp(Level.WARNING, CLASS_NAME, "onError", "Application threw an exception during onError: "+e.getMessage(), e); + } + } + } + }; +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceListener.java b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceListener.java new file mode 100755 index 0000000..27e97b7 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/legacy/EventSourceListener.java @@ -0,0 +1,53 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.legacy; + +import java.util.EventListener; + +/** + * Interface for the listener listening to events on the EventSource object + * + */ +public interface EventSourceListener extends EventListener{ + + /** + * Called when the EventSource is opened + * + * @param open EventSourceEvent of type OPEN + */ + public void onOpen(EventSourceEvent open); + + /** + * Called on the receipt of a message from the EventSource + * + * @param message EventSourceEvent of type MESSAGE + */ + public void onMessage(EventSourceEvent message); + + /** + * Called on the receipt of an error from the EventSource + * + * @param error EventSourceEvent of type ERROR + */ + public void onError(EventSourceEvent error); + +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerFactorySpiImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerFactorySpiImpl.java new file mode 100755 index 0000000..d4aa698 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerFactorySpiImpl.java @@ -0,0 +1,50 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.url; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import java.net.URLStreamHandler; +import java.util.Collection; + +import org.kaazing.net.URLStreamHandlerFactorySpi; + +public class SseURLStreamHandlerFactorySpiImpl extends URLStreamHandlerFactorySpi { + private static final Collection _supportedProtocols = unmodifiableList(asList("sse")); + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if (!_supportedProtocols.contains(protocol)) { + String s = String.format("Protocol not supported '%s'", protocol); + throw new IllegalArgumentException(s); + } + + return new SseURLStreamHandlerImpl(protocol); + } + + @Override + public Collection getSupportedProtocols() { + return _supportedProtocols; + } + +} diff --git a/android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerImpl.java b/android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerImpl.java new file mode 100755 index 0000000..54f1f54 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/sse/impl/url/SseURLStreamHandlerImpl.java @@ -0,0 +1,79 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.sse.impl.url; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLConnection; +import java.net.URLStreamHandler; + +import org.kaazing.net.sse.impl.SseURLConnectionImpl; + +public class SseURLStreamHandlerImpl extends URLStreamHandler { + + private String _scheme; + + public SseURLStreamHandlerImpl(String scheme) { + _scheme = scheme; + } + + @Override + protected URLConnection openConnection(URL location) throws IOException { + return new SseURLConnectionImpl(location); + } + + @Override + protected int getDefaultPort() { + return 80; + } + + @Override + protected void parseURL(URL location, String spec, int start, int limit) { + _scheme = spec.substring(0, spec.indexOf("://")); + + URI specURI = _getSpecURI(spec); + String host = specURI.getHost(); + int port = specURI.getPort(); + String authority = specURI.getAuthority(); + String userInfo = specURI.getUserInfo(); + String path = specURI.getPath(); + String query = specURI.getQuery(); + + setURL(location, _scheme, host, port, authority, userInfo, path, query, null); + } + + // ----------------- Private Methods ----------------------------------- + // Creates a URI that can be used to retrieve various parts such as host, + // port, etc. Based on whether the scheme includes ':', the method returns + // the appropriate URI that can be used to retrieve the needed parts. + private URI _getSpecURI(String spec) { + URI specURI = URI.create(spec); + + if (_scheme.indexOf(':') == -1) { + return specURI; + } + + String schemeSpecificPart = specURI.getSchemeSpecificPart(); + return URI.create(schemeSpecificPart); + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocket.java b/android/src/main/java/org/kaazing/net/ws/WebSocket.java new file mode 100755 index 0000000..4b43403 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocket.java @@ -0,0 +1,433 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.util.Collection; + +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.ws.WebSocketExtension.Parameter; + + +/** + * WebSocket provides bi-directional communications for text and binary + * messaging via the Kaazing Gateway. + *

+ * Refer to {@link http://www.w3.org/TR/websockets/} for the published standard + * W3C WebSocket API specification. + *

+ * Instances of {@link WebSocket} can be created using + * {@link WebSocketFactory#createWebSocket(java.net.URI, String...)} API. + */ +public abstract class WebSocket implements Closeable { + /** + * Disconnects with the server. This is a blocking call that returns only + * when the shutdown is complete. If the connection is already closed, then + * this method has no effect. + * + * @throws IOException if the disconnect did not succeed + */ + public abstract void close() throws IOException; + + /** + * Disconnects with the server with code. This is a blocking + * call that returns only when the shutdown is complete. If the connection + * is already closed, then this method has no effect. + * + * @param code the error code for closing + * @throws IOException if the disconnect did not succeed + * @throws IllegalArgumentException if the code isn't 1000 or out of + * range 3000 - 4999. + */ + public abstract void close(int code) + throws IOException; + + /** + * Disconnects with the server with code and reason. This is a blocking + * call that returns only when the shutdown is complete. If the connection + * is already closed, then this method has no effect. + * + * @param code the error code for closing + * @param reason the reason for closing + * @throws IOException if the disconnect did not succeed + * @throws IllegalArgumentException if the code isn't 1000 or out of + * range 3000 - 4999 OR if the reason + * is more than 123 bytes + */ + public abstract void close(int code, String reason) + throws IOException; + + /** + * Connects with the server using an endpoint. This is a blocking call. The + * thread invoking this method will be blocked till a successful connection + * is established. + *

+ * If the connection cannot be established, then an IOException is thrown + * and the thread is unblocked. + *

+ * An IllegalStateException is thrown if required parameters of an enabled + * extension has not been set/enabled. + * + * @throws IOException if the connection cannot be established + * @throws IllegalStateException if the required parameters of an enabled + * extension are not set/enabled + */ + public abstract void connect() throws IOException; + + /** + * Gets the {@link ChallengeHandler} that is used during authentication + * both at the connect-time as well as at subsequent revalidation-time that + * occurs at regular intervals. + * + * @return ChallengeHandler + */ + public abstract ChallengeHandler getChallengeHandler(); + + /** + * Gets the connect timeout in milliseconds. The timeout will expire if + * there is no exchange of packets(for example, 100% packet loss) while + * establishing the connection. A timeout value of zero indicates + * no timeout. Default connect timeout is zero. + * + * @return connect timeout value in milliseconds + */ + public abstract int getConnectTimeout(); + + /** + * Gets the names of all the extensions that have been enabled for this + * connection. The enabled extensions are negotiated between the client + * and the server during the handshake only if all the required parameters + * belonging to the extension have been set as enabled parameters. The + * names of the negotiated extensions can be obtained using + * {@link #getNegotiatedExtensions()} API. An empty Collection is returned + * if no extensions have been enabled for this connection. The enabled + * extensions will be a subset of the supported extensions. + * + * @return Collection names of the enabled extensions for this + * connection + */ + public abstract Collection getEnabledExtensions(); + + /** + * Gets the value of the specified {@link Parameter} defined in an enabled + * extension. If the parameter is not defined for this connection but a + * default value for the parameter is set using the method + * {@link WebSocketFactory#setDefaultParameter(Parameter, Object)}, + * then the default value is returned. + *

+ * Setting the parameter value when the connection is successfully + * established will result in an IllegalStateException. + *

+ * @param Generic type of the value of the Parameter + * @param parameter Parameter whose value needs to be set + * @return the value of the specified parameter + * @throw IllegalStateException if this method is invoked after connect() + */ + public abstract T getEnabledParameter(Parameter parameter); + + /** + * Gets the names of all the protocols that are enabled for this + * connection. Returns an empty Collection if protocols are not enabled. + * + * @return Collection supported protocols by this connection + */ + public abstract Collection getEnabledProtocols(); + + /** + * Returns {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (status code 3xx). The default options is + * {@link HttpRedirectPolicy.ALWAYS}. + * + * @return HttpRedirectOption option for following redirects(status + * code 3XX) + */ + public abstract HttpRedirectPolicy getRedirectPolicy(); + + /** + * Returns the {@link InputStream} to receive binary messages. The + * methods on {@link InputStream} will block till the message arrives. The + * {@link InputStream} must be used to only receive binary + * messages. + *

+ * An IOException is thrown if this method is invoked when the connection + * has not been established. Receiving a text message using the + * {@link InputStream} will result in an IOException. + *

+ * Once the connection is closed, a new {@link InputStream} should be + * obtained using this method after the connection has been established. + * Using the old InputStream will result in an IOException. + *

+ * @return InputStream to receive binary messages + * @throws IOException if the method is invoked before the connection is + * successfully opened; if a text message is being + * read using the InputStream + */ + public abstract InputStream getInputStream() throws IOException; + + /** + * Returns a {@link WebSocketMessageReader} that can be used to receive + * binary and text messages based on the + * {@link WebSocketMessageType}. + *

+ * If this method is invoked before a connection is established successfully, + * then an IOException is thrown. + *

+ * Once the connection is closed, a new {@link WebSocketMessageReader} + * should be obtained using this method after the connection has been + * established. Using the old WebSocketMessageReader will result in an + * IOException. + *

+ * @return WebSocketMessageReader to receive binary and text messages + * @throws IOException if invoked before the connection is opened + */ + public abstract WebSocketMessageReader getMessageReader() throws IOException; + + /** + * Returns a {@link WebSocketMessageWriter} that can be used to send + * binary and text messages. + *

+ * If this method is invoked before a connection is established + * successfully, then an IOException is thrown. + *

+ * Once the connection is closed, a new {@link WebSocketMessageWriter} + * should be obtained using this method after the connection has been + * established. Using the old WebSocketMessageWriter will result in an + * IOException. + *

+ * @return WebSocketMessageWriter to send binary and text messages + * @throws IOException if invoked before the connection is opened + */ + public abstract WebSocketMessageWriter getMessageWriter() throws IOException; + + /** + * Gets names of all the enabled extensions that have been successfully + * negotiated between the client and the server during the initial + * handshake. + *

+ * Returns an empty Collection if no extensions were negotiated between the + * client and the server. The negotiated extensions will be a subset of the + * enabled extensions. + *

+ * If this method is invoked before a connection is successfully established, + * an IllegalStateException is thrown. + * + * @return Collection successfully negotiated using this + * connection + * @throws IllegalStateException if invoked before the {@link #connect()} + * completes + */ + public abstract Collection getNegotiatedExtensions(); + + /** + * Returns the value of the specified {@link Parameter} of a negotiated + * extension. + *

+ * If this method is invoked before the connection is successfully + * established, an IllegalStateException is thrown. + *

+ * Once the connection is closed, the negotiated parameters are cleared. + * Trying to retrieve the value will result in an IllegalStateException. + *

+ * @param parameter type + * @param parameter parameter of a negotiated extension + * @return T value of the specified parameter + * @throws IllegalStateException if invoked before the {@link #connect()} + * completes + */ + public abstract T getNegotiatedParameter(Parameter parameter); + + /** + * Gets the protocol that the client and the server have successfully + * negotiated. + *

+ * If this method is invoked before the connection is successfully + * established, an IllegalStateException is thrown. + *

+ * Once the connection is closed, trying to retrieve the negotiated + * protocol will result in an IllegalStateException. + *

+ * @return protocol negotiated by the client and the server + * @throws IllegalStateException if invoked before the {@link #connect()} + * completes + */ + public abstract String getNegotiatedProtocol(); + + /** + * Returns the {@link OutputStream} to send binary messages. The + * message is put on the wire only when {@link OutputStream#flush()} is + * invoked. + *

+ * If this method is invoked before {@link #connect()} is complete, an + * IOException is thrown. + *

+ * Once the connection is closed, a new {@link OutputStream} should + * be obtained using this method after the connection has been + * established. Using the old OutputStream will result in IOException. + *

+ * @return OutputStream to send binary messages + * @throws IOException if the method is invoked before the connection is + * successfully opened + */ + public abstract OutputStream getOutputStream() throws IOException; + + /** + * Returns a {@link Reader} to receive text messages from this + * connection. This method should be used to only to receive text + * messages. Methods on {@link Reader} will block till a message arrives. + *

+ * If the Reader is used to receive binary messages, then an + * IOException is thrown. + *

+ * If this method is invoked before a connection is established + * successfully, then an IOException is thrown. + *

+ * Once the connection is closed, a new {@link Reader} should be obtained + * using this method after the connection has been established. Using the + * old Reader will result in an IOException. + *

+ * @return Reader used to receive text messages from this connection + * @throws IOException if the method is invoked before the connection is + * successfully opened + */ + public abstract Reader getReader() throws IOException; + + /** + * Returns the names of supported extensions that have been discovered. An + * empty Collection is returned if no extensions were discovered. + * + * @return Collection extension names discovered for this + * connection + */ + public abstract Collection getSupportedExtensions(); + + /** + * Returns a {@link Writer} to send text messages from this + * connection. The message is put on the wire only when + * {@link Writer#flush()} is invoked. + *

+ * An IOException is thrown if this method is invoked when the connection + * has not been established. + *

+ * Once the connection is closed, a new {@link Writer} should be obtained + * using this method after the connection has been established. Using the + * old Writer will result in an IOException. + *

+ * @return Writer used to send text messages from this connection + * @throws IOException if the method is invoked before the connection is + * successfully opened + */ + public abstract Writer getWriter() throws IOException; + + /** + * Sets the {@link ChallengeHandler} that is used during authentication + * both at the connect-time as well as at subsequent revalidation-time that + * occurs at regular intervals. + * + * @param challengeHandler ChallengeHandler used for authentication + */ + public abstract void setChallengeHandler(ChallengeHandler challengeHandler); + + /** + * Sets the connect timeout in milliseconds. The timeout will expire if + * there is no exchange of packets(for example, 100% packet loss) while + * establishing the connection. A timeout value of zero indicates + * no timeout. + * + * @param connectTimeout timeout value in milliseconds + * @throws IllegalStateException if the connect timeout is being set + * after the connection has been established + * @throws IllegalArgumentException if connectTimeout is negative + */ + public abstract void setConnectTimeout(int connectTimeout); + + /** + * Registers the names of all the extensions that must be negotiated between + * the client and the server during the handshake. This method must be + * invoked before invoking the {@link #connect()} method. The + * enabled extensions should be a subset of the supported extensions. Only + * the extensions that are explicitly enabled are put on the wire even + * though there could be more supported extensions on this connection. + *

+ * If this method is invoked after connection is successfully established, + * an IllegalStateException is thrown. If an enabled extension is not + * discovered as a supported extension, then IllegalStateException is thrown. + *

+ * @param extensions list of extensions to be negotiated with the server + * during the handshake + * @throw IllegalStateException if this method is invoked after successful + * connection or any of the specified + * extensions is not a supported extension + */ + public abstract void setEnabledExtensions(Collection extensions); + + /** + * Sets the value of the specified {@link Parameter} defined in an enabled + * extension. The application developer should set the extension + * parameters of the enabled extensions before invoking the + * {@link #connect()} method. + *

+ * Setting the parameter value when the connection is successfully + * established will result in an IllegalStateException. + *

+ * If the parameter has a default value that was specified using + * {@link WebSocketFactory#setDefaultParameter(Parameter, Object)}, + * then setting the same parameter using this method will override the + * default value. + *

+ * @param extension parameter type + * @param parameter Parameter whose value needs to be set + * @param value of the specified parameter + * @throw IllegalStateException if this method is invoked after connect() + */ + public abstract void setEnabledParameter(Parameter parameter, + T value); + + /** + * Registers the protocols to be negotiated with the server during the + * handshake. This method must be invoked before {@link #connect()} is + * called. + *

+ * If this method is invoked after a connection has been successfully + * established, an IllegalStateException is thrown. + *

+ * @param extensions list of extensions to be negotiated with the server + * during the handshake + * @throw IllegalStateException if this method is invoked after connect() + */ + public abstract void setEnabledProtocols(Collection protocols); + + /** + * Sets {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). + * + * @param option HttpRedirectOption to used for following the + * redirects + */ + public abstract void setRedirectPolicy(HttpRedirectPolicy option); + +} diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocketException.java b/android/src/main/java/org/kaazing/net/ws/WebSocketException.java new file mode 100755 index 0000000..19bf720 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocketException.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class WebSocketException extends IOException { + private static final long serialVersionUID = 1L; + private static final Map _STATUS_CODES; + + static { + _STATUS_CODES = new HashMap(); + _STATUS_CODES.put(1000, "Connection has been closed normally"); + _STATUS_CODES.put(1001, "End-point is going away"); + _STATUS_CODES.put(1002, "Connection terminated due to protocol error"); + _STATUS_CODES.put(1003, "Connection terminated due to incorrect message type"); + _STATUS_CODES.put(1004, "Reserved for future use"); + _STATUS_CODES.put(1005, "No status code was present"); + _STATUS_CODES.put(1006, "Connection was closed abnormally, e.g., without sending or receiving a Close control frame."); + _STATUS_CODES.put(1007, "Connection terminated due to inconsistency between the data and the message type"); + _STATUS_CODES.put(1008, "Connection terminated as the received a message violates the policy"); + _STATUS_CODES.put(1009, "Connection terminated as the received message is too big to process"); + _STATUS_CODES.put(1010, "Connection terminated by the client because an extension could not be negotiated with the server during the handshake"); + _STATUS_CODES.put(1011, "Connection terminated by the server because of an unexpected condition"); + _STATUS_CODES.put(1015, "Connection was closed due to a failure to perform a TLS handshake"); + } + + private int _code = 0; + + public WebSocketException(String reason) { + super(reason); + } + + public WebSocketException(Exception ex) { + super(ex); + } + + public WebSocketException(String reason, Exception ex) { + super(reason, ex); + } + + public WebSocketException(int code, String reason) { + super((_STATUS_CODES.get(code) == null) ? reason : _STATUS_CODES.get(code)); + _code = code; + } + + public WebSocketException(int code, String reason, Exception ex) { + super((_STATUS_CODES.get(code) == null) ? reason : _STATUS_CODES.get(code), + ex); + _code = code; + } + + public int getCode() { + return _code; + } + + public String getReason() { + String s = _STATUS_CODES.get(_code); + if (s == null) { + return super.getMessage(); + } + + return s; + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocketExtension.java b/android/src/main/java/org/kaazing/net/ws/WebSocketExtension.java new file mode 100755 index 0000000..e08e30c --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocketExtension.java @@ -0,0 +1,315 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.Map; + +import org.kaazing.net.ws.WebSocketExtension.Parameter.Metadata; + +/** + * {@link WebSocketExtension} should be extended to define or register + * {@link Parameter}s constants that will be used by the + * application developers. + */ +public abstract class WebSocketExtension { + private static final Map _extensions; + + static { + _extensions = new HashMap(); + }; + + private Collection> _parameters; + + /** + * Creates an {@link WebSocketExtensionParamter} of the specified type. + * + * @param generic parameter type + * @param parameterName name of the parameter + * @param parameterType Class object representing the parameter type + * @param parameterMetadata characteristics of the parameter + * @return Parameter of the specified type + */ + protected Parameter createParameter(String parameterName, + Class parameterType, + EnumSet parameterMetadata) { + if ((parameterName == null) || (parameterName.trim().length() == 0)) { + String s = "Parameter name cannot be null or empty"; + throw new IllegalArgumentException(s); + } + + if (parameterType == null) { + String s = String.format("Null type specified for parameter '%s'", + parameterName); + throw new IllegalArgumentException(s); + } + + Parameter parameter = new Parameter(this, + parameterName, + parameterType, + parameterMetadata); + _parameters.add(parameter); + + return parameter; + } + + /** + * Protected constructor to be invoked by the sub-class constructor. + * + * @param name name of the WebSocketExtension + */ + protected WebSocketExtension() { + _parameters = new ArrayList>(); + _extensions.put(name(), this); + } + + /** + * Returns the {@link WebSocketExtension} with the specified name. A null + * is returned if there are no extensions with the specified name. + * + * @param name name of the WebSocketExtension + * @return WebSocketExtension + */ + public static WebSocketExtension getWebSocketExtension(String name) { + return _extensions.get(name); + } + + /** + * Returns the {@link Parameter} defined in this + * {@link WebSoketExtension} with the specified name. + * + * @param name parameter's name + * @return Parameter + */ + public Parameter getParameter(String name) { + Collection> extnParameters = getParameters(); + + for (Parameter extnParameter : extnParameters) { + if (extnParameter.name().equals(name)) { + return extnParameter; + } + } + + return null; + } + + /** + * Returns all the {@link Parameter}s that are defined in this + * {@link WebSocketExtension}. An empty Collection is returned if there + * are no {@link Parameter}s defined. + * + * @return Collection of WebSocketExtensionParameters + */ + public Collection> getParameters() { + if (_parameters == null) { + return Collections.>emptyList(); + } + + return Collections.unmodifiableCollection(_parameters); + } + + /** + * Returns {@link Parameter}s defined in this {@link WebSocketExtension} + * that match all the specified characteristics. An empty Collection is + * returned if none of the {@link Parameter}s defined in this + * {@link WebSocketExtension} match all the specified characteristics. + * + * @return Collection of WebSocketExtensionParameters + */ + public Collection> getParameters(Metadata... characteristics) { + if ((characteristics == null) || (characteristics.length == 0)) { + return Collections.>emptySet(); + } + + EnumSet metadataSet = null; + int length = characteristics.length; + + if (length == 1) { + metadataSet = EnumSet.of(characteristics[0]); + } + else { + Metadata[] array = new Metadata[length -1]; + + // Start from the second(0-based index) element onwards to populate + // the array. + for (int i = 1; i < length; i++) { + array[i - 1] = characteristics[i]; + } + + metadataSet = EnumSet.of(characteristics[0], array); + } + + Collection> extnParameters = getParameters(); + Collection> result = new ArrayList>(); + + for (Parameter extnParameter : extnParameters) { + EnumSet paramMetadata = extnParameter.metadata(); + if (paramMetadata.containsAll(metadataSet)) { + result.add(extnParameter); + } + } + + return result; + } + + /** + * Returns the name of this {@link WebSocketExtension}. + * @return + */ + public abstract String name() ; + + /** + * {@link Parameter} represents an extension parameter. + * + * @param parameter type + */ + public static final class Parameter { + public enum Metadata { + /** + * Name of a parameter marked as anonymous will not be put on the wire + * during the handshake. By default, a parameter is considered "named" + * and it's name will be put on the wire during the handshake. + */ + ANONYMOUS, + + /** + * Parameters marked as required must be set for the entire extension + * to be negotiated during the handshake. By default, a parameter is + * considered to be optional. + */ + REQUIRED, + + /** + * Parameter marked as temporal will not be negotiated during the + * handshake. + */ + TEMPORAL; + }; + + private final WebSocketExtension _parent; + private final String _parameterName; + private final Class _parameterType; + private final EnumSet _parameterMetadata; + + public Parameter(WebSocketExtension parent, + String name, + Class type, + EnumSet metadata) { + if ((name == null) || (name.trim().length() == 0)) { + String s = String.format("Parameters must have a name"); + throw new IllegalArgumentException(s); + } + + if (parent == null) { + String s = String.format("Null parent specified for " + + "Parameter '%s'", name); + throw new IllegalArgumentException(s); + } + + if ((metadata == null) || metadata.isEmpty()) { + _parameterMetadata = EnumSet.noneOf(Metadata.class); + } + else { + _parameterMetadata = metadata; + } + + _parent = parent; + _parameterName = name; + _parameterType = type; + } + + /** + * Returns the parent {@link WebSocketExtension} that this parameter is + * defined in. + * + * @return String name of the extension + */ + public WebSocketExtension extension() { + return _parent; + } + + /** + * Indicates whether the parameter is anonymous or named. If the parameter + * is anonymous and it is not transient, then it's name is NOT put on the + * wire during the handshake. However, it's value is put on the wire. + * + * @return boolean true if the parameter is anonymous, false if the + * parameter is named + */ + public boolean anonymous() { + return _parameterMetadata.contains(Metadata.ANONYMOUS); + } + + /** + * Returns the metadata characteristics of this extension parameter. The + * returned EnumSet is a clone so any changes to it will not be picked by + * by the extension parameter. + * + * @return EnumSet characteristics of the extension parameter + */ + public EnumSet metadata() { + return _parameterMetadata.clone(); + } + + /** + * Returns the name of the parameter. + * + * @return String name of the parameter + */ + public String name() { + return _parameterName; + } + + /** + * Indicates whether the parameter is required. If the required parameter + * is not set, then the extension is not negotiated during the handshake. + * + * @return boolean true if the parameter is required, otherwise false + */ + public boolean required() { + return _parameterMetadata.contains(Metadata.REQUIRED); + } + + /** + * Indicates whether the parameter is temporal/transient. Temporal + * parameters are not put on the wire during the handshake. + * + * @return boolean true if the parameter is temporal, otherwise false + */ + public boolean temporal() { + return _parameterMetadata.contains(Metadata.TEMPORAL); + } + + /** + * Returns the type of the parameter. + * + * @return Class type of the parameter + */ + public Class type() { + return _parameterType; + } + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocketFactory.java b/android/src/main/java/org/kaazing/net/ws/WebSocketFactory.java new file mode 100755 index 0000000..773fc7b --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocketFactory.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Collection; +import java.util.ServiceLoader; + +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.ws.WebSocketExtension.Parameter; + +/** + * {@link WebSocketFactory} is an abstract class that can be used to create + * {@link WebSocket}s by specifying the end-point and the enabled protocols. + * It may be extended to instantiate particular subclasses of {@link WebSocket} + * and thus provide a general framework for the addition of public + * WebSocket-level functionality. + *

+ * Using {@link WebSocketFactory} instance, application developers can set + * {@link Parameter}s that will be inherited by all the + * {@link WebSocket} instances created from the factory. Application developers + * can override the {@link Parameter}s at the individual + * {@link WebSocket} level too. + */ +public abstract class WebSocketFactory { + + protected WebSocketFactory() { + } + + /** + * Creates and returns a new instance of the default implementation of the + * {@link WebSocketFactory}. + * + * @return WebSocketFactory + */ + public static WebSocketFactory createWebSocketFactory() { + Class clazz = WebSocketFactory.class; + ServiceLoader loader = ServiceLoader.load(clazz); + return loader.iterator().next(); + } + + /** + * Creates a {@link WebSocket} to establish a full-duplex connection to the + * target location. + *

+ * The default extension parameters that were set on the + * {@link WebSocketFactory} prior to this call are inherited by the newly + * newly created {@link WebSocket} instance. + * + * @param location URI of the WebSocket service for the connection + * @throws URISyntaxException + */ + public abstract WebSocket createWebSocket(URI location) + throws URISyntaxException; + + /** + * Creates a {@link WebSocket} to establish a full-duplex connection to the + * target location with one of the specified protocols on a supported + * WebSocket provider. + *

+ * The default extension parameters that were set on the + * {@link WebSocketFactory} prior to this call are inherited by the newly + * newly created {@link WebSocket} instance. + * + * @param location URI of the WebSocket service for the connection + * @param protocols protocols to be negotiated over the WebSocket, or + * null for any protocol + * @throws URISyntaxException + */ + public abstract WebSocket createWebSocket(URI location, + String... protocols) + throws URISyntaxException; + + /** + * Gets the default {@link ChallengeHandler} that is used during + * authentication both at the connect-time as well as at subsequent + * revalidation-time that occurs at regular intervals. + * + * @return the default ChallengeHandler + */ + public abstract ChallengeHandler getDefaultChallengeHandler(); + + /** + * Gets the default connect timeout in milliseconds. Default value of the + * default connect timeout is zero -- which means no timeout. + * + * @return connect timeout value in milliseconds + */ + public abstract int getDefaultConnectTimeout(); + + /** + * Gets the names of the default enabled extensions that will be inherited + * by all the {@link WebSocket}s created using this factory. These + * extensions are negotiated between the client and the server during the + * WebSocket handshake only if all the required parameters belonging to the + * extension have been set as enabled parameters. An empty Collection is + * returned if no extensions have been enabled for this factory. + * + * @return Collection names of the enabled extensions for + */ + public abstract Collection getDefaultEnabledExtensions(); + + /** + * Returns the default {@link HttpRedirectPolicy} that was specified at + * on the factory. The default options is {@link HttpRedirectPolicy.ALWAYS}. + * + * @return HttpRedirectOption + */ + public abstract HttpRedirectPolicy getDefaultRedirectPolicy(); + + /** + * Returns the default value of the specified {@link Parameter}. + * + * @param parameter type + * @param parameter extension parameter + * @return T parameter value of type + */ + public abstract T getDefaultParameter(Parameter parameter); + + /** + * Returns the names of supported extensions that have been discovered. An + * empty Collection is returned if no extensions were discovered. + * + * @return Collection extension names discovered for this factory + */ + public abstract Collection getSupportedExtensions(); + + /** + * Sets the default {@link ChallengeHandler} that is used during + * authentication both at the connect-time as well as at subsequent + * revalidation-time that occurs at regular intervals. All the + * {@link WebSocket}s created using this factory will inherit the default + * ChallengeHandler. + * + * @param challengeHandler default ChallengeHandler + */ + public abstract void setDefaultChallengeHandler(ChallengeHandler challengeHandler); + + /** + * Sets the default connect timeout in milliseconds. The specified + * timeout is inherited by all the WebSocket instances that are created + * using this WebSocketFactory instance. The timeout will expire if there is + * no exchange of packets(for example, 100% packet loss) while establishing + * the connection. A timeout value of zero indicates no timeout. + * + * @param connectTimeout timeout value in milliseconds + * @throws IllegalArgumentException if connectTimeout is negative + */ + public abstract void setDefaultConnectTimeout(int connectTimeout); + + /** + * Registers the names of all the default enabled extensions to be inherited + * by all the {@link WebSocket}s created using this factory. The extensions + * will be negotiated between the client and the server during the WebSocket + * handshake if all the required parameters belonging to the extension have + * been set. The default enabled extensions should be a subset of the + * supported extensions. + *

+ * If an enabled extension is not in the list of supported extensions, then + * IllegalStateException is thrown. + *

+ * @param extensions list of extensions to be inherited by all the + * WebSockets created using this factory + * @throws IllegalStateException if an extension is not in the list of the + * supported extensions. + */ + public abstract void setDefaultEnabledExtensions(Collection extensions); + + /** + * Sets the default {@link HttpRedirectPolicy} that is to be inherited by + * all the {@link WebSocket}s created using this factory instance. + * + * @param option HttpRedirectOption + */ + public abstract void setDefaultRedirectPolicy(HttpRedirectPolicy option); + + /** + * Sets the default value of the specified {@link Parameter} + * that will be inherited by all the {@link WebSocket}s that are created + * using this factory instance. {@link WebSocket}s that were created before + * setting the {@link Parameter} using this API will not + * be able to inherit the default value of the parameter. + * + * @param parameter type + * @param parameter extension parameter whose default value is to be set + * @param value default value of type + */ + public abstract void setDefaultParameter(Parameter parameter, + T value); +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocketMessageReader.java b/android/src/main/java/org/kaazing/net/ws/WebSocketMessageReader.java new file mode 100755 index 0000000..fee5725 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocketMessageReader.java @@ -0,0 +1,124 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.io.IOException; + +import java.nio.ByteBuffer; + +/** + * {@link WebSocketMessageReader} is used to receive binary and text messages. + * A reference to {@link WebSocketMessageReader} can be obtained by using + * {@link WebSocket#getMessageReader()} or + * {@link WsURLConnection#getMessageReader()} methods only after a + * connection has been successfully established. {@link WebSocketMessageReader} + * allows looking at {@link WebSocketMessageType} to figure out whether it's a + * text or a binary message so that appropriate getter methods can be + * subsequently invoked to retrieve the message. + *

+ * Trying to get a reference to {@link WebSocketMessageReader} before the + * connection is established will result in an IOException. + *

+ * Once the connection is closed, a new {@link WebSocketMessageReader} should + * be obtained using the aforementioned methods after the connection has been + * established. Using the old reader will result in IOException. + */ +public abstract class WebSocketMessageReader { + /** + * Returns the payload of the last received message. This method should + * be invoked after {@link #next()} only if the type of the received + * message is {@link WebSocketMessageType#BINARY}. This is not a blocking + * call. + *

+ * A null is returned if this method is invoked before invoking + * {@link #next()} method. + *

+ * If the type of the last received message is not + * {@link WebSocketMessageType#BINARY}, then invoking this method to obtain + * the payload of the message as ByteBuffer will result in an IOException. + *

+ * @return ByteBuffer binary payload of the message + * @throws IOException if the type of the last received message is not + * {@link WebSocketMessageType#BINARY} + */ + public abstract ByteBuffer getBinary() throws IOException; + + /** + * Returns the payload of the last received message. This method should + * be invoked after {@link #next()} only if the type of the received + * message is {@link WebSocketMessageType#TEXT}. This is not a blocking + * call. + *

+ * A null is returned if this method is invoked before invoking + * {@link #next()} method. + *

+ * If the type of the last received message is not + * {@link WebSocketMessageType#TEXT}, then invoking this method to obtain + * the payload of the message as CharSequence will result in an IOException. + *

+ * @return CharSequence text payload of the message + * @throws IOException if the type of the last received message is not + * {@link WebSocketMessageType#TEXT} + */ + public abstract CharSequence getText() throws IOException; + + /** + * Returns the {@link WebSocketMessageType} of the already received message. + * This method returns a null until the first message is received. Note + * that this is not a blocking call. When connected, if this method is + * invoked immediately after {@link #next()}, then they will return the same + * value. + *

+ * Based on the returned {@link WebSocketMessageType}, appropriate read + * methods can be used to receive the message. This method will continue to + * return the same {@link WebSocketMessageType} till the next message + * arrives. When the next message arrives, this method will return the + * the {@link WebSocketMessageType} associated with that message. + *

+ + * @return WebSocketMessageType WebSocketMessageType.TEXT for a text + * message; WebSocketMessageType.BINARY + * for a binary message; WebSocketMessageType.EOS + * if the connection is closed; null before + * the first message + */ + public abstract WebSocketMessageType getType(); + + /** + * Invoking this method will cause the thread to block until a message is + * received. When the message is received, this method returns the type of + * the newly received message. Based on the returned + * {@link WebSocketMessageType}, appropriate getter methods can be used to + * retrieve the binary or text message. When the connection is closed, this + * method returns {@link WebSocketMessageType#EOS}. + *

+ * An IOException is thrown if this method is invoked before the connection + * has been established. + *

+ * @return WebSocketMessageType WebSocketMessageType.TEXT for a text + * message; WebSocketMessageType.BINARY + * for a binary message; WebSocketMessageType.EOS + * if the connection is closed + * @throws IOException if invoked before the connection is established + */ + public abstract WebSocketMessageType next() throws IOException; +} diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocketMessageType.java b/android/src/main/java/org/kaazing/net/ws/WebSocketMessageType.java new file mode 100755 index 0000000..eb70948 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocketMessageType.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +/** + * {@link WebSocketMessageType} represents the types of the messages that are + * received by the {@link WebSocketMessageReader}. WebSocketMessageType.EOS + * represents end-of-stream. + */ +public enum WebSocketMessageType { + EOS, TEXT, BINARY; +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/ws/WebSocketMessageWriter.java b/android/src/main/java/org/kaazing/net/ws/WebSocketMessageWriter.java new file mode 100755 index 0000000..f04e736 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WebSocketMessageWriter.java @@ -0,0 +1,61 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.io.IOException; + +import java.nio.ByteBuffer; + +/** + * {@link WebSocketMessageWriter} is used to send binary and text messages. A + * reference to {@link WebSocketMessageWriter} is obtained by invoking either + * {@link WebSocket#getMessageWriter()} or + * {@link WsURLConnection#getMessageWriter() methods after the connection has + * been established. Trying to get a reference to {@link WebSocketMessageWriter} + * before the connection is established will result in an IOException. + *

+ * Once the connection is closed, a new {@link WebSocketMessageReader} should + * be obtained using the aforementioned methods after the connection has been + * established. Using the old reader will result in IOException. + */ +public abstract class WebSocketMessageWriter { + + /** + * Sends a text message using the specified payload. Trying to write + * after the underlying connection has been closed will result in an + * IOException. + * + * @param src CharSequence payload of the message + * @throws IOException if the connection is not open or if the connection + * has been closed + */ + public abstract void writeText(CharSequence src) throws IOException; + + /** + * Sends a binary message using the specified payload. + * + * @param src ByteBuffer payload of the message + * @throws IOException if the connection is not open or if the connection + * has been closed + */ + public abstract void writeBinary(ByteBuffer src) throws IOException; +} diff --git a/android/src/main/java/org/kaazing/net/ws/WsURLConnection.java b/android/src/main/java/org/kaazing/net/ws/WsURLConnection.java new file mode 100755 index 0000000..4351cc2 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/WsURLConnection.java @@ -0,0 +1,425 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URL; +import java.net.URLConnection; +import java.util.Collection; + +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.ws.WebSocketExtension.Parameter; + +/** + * A URLConnection with support for WebSocket-specific features. See + * {@link http://www.w3.org/TR/websockets/} for details. + *

+ * Each WsURLConnection provides bi-directional communications for text and + * binary messaging via the Kaazing Gateway. + *

+ * An instance of {@link WsURLConnection} is created as shown below: + *

+ * {@code 
+ *     URL location = URLFactory.create("ws://:/");
+ *     URLConnection connection = location.openConnection();
+ *     WsURLConnection wsConnection = (WsURLConnection)connection;
+ * }
+ * 
+ */ +public abstract class WsURLConnection extends URLConnection { + + protected WsURLConnection(URL url) { + super(url); + } + + /** + * Disconnects with the server. This is a blocking call that returns only + * when the shutdown is complete. + * + * @throws IOException if the disconnect did not succeed + */ + public abstract void close() throws IOException; + + /** + * Disconnects with the server with code. This is a blocking + * call that returns only when the shutdown is complete. + * + * @param code the error code for closing + * @throws IOException if the disconnect did not succeed + * @throws IllegalArgumentException if the code isn't 1000 or out of + * range 3000 - 4999. + */ + public abstract void close(int code) throws IOException; + + /** + * Disconnects with the server with code and reason. This is a blocking + * call that returns only when the shutdown is complete. + * + * @param code the error code for closing + * @param reason the reason for closing + * @throws IOException if the disconnect did not succeed + * @throws IllegalArgumentException if the code isn't 1000 or out of + * range 3000 - 4999 OR if the reason + * is more than 123 bytes + */ + public abstract void close(int code, String reason) + throws IOException; + + /** + * Connects with the server using an end-point. This is a blocking call. The + * thread invoking this method will be blocked till a successful connection + * is established. If the connection cannot be established, then an + * IOException is thrown and the thread is unblocked. + * + * @throws IOException if the connection cannot be established + */ + @Override + public abstract void connect() throws IOException; + + /** + * Gets the {@link ChallengeHandler} that is used during authentication + * both at the connect-time as well as at subsequent revalidation-time that + * occurs at regular intervals. + * + * @return ChallengeHandler + */ + public abstract ChallengeHandler getChallengeHandler(); + + /** + * Gets the connect timeout in milliseconds. Default connect timeout is + * zero milliseconds. + * + * @return connect timeout value in milliseconds + */ + @Override + public abstract int getConnectTimeout(); + + /** + * Gets the names of all the extensions that have been enabled for this + * connection. The enabled extensions are negotiated between the client + * and the server during the handshake. The names of the negotiated + * extensions can be obtained using {@link #getNegotiatedExtensions()} API. + * An empty Collection is returned if no extensions have been enabled for + * this connection. The enabled extensions will be a subset of the + * supported extensions. + * + * @return Collection names of the enabled extensions for this + * connection + */ + public abstract Collection getEnabledExtensions(); + + /** + * Gets the value of the specified {@link Parameter} defined in an enabled + * extension. If the parameter is not defined for this connection but a + * default value for the parameter is set using the method + * {@link WebSocketFactory#setDefaultParameter(Parameter, Object)}, + * then the default value is returned. + *

+ * Setting the parameter value when the connection is successfully + * established will result in an IllegalStateException. + *

+ * @param Generic type of the value of the Parameter + * @param parameter Parameter whose value needs to be set + * @return the value of the specified parameter + * @throw IllegalStateException if this method is invoked after connect() + */ + public abstract T getEnabledParameter(Parameter parameter); + + /** + * Gets the names of all the protocols that are enabled for this + * connection. Returns an empty Collection if protocols are not enabled. + * + * @return Collection supported protocols by this connection + */ + public abstract Collection getEnabledProtocols(); + + /** + * Returns {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). + * + * @return HttpRedirectOption indicating the + */ + public abstract HttpRedirectPolicy getRedirectPolicy(); + + /** + * Returns the {@link InputStream} to receive binary messages. The + * methods on {@link InputStream} will block till the message arrives. The + * {@link InputStream} must be used to only receive binary + * messages. + *

+ * An IOException is thrown if this method is invoked when the connection + * has not been established. Receiving a text message using the + * {@link InputStream} will result in an IOException. + *

+ * Once the connection is closed, a new {@link InputStream} should be + * obtained using this method after the connection has been established. + * Using the old InputStream will result in an IOException. + *

+ * @return InputStream to receive binary messages + * @throws IOException if the method is invoked before the connection is + * successfully opened; if a text message is being + * read using the InputStream + */ + @Override + public abstract InputStream getInputStream() throws IOException; + + /** + * Returns a {@link WebSocketMessageReader} that can be used to receive + * binary and text messages based on the + * {@link WebSocketMessageType}. + *

+ * If this method is invoked before a connection is established successfully, + * then an IOException is thrown. + *

+ * Once the connection is closed, a new {@link WebSocketMessageReader} + * should be obtained using this method after the connection has been + * established. Using the old WebSocketMessageReader will result in an + * IOException. + *

+ * @return WebSocketMessageReader to receive binary and text messages + * @throws IOException if invoked before the connection is opened + */ + public abstract WebSocketMessageReader getMessageReader() throws IOException; + + /** + * Returns a {@link WebSocketMessageWriter} that can be used to send + * binary and text messages. + *

+ * If this method is invoked before a connection is established + * successfully, then an IOException is thrown. + *

+ * Once the connection is closed, a new {@link WebSocketMessageWriter} + * should be obtained using this method after the connection has been + * established. Using the old WebSocketMessageWriter will result in an + * IOException. + *

+ * @return WebSocketMessageWriter to send binary and text messages + * @throws IOException if invoked before the connection is opened + */ + public abstract WebSocketMessageWriter getMessageWriter() throws IOException; + + /** + * Gets names of all the enabled extensions that have been successfully + * negotiated between the client and the server during the initial + * handshake. + *

+ * Returns an empty Collection if no extensions were negotiated between the + * client and the server. The negotiated extensions will be a subset of the + * enabled extensions. + *

+ * If this method is invoked before a connection is successfully established, + * an IllegalStateException is thrown. + * + * @return Collection successfully negotiated using this + * connection + * @throws IllegalStateException if invoked before the {@link #connect()} + * completes + */ + public abstract Collection getNegotiatedExtensions(); + + /** + * Returns the value of the specified {@link Parameter} of a negotiated + * extension. + *

+ * If this method is invoked before the connection is successfully + * established, an IllegalStateException is thrown. + * + * @param parameter type + * @param parameter parameter of a negotiated extension + * @return T value of the specified parameter + * @throws IllegalStateException if invoked before the {@link #connect()} + * completes + */ + public abstract T getNegotiatedParameter(Parameter parameter); + + /** + * Gets the protocol that the client and the server have successfully + * negotiated. + *

+ * If this method is invoked before the connection is successfully + * established, an IllegalStateException is thrown. + *

+ * @return protocol negotiated by the client and the server + * @throws IllegalStateException if invoked before the {@link #connect()} + * completes + */ + public abstract String getNegotiatedProtocol(); + + /** + * Returns the {@link OutputStream} to send binary messages. The + * message is put on the wire only when {@link OutputStream#flush()} is + * invoked. + *

+ * If this method is invoked before {@link #connect()} is complete, an + * IOException is thrown. + *

+ * Once the connection is closed, a new {@link OutputStream} should + * be obtained using this method after the connection has been + * established. Using the old OutputStream will result in IOException. + *

+ * @return OutputStream to send binary messages + * @throws IOException if the method is invoked before the connection is + * successfully opened + */ + @Override + public abstract OutputStream getOutputStream() throws IOException; + + /** + * Returns a {@link Reader} to receive text messages from this + * connection. This method should be used to only to receive text + * messages. Methods on {@link Reader} will block till a message arrives. + *

+ * If the Reader is used to receive binary messages, then an + * IOException is thrown. + *

+ * If this method is invoked before a connection is established + * successfully, then an IOException is thrown. + *

+ * Once the connection is closed, a new {@link Reader} should be obtained + * using this method after the connection has been established. Using the + * old Reader will result in an IOException. + *

+ * @return Reader used to receive text messages from this connection + * @throws IOException if the method is invoked before the connection is + * successfully opened + */ + public abstract Reader getReader() throws IOException; + + /** + * Returns the names of extensions that have been discovered for this + * connection. An empty Collection is returned if no extensions were + * discovered for this connection. + * + * @return Collection extension names discovered for this + * connection + */ + public abstract Collection getSupportedExtensions(); + + /** + * Returns a {@link Writer} to send text messages from this + * connection. The message is put on the wire only when + * {@link Writer#flush()} is invoked. + *

+ * An IOException is thrown if this method is invoked when the connection + * has not been established. + *

+ * Once the connection is closed, a new {@link Writer} should be obtained + * using this method after the connection has been established. Using the + * old Writer will result in an IOException. + *

+ * @return Writer used to send text messages from this connection + * @throws IOException if the method is invoked before the connection is + * successfully opened + */ + public abstract Writer getWriter() throws IOException; + + /** + * Sets the connect timeout in milliseconds. The timeout will expire if + * there is no exchange of packets(for example, 100% packet loss) while + * establishing the connection. A timeout value of zero indicates + * no timeout. + * + * @param connectTimeout timeout value in milliseconds + * @throws IllegalStateException if the connection timeout is being set + * after the connection has been established + * @throws IllegalArgumentException if connectTimeout is negative + */ + @Override + public abstract void setConnectTimeout(int connectTimeout); + + /** + * Sets the {@link ChallengeHandler} that is used during authentication + * both at the connect-time as well as at subsequent revalidation-time that + * occurs at regular intervals. + * + * @param challengeHandler ChallengeHandler used for authentication + */ + public abstract void setChallengeHandler(ChallengeHandler challengeHandler); + + /** + * Registers the names of all the extensions that must be negotiated between + * the client and the server during the handshake. This method must be + * invoked before invoking the {@link #connect()} method. The + * enabled extensions should be a subset of the supported extensions. Only + * the extensions that are explicitly enabled are put on the wire even + * though there could be more supported extensions on this connection. + *

+ * If this method is invoked after connection is successfully established, + * an IllegalStateException is thrown. If an enabled extension is not + * discovered as a supported extension, then IllegalStateException is thrown. + *

+ * @param extensions list of extensions to be negotiated with the server + * during the handshake + * @throw IllegalStateException if this method is invoked after successful + * connection or any of the specified + * extensions is not a supported extension + */ + public abstract void setEnabledExtensions(Collection extensions); + + /** + * Sets the value of the specified {@link Parameter} defined in an enabled + * extension. The application developer should set the extension parameters + * of the enabled extensions before invoking the {@link #connect()} method. + *

+ * Setting the parameter value when the connection is successfully + * established will result in an IllegalStateException. + *

+ * If the parameter has a default value that was specified using + * {@link WebSocketFactory#setDefaultParameter(Parameter, Object)}, + * then setting the same parameter using this method will override the + * default value. + *

+ * @param extension parameter type + * @param parameter Parameter whose value needs to be set + * @param value of the specified parameter + * @throw IllegalStateException if this method is invoked after connect() + */ + public abstract void setEnabledParameter(Parameter parameter, T value); + + /** + * Registers the protocols to be negotiated with the server during the + * handshake. This method must be invoked before {@link #connect()} is + * called. + *

+ * If this method is invoked after a connection has been successfully + * established, an IllegalStateException is thrown. + *

+ * @param extensions list of extensions to be negotiated with the server + * during the handshake + * @throw IllegalStateException if this method is invoked after connect() + */ + public abstract void setEnabledProtocols(Collection protocols); + + /** + * Sets {@link HttpRedirectPolicy} indicating the policy for + * following HTTP redirects (3xx). + * + * @param option HttpRedirectOption to used for following the + * redirects + */ + public abstract void setRedirectPolicy(HttpRedirectPolicy option); +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/DefaultWebSocketFactory.java b/android/src/main/java/org/kaazing/net/ws/impl/DefaultWebSocketFactory.java new file mode 100755 index 0000000..fa1e973 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/DefaultWebSocketFactory.java @@ -0,0 +1,219 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl; + +import static java.util.Collections.unmodifiableCollection; +import static java.util.Collections.unmodifiableMap; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.ServiceLoader; + +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.ws.WebSocket; +import org.kaazing.net.ws.WebSocketExtension.Parameter; +import org.kaazing.net.ws.WebSocketFactory; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionFactorySpi; + +public final class DefaultWebSocketFactory extends WebSocketFactory { + private static final Map _extensionFactories; + + private final Map _parameters; + private HttpRedirectPolicy _redirectOption; + private Collection _supportedExtensions; + private Collection _enabledExtensions; + private ChallengeHandler _challengeHandler; + private int _connectTimeout = 0; // milliseconds + + static { + Class clazz = WebSocketExtensionFactorySpi.class; + ServiceLoader loader = ServiceLoader.load(clazz); + Map factories = new HashMap(); + + for (WebSocketExtensionFactorySpi factory: loader) { + String extensionName = factory.getExtensionName(); + + if (extensionName != null) + { + factories.put(extensionName, factory); + } + } + _extensionFactories = unmodifiableMap(factories); + } + + public DefaultWebSocketFactory() { + _parameters = new HashMap(); + + _supportedExtensions = new HashSet(); + _supportedExtensions.addAll(_extensionFactories.keySet()); + + // ### TODO: Should _redirectOption be null or + // HttpRedirectOption.ALWAYS by default. Note that in + // HttpURLConnection, followRedirects is true by default. + _redirectOption = HttpRedirectPolicy.ALWAYS; + + } + + @Override + public WebSocket createWebSocket(URI location) + throws URISyntaxException { + return createWebSocket(location, (String[]) null); + } + + @Override + public WebSocket createWebSocket(URI location, String... protocols) + throws URISyntaxException { + Collection enabledProtocols = null; + Collection enabledExtensions = null; + + // Clone enabled protocols maintained at the WebSocketFactory level to + // pass into the WebSocket instance. + if (protocols != null) { + enabledProtocols = new HashSet(Arrays.asList(protocols)); + } + + // Clone enabled extensions maintained at the WebSocketFactory level to + // pass into the WebSocket instance. + if (_enabledExtensions != null) { + enabledExtensions = new ArrayList(_enabledExtensions); + } + + // Clone the map of default parameters maintained at the + // WebSocketFactory level to pass into the WebSocket instance. + Map enabledParams = + new HashMap(); + enabledParams.putAll(_parameters); + + // Create a WebSocket instance that inherits the enabled protocols, + // enabled extensions, enabled parameters, the HttpRedirectOption, + // the extension factories(ie. the supported extensions). + WebSocketImpl ws = new WebSocketImpl(location, + _extensionFactories, + _redirectOption, + enabledExtensions, + enabledProtocols, + enabledParams, + _challengeHandler, + _connectTimeout); + return ws; + } + + @Override + public int getDefaultConnectTimeout() { + return _connectTimeout; + } + + @Override + public ChallengeHandler getDefaultChallengeHandler() { + return _challengeHandler; + } + + + + @Override + public Collection getDefaultEnabledExtensions() { + return (_enabledExtensions == null) ? Collections.emptySet() : + unmodifiableCollection(_enabledExtensions); + } + + + @Override + public HttpRedirectPolicy getDefaultRedirectPolicy() { + return _redirectOption; + } + + @Override + public T getDefaultParameter(Parameter parameter) { + String extName = parameter.extension().name(); + WsExtensionParameterValuesSpiImpl paramValues = _parameters.get(extName); + + if (paramValues == null) { + return null; + } + + return paramValues.getParameterValue(parameter); + } + + @Override + public Collection getSupportedExtensions() { + return (_supportedExtensions == null) ? Collections.emptySet() : + unmodifiableCollection(_supportedExtensions); + } + + @Override + public void setDefaultChallengeHandler(ChallengeHandler challengeHandler) { + _challengeHandler = challengeHandler; + } + + @Override + public void setDefaultConnectTimeout(int connectTimeout) { + _connectTimeout = connectTimeout; + } + + @Override + public void setDefaultEnabledExtensions(Collection extensions) { + if (extensions == null) { + _enabledExtensions = extensions; + return; + } + + Collection supportedExtns = getSupportedExtensions(); + for (String extension : extensions) { + if (!supportedExtns.contains(extension)) { + String s = String.format("'%s' is not a supported extension", extension); + throw new IllegalStateException(s); + } + + if (_enabledExtensions == null) { + _enabledExtensions = new ArrayList(); + } + + _enabledExtensions.add(extension); + } + } + + @Override + public void setDefaultRedirectPolicy(HttpRedirectPolicy redirectOption) { + _redirectOption = redirectOption; + } + + @Override + public void setDefaultParameter(Parameter parameter, T value) { + String extensionName = parameter.extension().name(); + + WsExtensionParameterValuesSpiImpl parameterValues = _parameters.get(extensionName); + if (parameterValues == null) { + parameterValues = new WsExtensionParameterValuesSpiImpl(); + _parameters.put(extensionName, parameterValues); + } + + parameterValues.setParameterValue(parameter, value); + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/WebSocketImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/WebSocketImpl.java new file mode 100755 index 0000000..d950290 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/WebSocketImpl.java @@ -0,0 +1,1188 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl; + +import static java.util.Collections.unmodifiableCollection; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.UnsupportedEncodingException; +import java.io.Writer; +import java.net.SocketTimeoutException; +import java.net.URI; +import java.net.URISyntaxException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.gateway.client.impl.CommandMessage; +import org.kaazing.gateway.client.impl.WebSocketChannel; +import org.kaazing.gateway.client.impl.WebSocketHandlerListener; +import org.kaazing.gateway.client.impl.util.WSCompositeURI; +import org.kaazing.gateway.client.impl.util.WSURI; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeChannel; +import org.kaazing.gateway.client.impl.ws.WebSocketCompositeHandler; +import org.kaazing.gateway.client.impl.ws.WebSocketSelectedChannel; +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.impl.util.BlockingQueueImpl; +import org.kaazing.net.impl.util.ResumableTimer; +import org.kaazing.net.ws.WebSocket; +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketExtension; +import org.kaazing.net.ws.WebSocketExtension.Parameter; +import org.kaazing.net.ws.WebSocketExtension.Parameter.Metadata; +import org.kaazing.net.ws.WebSocketMessageReader; +import org.kaazing.net.ws.WebSocketMessageWriter; +import org.kaazing.net.ws.impl.io.WsInputStreamImpl; +import org.kaazing.net.ws.impl.io.WsMessageReaderAdapter; +import org.kaazing.net.ws.impl.io.WsMessageReaderImpl; +import org.kaazing.net.ws.impl.io.WsMessageWriterImpl; +import org.kaazing.net.ws.impl.io.WsOutputStreamImpl; +import org.kaazing.net.ws.impl.io.WsReaderImpl; +import org.kaazing.net.ws.impl.io.WsWriterImpl; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionFactorySpi; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionHandlerSpi; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionParameterValuesSpi; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionSpi; + +public class WebSocketImpl extends WebSocket { + private static final String _CLASS_NAME = WebSocketImpl.class.getName(); + private static final Logger _LOG = Logger.getLogger(_CLASS_NAME); + + // These member variables are final as they will not change once they are + // created/set. + private final Map _enabledParameters; + private final Map _negotiatedParameters; + private final Map _extensionFactories; + private final WSURI _location; + private final WebSocketCompositeHandler _handler; + private final WebSocketCompositeChannel _channel; + + private Collection _enabledExtensions; + private Collection _negotiatedExtensions; + private Collection _supportedExtensions; + private Collection _enabledProtocols; + private String _negotiatedProtocol; + private WsInputStreamImpl _inputStream; + private WsOutputStreamImpl _outputStream; + private WsReaderImpl _reader; + private WsWriterImpl _writer; + private WsMessageReaderImpl _messageReader; + private WsMessageWriterImpl _messageWriter; + private BlockingQueueImpl _sharedQueue; + private HttpRedirectPolicy _followRedirect; + private ChallengeHandler _challengeHandler; + private int _connectTimeout = 0; + + private ReadyState _readyState; + private Exception _exception; + + /** + * Values are CONNECTING = 0, OPEN = 1, CLOSING = 2, and CLOSED = 3; + */ + enum ReadyState { + CONNECTING, OPEN, CLOSING, CLOSED; + } + + /** + * Creates a WebSocket that opens up a full-duplex connection to the target + * location on a supported WebSocket provider. Call connect() to establish + * the location after adding event listeners. + * + * @param location URI of the WebSocket service for the connection + * @throws Exception if connection could not be established + */ + public WebSocketImpl(URI location, + Map extensionFactories) + throws URISyntaxException { + this(location, + extensionFactories, + HttpRedirectPolicy.ALWAYS, + null, + null, + new HashMap(), + null, + 0); + } + + public WebSocketImpl(URI location, + Map extensionFactories, + HttpRedirectPolicy followRedirect, + Collection enabledExtensions, + Collection enabledProtocols, + Map enabledParameters, + ChallengeHandler challengeHandler, + int connectTimeout) + throws URISyntaxException { + WSCompositeURI compUri = new WSCompositeURI(location); + + _readyState = ReadyState.CLOSED; + _location = compUri.getWSEquivalent(); + + _followRedirect = followRedirect; + _enabledParameters = enabledParameters; + _negotiatedParameters = new HashMap(); + _extensionFactories = extensionFactories; + _challengeHandler = challengeHandler; + _connectTimeout = connectTimeout; + + // Set up the WebCompositeHandler with the listener. Methods on the + // listener will be invoked from the pipeline. This will allow us to + // manage the lifecycle of the WebSocket. + _handler = WebSocketCompositeHandler.COMPOSITE_HANDLER; + _handler.setListener(handlerListener); + + // Setup the channel that will represent this instance of the WebSocket. + _channel = new WebSocketCompositeChannel(compUri); + _channel.setWebSocket(this); + + if ((_extensionFactories != null) && (_extensionFactories.size() > 0)) + { + _supportedExtensions = new HashSet(); + _supportedExtensions.addAll(_extensionFactories.keySet()); + } + + setEnabledExtensions(enabledExtensions); + setEnabledProtocols(enabledProtocols); + } + + + @Override + public synchronized void close() throws IOException { + close(0, null); + } + + @Override + public synchronized void close(int code) throws IOException { + close(code, null); + } + + @Override + public synchronized void close(int code, String reason) throws IOException { + String args = String.format("code = '%d', reason = '%s'", code, reason); + _LOG.entering(_CLASS_NAME, "close", args); + + if (code != 0) { + //verify code and reason agaist RFC 6455 + //if code is present, it must equal to 1000 or in range 3000 to 4999 + if (code != 1000 && (code < 3000 || code > 4999)) { + throw new IllegalArgumentException("code must equal to 1000 or in range 3000 to 4999"); + } + + //if reason is present, it must not be longer than 123 bytes + if (reason != null && reason.length() > 0) { + //convert reason to UTF8 string + try { + byte[] reasonBytes = reason.getBytes("UTF8"); + if (reasonBytes.length > 123) { + throw new IllegalArgumentException("Reason is longer than 123 bytes"); + } + reason = new String(reasonBytes, "UTF8"); + + } catch (UnsupportedEncodingException e) { + _LOG.log(Level.FINEST, e.getMessage(), e); + throw new IllegalArgumentException("Reason must be encodable to UTF8"); + } + } + } + + if ((_readyState == ReadyState.CLOSED) || (_readyState == ReadyState.CLOSING)) { + // Since the WebSocket is already closed/closing, we just bail. + _LOG.log(Level.FINE, "WebSocket is closed or closing"); + return; + } + + if (_readyState == ReadyState.CONNECTING) { + _LOG.log(Level.FINE, "WebSocket is connecting"); + _readyState = ReadyState.CLOSED; + cleanupAfterClose(); + + // If the close() is called and connection is still being established + // with WebSocket.connect(), inform the application that the connection + // failed. + setException(new WebSocketException("Connection Failed")); + notifyAll(); + return; + } + + setException(null); + _readyState = ReadyState.CLOSING; + _handler.processClose(_channel, code, reason); + + // Block till the WebSocket is closed completely. + // Sometimes the thread can have a spurious wakeup without getting + // notified, interrupted, or timing out. So, we should guard with + // the WHILE loop. + while ((_readyState != ReadyState.CLOSED) && (_exception == null)) { + try { + wait(); + if (getException() != null) { + break; + } + } + catch (InterruptedException e) { + throw new WebSocketException(e); + } + } + + // Check if there is any exception that needs to be reported. + Exception exception = getException(); + if (exception != null) { + throw new WebSocketException(exception); + } + + // At this point, the cleanup after close has already been performed. + } + + @Override + public void connect() throws IOException { + _LOG.entering(_CLASS_NAME, "connect"); + + ResumableTimer connectTimer = null; + String[] enabledProtocols = null; + + synchronized (this) { + if (_readyState == ReadyState.OPEN) { + return; + } + else if (_readyState == ReadyState.CONNECTING){ + String s = "WebSocket connection is in progress"; + throw new IllegalStateException(s); + } + else if (_readyState == ReadyState.CLOSING) { + String s = "WebSocket is not in a state to connect at this time"; + throw new IllegalStateException(s); + } + + // Prepare for connecting. + _readyState = ReadyState.CONNECTING; + setException(null); + + int len = getEnabledProtocols().size(); + + if (len > 0) { + enabledProtocols = getEnabledProtocols().toArray(new String[len]); + } + // Used by the producer(i.e. the handlerListener) and the + // consumer(i.e. the WebSocketMessageReader). + _sharedQueue = new BlockingQueueImpl(); + + // ### TODO: This might be temporary till we install extensions' + // handler directly in the pipeline. + _channel.setChallengeHandler(_challengeHandler); + + // Setup the channel with the specified characteristics. + String extensionsHeader = rfc3864FormattedString(); + _channel.setEnabledExtensions(extensionsHeader); + _channel.setFollowRedirect(_followRedirect); + + // If _connectTimeout == 0, then it means there is no timeout. + if (_connectTimeout > 0) { + // Create connect timer that is scheduled to run after _connectTimeout + // milliseconds once it is started. If the connection is not created + // before the timer expires, then an exception is thrown. + connectTimer = new ResumableTimer(new Runnable() { + @Override + public void run() { + if (_readyState == ReadyState.CONNECTING) { + SocketTimeoutException ex = new SocketTimeoutException("Connection timeout"); + // Inform the app by raising the CLOSE event. + _handler.doClose(_channel, ex); + + // Try closing the connection all the way down. This may + // block when there is a network loss. That's why we are + // first informing the application about the connection + // timeout. + _handler.processClose(_channel, 0, "Connection timeout"); + } + } + }, _connectTimeout, false); + + _channel.setConnectTimer(connectTimer); + + // Start the connect timer just before we connect to the end-point. + connectTimer.start(); + } + } + + // Connect to the end-point. Keep this out of the synchronized blocks + // that are above and below to avoid deadlock(between connect-timer + // thread and this thread) when there is a network loss. + _handler.processConnect(_channel, _location, enabledProtocols); + + synchronized (this) { + // Block till the WebSocket is opened. + // Sometimes the thread can have a spurious wakeup without getting + // notified, interrupted, or timing out. So, we should guard with + // the WHILE loop. + while ((_readyState != ReadyState.OPEN) && (_exception == null)) { + try { + wait(); + + if (getException() != null) { + break; + } + } + catch (InterruptedException e) { + throw new WebSocketException(e); + } + } + } + + if (connectTimer != null) { + // Cancel the timer and clear the timer in the channel. + connectTimer.cancel(); + _channel.setConnectTimer(null); + } + + // Check if there is any exception that needs to be reported. + Exception exception = getException(); + if (exception != null) { + String s = "Connection failed"; + throw new WebSocketException(s, exception); + } + + // At this point, the _negotiatedProtocol and the _negotiatedExtensions + // should be set. + + // ### TODO: If an enabled extension is successfully negotiated, then + // add the corresponding handler to the pipeline. + } + + @Override + public ChallengeHandler getChallengeHandler() { + return _challengeHandler; + } + + @Override + public int getConnectTimeout() { + return _connectTimeout; + } + + @Override + public Collection getEnabledExtensions() { + return (_enabledExtensions == null) ? Collections.emptySet() : + unmodifiableCollection(_enabledExtensions); + } + + @Override + public T getEnabledParameter(Parameter parameter) { + String extName = parameter.extension().name(); + WsExtensionParameterValuesSpiImpl paramValues = _enabledParameters.get(extName); + + if (paramValues == null) { + return null; + } + + return paramValues.getParameterValue(parameter); + } + + @Override + public Collection getEnabledProtocols() { + return (_enabledProtocols == null) ? Collections.emptySet() : + unmodifiableCollection(_enabledProtocols); + } + + @Override + public HttpRedirectPolicy getRedirectPolicy() { + return _followRedirect; + } + + @Override + public InputStream getInputStream() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot create InputStream as the WebSocket is not connected"; + throw new IOException(s); + } + + synchronized (this) { + if ((_inputStream != null) && !_inputStream.isClosed()) { + return _inputStream; + } + + WsMessageReaderAdapter adapter = null; + adapter = new WsMessageReaderAdapter(getMessageReader()); + _inputStream = new WsInputStreamImpl(adapter); + } + + return _inputStream; + } + + @Override + public WebSocketMessageReader getMessageReader() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot create MessageReader as the WebSocket is not connected"; + throw new IOException(s); + } + + synchronized (this) { + if ((_messageReader != null) && !_messageReader.isClosed()) { + return _messageReader; + } + + if (_sharedQueue == null) { + // Used by the producer(i.e. the handlerListener) and the + // consumer(i.e. the WebSocketMessageReader). + _sharedQueue = new BlockingQueueImpl(); + } + + _messageReader = new WsMessageReaderImpl(this, _sharedQueue); + } + + return _messageReader; + } + + @Override + public WebSocketMessageWriter getMessageWriter() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot create MessageWriter as the WebSocket is not connected"; + throw new IOException(s); + } + + synchronized (this) { + if ((_messageWriter != null) && !_messageWriter.isClosed()) { + return _messageWriter; + } + + _messageWriter = new WsMessageWriterImpl(this); + } + return _messageWriter; + } + + @Override + public Collection getNegotiatedExtensions() { + if (_readyState != ReadyState.OPEN) { + String s = "Extensions have not been negotiated as the webSocket " + + "is not yet connected"; + throw new IllegalStateException(s); + } + + return (_negotiatedExtensions == null) ? Collections.emptySet() : + unmodifiableCollection(_negotiatedExtensions); + } + + @Override + public T getNegotiatedParameter(Parameter parameter) { + if (_readyState != ReadyState.OPEN) { + String s = "Extensions have not been negotiated as the webSocket " + + "is not yet connected"; + throw new IllegalStateException(s); + } + + String extName = parameter.extension().name(); + WsExtensionParameterValuesSpiImpl paramValues = _negotiatedParameters.get(extName); + + if (paramValues == null) { + return null; + } + + return paramValues.getParameterValue(parameter); + } + + @Override + public String getNegotiatedProtocol() { + if (_readyState != ReadyState.OPEN) { + String s = "Protocols have not been negotiated as the webSocket " + + "is not yet connected"; + throw new IllegalStateException(s); + } + + return _negotiatedProtocol; + } + + @Override + public OutputStream getOutputStream() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot get the OutputStream as the WebSocket is not yet connected"; + throw new IOException(s); + } + + synchronized (this) + { + if ((_outputStream != null) && !_outputStream.isClosed()) { + return _outputStream; + } + + _outputStream = new WsOutputStreamImpl(getMessageWriter()); + } + + return _outputStream; + } + + @Override + public Reader getReader() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot create Reader as the WebSocket is not connected"; + throw new IOException(s); + } + + synchronized (this) { + if ((_reader != null) && !_reader.isClosed()) { + return _reader; + } + + WsMessageReaderAdapter adapter = null; + adapter = new WsMessageReaderAdapter(getMessageReader()); + _reader = new WsReaderImpl(adapter); + } + + return _reader; + } + + @Override + public Collection getSupportedExtensions() { + return (_supportedExtensions == null) ? Collections.emptySet() : + unmodifiableCollection(_supportedExtensions); + } + + @Override + public Writer getWriter() throws IOException { + if (_readyState != ReadyState.OPEN) { + String s = "Cannot create Writer as the WebSocket is not yet connected"; + throw new IOException(s); + } + + synchronized (this) + { + if ((_writer != null) && !_writer.isClosed()) { + return _writer; + } + + _writer = new WsWriterImpl(getMessageWriter()); + } + + return _writer; + } + + @Override + public void setChallengeHandler(ChallengeHandler challengeHandler) { + _challengeHandler = challengeHandler; + } + + @Override + public void setConnectTimeout(int connectTimeout) { + if (_readyState != ReadyState.CLOSED) { + String s = "Connection timeout can be set only when the WebSocket is closed"; + throw new IllegalStateException(s); + } + + if (connectTimeout < 0) { + throw new IllegalArgumentException("Connect timeout cannot be negative"); + } + + _connectTimeout = connectTimeout; + } + + @Override + public void setEnabledExtensions(Collection extensions) { + if (_readyState != ReadyState.CLOSED) { + String s = "Extensions can be enabled only when the WebSocket is closed"; + throw new IllegalStateException(s); + } + + if (extensions == null) { + _enabledExtensions = extensions; + return; + } + + Collection supportedExtns = getSupportedExtensions(); + for (String extension : extensions) { + if (!supportedExtns.contains(extension)) { + String s = String.format("'%s' is not a supported extension", extension); + throw new IllegalStateException(s); + } + + if (_enabledExtensions == null) { + _enabledExtensions = new ArrayList(); + } + + _enabledExtensions.add(extension); + } + } + + @Override + public void setEnabledParameter(Parameter parameter, T value) { + if (_readyState != ReadyState.CLOSED) { + String s = "Parameters can be set only when the WebSocket is closed"; + throw new IllegalStateException(s); + } + + String extensionName = parameter.extension().name(); + + WsExtensionParameterValuesSpiImpl parameterValues = _enabledParameters.get(extensionName); + if (parameterValues == null) { + parameterValues = new WsExtensionParameterValuesSpiImpl(); + _enabledParameters.put(extensionName, parameterValues); + } + + parameterValues.setParameterValue(parameter, value); + } + + @Override + public void setEnabledProtocols(Collection protocols) { + if (_readyState != ReadyState.CLOSED) { + String s = "Protocols can be enabled only when the WebSocket is closed"; + throw new IllegalStateException(s); + } + + if ((protocols == null) || protocols.isEmpty()) { + _enabledProtocols = protocols; + return; + } + + _enabledProtocols = new ArrayList(); + + for (String protocol : protocols) { + _enabledProtocols.add(protocol); + } + } + + @Override + public void setRedirectPolicy(HttpRedirectPolicy option) { + _followRedirect = option; + } + + // --------------------- Internal Implementation ------------------ + + public WebSocketCompositeChannel getCompositeChannel() { + return _channel; + } + + public boolean isConnected() { + return (_readyState == ReadyState.OPEN); + } + + public boolean isDisconnected() { + return (_readyState == ReadyState.CLOSED); + } + + public Exception getException() { + return _exception; + } + + public void setException(Exception exception) { + _exception = exception; + } + + public synchronized void send(ByteBuffer buf) throws IOException { + _LOG.entering(_CLASS_NAME, "send", buf); + + if (_readyState != ReadyState.OPEN) { + String s = "Messages can be sent only when the WebSocket is connected"; + throw new WebSocketException(s); + } + + _handler.processBinaryMessage(_channel, new WrappedByteBuffer(buf)); + } + + public synchronized void send(String message) throws IOException { + _LOG.entering(_CLASS_NAME, "send", message); + + if (_readyState != ReadyState.OPEN) { + String s = "Messages can be sent only when the WebSocket is connected"; + throw new WebSocketException(s); + } + + _handler.processTextMessage(_channel, message); + } + + // --------------------- Private Implementation -------------------------- + + private synchronized void connectionOpened(String protocol, + String extensionsHeader) { + // ### TODO: Currently, the Gateway is not sending the negotiated + // protocol. + setNegotiatedProtocol(protocol); + + // Parse the negotiated extensions and parameters. This can result in + // _exception to be setup indicating that there is something wrong + // while parsing the negotiated extensions and parameters. + setNegotiatedExtensions(extensionsHeader); + + if ((getException() == null) && (_readyState == ReadyState.CONNECTING)) { + _readyState = ReadyState.OPEN; + } + else { + // The exception can be caused either while parsing the negotiated + // extensions and parameters or the expiry of the connection timeout. + // The parsing of negotiated extension can cause an exception if -- + // 1) a negotiated extension is not an enabled extension or + // 2) the type of a negotiated parameter is not String. + _readyState = ReadyState.CLOSED; + + // Inform the Gateway to close the WebSocket. + _handler.processClose(_channel, 0, null); + } + + // Unblock the connect() call so that it can proceed. + notifyAll(); + } + + private synchronized void connectionClosed(boolean wasClean, + int code, + String reason) { + if (_readyState == ReadyState.CLOSED) { + return; + } + + _readyState = ReadyState.CLOSED; + + if (!wasClean) { + if (reason == null) { + reason = "Connection Failed"; + } + + setException(new WebSocketException(code, reason)); + } + + cleanupAfterClose(); + + // Unblock the close() call so that it can proceed. + notifyAll(); + } + + private synchronized void connectionClosed(Exception ex) { + if (_readyState == ReadyState.CLOSED) { + return; + } + + setException(ex); + + _readyState = ReadyState.CLOSED; + + cleanupAfterClose(); + + // Unblock the close() call so that it can proceed. + notifyAll(); + } + + private synchronized void connectionFailed(Exception ex) { + if (_readyState == ReadyState.CLOSED) { + return; + } + + if (ex == null) { + ex = new WebSocketException("Connection Failed"); + } + + setException(ex); + + _readyState = ReadyState.CLOSED; + + cleanupAfterClose(); + + // Unblock threads so that they can proceed. + notifyAll(); + } + + private synchronized void cleanupAfterClose() { + setNegotiatedExtensions(null); + setNegotiatedProtocol(null); + _negotiatedParameters.clear(); + + // ### TODO: + // 1. WsExtensionHandlerSpis that were been added to the pipeline based + // on negotiated extensions for this connection should be removed. + + if (_messageReader != null) { + // Notify the waiting consumers that the connection is closing. + try { + _messageReader.close(); + } + catch (IOException ex) { + _LOG.log(Level.FINE, ex.getMessage(), ex); + } + } + + if (_sharedQueue != null) { + _sharedQueue.done(); + } + + if (_inputStream != null) { + try { + _inputStream.close(); + } + catch (Exception ex) { + _LOG.log(Level.FINE, ex.getMessage(), ex); + } + } + + if (_outputStream != null) { + try { + _outputStream.close(); + } + catch (Exception ex) { + _LOG.log(Level.FINE, ex.getMessage(), ex); + } + } + + if (_reader != null) { + try { + _reader.close(); + } + catch (Exception ex) { + _LOG.log(Level.FINE, ex.getMessage(), ex); + } + } + + if (_writer != null) { + try { + _writer.close(); + } + catch (Exception ex) { + _LOG.log(Level.FINE, ex.getMessage(), ex); + } + } + + _messageReader = null; + _sharedQueue = null; + _messageWriter = null; + _inputStream = null; + _outputStream = null; + _reader = null; + _writer = null; + } + + private String formattedExtension(String extensionName, + WebSocketExtensionParameterValuesSpi paramValues) { + if (extensionName == null) { + return ""; + } + + WebSocketExtension extension = + WebSocketExtension.getWebSocketExtension(extensionName); + Collection> extnParameters = extension.getParameters(); + StringBuffer buffer = new StringBuffer(extension.name()); + + // We are using extnParameters to iterate as we want the ordered list + // of parameters. + for (Parameter param : extnParameters) { + if (param.required()) { + // Required parameter is not enabled/set. + String s = String.format("Extension '%s': Required parameter " + + "'%s' must be set", extension.name(), param.name()); + if ((paramValues == null) || + (paramValues.getParameterValue(param) == null)) { + throw new IllegalStateException(s); + } + } + + if (paramValues == null) { + // We should continue so that we can throw an exception if + // any of the required parameters has not been set. + continue; + } + + Object value = paramValues.getParameterValue(param); + + if (value == null) { + // Non-required parameter has not been set. So, let's continue + // to the next one. + continue; + } + + if (param.temporal()) { + // Temporal/transient parameters, even if they are required, + // are not put on the wire. + continue; + } + + if (param.anonymous()) { + // If parameter is anonymous, then only it's value is put + // on the wire. + buffer.append(";").append(value); + continue; + } + + // Otherwise, append the name=value pair. + buffer.append(";").append(param.name()).append("=").append(value); + } + + return buffer.toString(); + } + + private BlockingQueueImpl getSharedQueue() { + return _sharedQueue; + } + + private String rfc3864FormattedString() { + // Iterate over enabled extensions. Using WebSocketExtensionFactorySpi + // for each extension, create a WebSocketExtensionSpi instance for each + // of the enabled extensions and pass WebSocketExtensionParameterValuesSpi + // that should contain the values of the enabled parameters. + StringBuffer extensionsHeader = new StringBuffer(""); + Map handlers = + new HashMap(); + for (String extensionName : getEnabledExtensions()) { + WebSocketExtensionFactorySpi extensionFactory = + _extensionFactories.get(extensionName); + WebSocketExtensionParameterValuesSpi paramValues = + _enabledParameters.get(extensionName); + + // ### TODO: We are not setting up the extensions' handler in the + // pipeline at this point. + WebSocketExtensionSpi extension = extensionFactory.createWsExtension(paramValues); + WebSocketExtensionHandlerSpi extHandler = extension.createHandler(); + handlers.put(extensionName, extHandler); + + // Get the RFC-3864 formatted string representation of the + // WebSocketExtension. + String formatted = formattedExtension(extensionName, paramValues); + + if (formatted.length() > 0) { + if (extensionsHeader.length() > 0) { + // Add the ',' separator between strings representing + // different extensions. + extensionsHeader.append(","); + } + + extensionsHeader.append(formatted); + } + } + + return extensionsHeader.toString(); + } + + // Comma separated list of negotiated extensions and parameters based on + // RFC 3864 format. + private void setNegotiatedExtensions(String extensionsHeader) { + if ((extensionsHeader == null) || + (extensionsHeader.trim().length() == 0)) { + _negotiatedExtensions = null; + return; + } + + String[] extns = extensionsHeader.split(","); + List extnNames = new ArrayList(); + + for (String extn : extns) { + String[] properties = extn.split(";"); + String extnName = properties[0].trim(); + + if (!getEnabledExtensions().contains(extnName)) { + String s = String.format("Extension '%s' is not an enabled " + + "extension so it should not have been negotiated", extnName); + setException(new WebSocketException(s)); + return; + } + + WebSocketExtension extension = + WebSocketExtension.getWebSocketExtension(extnName); + WsExtensionParameterValuesSpiImpl paramValues = + _negotiatedParameters.get(extnName); + Collection> anonymousParams = + extension.getParameters(Metadata.ANONYMOUS); + + // Start from the second(0-based) property to parse the name-value + // pairs as the first(or 0th) is the extension name. + for (int i = 1; i < properties.length; i++) { + String property = properties[i].trim(); + String[] pair = property.split("="); + Parameter parameter = null; + String paramValue = null; + + if (pair.length == 1) { + // We are dealing with an anonymous parameter. Since the + // Collection is actually an ArrayList, we are guaranteed to + // iterate the parameters in the definition/creation order. + // As there is no parameter name, we will just get the next + // anonymous Parameter instance and use it for setting the + // value. The onus is on the extension implementor to either + // use only named parameters or ensure that the anonymous + // parameters are defined in the order in which the server + // will send them back during negotiation. + parameter = anonymousParams.iterator().next(); + paramValue = pair[0].trim(); + } + else { + parameter = extension.getParameter(pair[0].trim()); + paramValue = pair[1].trim(); + } + + if (parameter.type() != String.class) { + String paramName = parameter.name(); + String s = String.format("Negotiated Extension '%s': " + + "Type of parameter '%s' should be String", + extnName, paramName); + setException(new WebSocketException(s)); + return; + } + + if (paramValues == null) { + paramValues = new WsExtensionParameterValuesSpiImpl(); + _negotiatedParameters.put(extnName, paramValues); + } + + paramValues.setParameterValue(parameter, paramValue); + + } + extnNames.add(extnName); + } + + HashSet extnsSet = new HashSet(extnNames); + _negotiatedExtensions = unmodifiableCollection(extnsSet); + } + + private void setNegotiatedProtocol(String protocol) { + _negotiatedProtocol = protocol; + } + + private static final WebSocketHandlerListener handlerListener = new WebSocketHandlerListener() { + + @Override + public void connectionOpened(WebSocketChannel channel, String protocol) { + _LOG.entering(_CLASS_NAME, "connectionOpened"); + + WebSocketCompositeChannel cc = (WebSocketCompositeChannel)channel; + WebSocketImpl webSocket = (WebSocketImpl) cc.getWebSocket(); + WebSocketSelectedChannel selChan = ((WebSocketCompositeChannel)channel).selectedChannel; + + synchronized (webSocket) { + // ### TODO: Currently, Gateway is not returning the negotiated + // protocol. + // Try parsing the negotiated extensions in the + // connectionOpened() method. Only when everything looks good, + // mark the connection as opened. If a negotiated extension is + // not in the list of enabled extensions, then we will setup an + // exception and close down. + webSocket.connectionOpened(protocol, + selChan.getNegotiatedExtensions()); + } + } + + @Override + public void binaryMessageReceived(WebSocketChannel channel, WrappedByteBuffer buf) { + _LOG.entering(_CLASS_NAME, "binaryMessageReceived"); + + WebSocketCompositeChannel cc = (WebSocketCompositeChannel)channel; + WebSocketImpl webSocket = (WebSocketImpl) cc.getWebSocket(); + + synchronized (webSocket) { + BlockingQueueImpl sharedQueue = webSocket.getSharedQueue(); + if (sharedQueue != null) { + synchronized (sharedQueue) { + try { + ByteBuffer payload = buf.getNioByteBuffer(); + sharedQueue.put(payload); + } + catch (InterruptedException ex) { + _LOG.log(Level.INFO, ex.getMessage(), ex); + } + } + } + } + } + + @Override + public void textMessageReceived(WebSocketChannel channel, String text) { + _LOG.entering(_CLASS_NAME, "textMessageReceived", text); + + WebSocketCompositeChannel cc = (WebSocketCompositeChannel)channel; + WebSocketImpl webSocket = (WebSocketImpl) cc.getWebSocket(); + + synchronized (webSocket) { + BlockingQueueImpl sharedQueue = webSocket.getSharedQueue(); + if (sharedQueue != null) { + synchronized (sharedQueue) { + try { + sharedQueue.put(text); + } + catch (InterruptedException ex) { + _LOG.log(Level.INFO, ex.getMessage(), ex); + } + } + } + } + } + + @Override + public void connectionClosed(WebSocketChannel channel, + boolean wasClean, + int code, + String reason) { + _LOG.entering(_CLASS_NAME, "connectionClosed"); + + WebSocketCompositeChannel cc = (WebSocketCompositeChannel)channel; + WebSocketImpl webSocket = (WebSocketImpl) cc.getWebSocket(); + + // Since close() is a blocking call, if there is any thread + // waiting then we should call webSocket.connectionClosed() to + // unblock it. + synchronized (webSocket) { + webSocket.connectionClosed(wasClean, code, reason); + } + } + + @Override + public void connectionClosed(WebSocketChannel channel, Exception ex) { + _LOG.entering(_CLASS_NAME, "onError"); + + WebSocketCompositeChannel cc = (WebSocketCompositeChannel)channel; + WebSocketImpl webSocket = (WebSocketImpl) cc.getWebSocket(); + + synchronized (webSocket) { + webSocket.connectionClosed(ex); + } + } + + @Override + public void connectionFailed(WebSocketChannel channel, Exception ex) { + _LOG.entering(_CLASS_NAME, "onError"); + + WebSocketCompositeChannel cc = (WebSocketCompositeChannel)channel; + WebSocketImpl webSocket = (WebSocketImpl) cc.getWebSocket(); + + synchronized (webSocket) { + webSocket.connectionFailed(ex); + } + } + + @Override + public void authenticationRequested(WebSocketChannel channel, + String location, + String challenge) { + // Should never be fired from WebSocketCompositeHandler + } + + @Override + public void redirected(WebSocketChannel channel, String location) { + // Should never be fired from WebSocketCompositeHandler + } + + @Override + public void commandMessageReceived(WebSocketChannel channel, + CommandMessage message) { + // ignore + } + }; +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/WsExtensionParameterValuesSpiImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/WsExtensionParameterValuesSpiImpl.java new file mode 100755 index 0000000..8d2e022 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/WsExtensionParameterValuesSpiImpl.java @@ -0,0 +1,68 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl; + +import static java.util.Collections.unmodifiableSet; + +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +import org.kaazing.net.ws.WebSocketExtension.Parameter; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionParameterValuesSpi; + +public final class WsExtensionParameterValuesSpiImpl extends WebSocketExtensionParameterValuesSpi { + + private final Map, Object> values; + + WsExtensionParameterValuesSpiImpl() { + values = new HashMap, Object>(); + } + + @Override + public Collection> getParameters() { + if (values.isEmpty()) { + return unmodifiableSet(Collections.>emptySet()); + } + + Set> keys = values.keySet(); + return unmodifiableSet(keys); + } + + @Override + public T getParameterValue(Parameter parameter) { + return parameter.type().cast(values.get(parameter)); + } + + public void setParameterValue(Parameter parameter, T value) { + values.put(parameter, value); + } + + // This is used to set value of a negotiated parameter. At that time, we + // only have the string representation of the parameter. So, it's important + // that negotiated parameters be of type String. + public void setParameterValue(Parameter parameter, String value) { + values.put(parameter, value); + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/ws/impl/WsURLConnectionImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/WsURLConnectionImpl.java new file mode 100755 index 0000000..654fb34 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/WsURLConnectionImpl.java @@ -0,0 +1,367 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.net.URISyntaxException; +import java.net.URL; +import java.security.Permission; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.kaazing.net.auth.ChallengeHandler; +import org.kaazing.net.http.HttpRedirectPolicy; +import org.kaazing.net.ws.WebSocketExtension.Parameter; +import org.kaazing.net.ws.WebSocketMessageReader; +import org.kaazing.net.ws.WebSocketMessageWriter; +import org.kaazing.net.ws.WsURLConnection; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionFactorySpi; + +public class WsURLConnectionImpl extends WsURLConnection { + private WebSocketImpl _webSocket; + + + public WsURLConnectionImpl(URL location, + Map extensionFactories) { + super(location); + + try { + _webSocket = new WebSocketImpl(location.toURI(), extensionFactories); + } + catch (URISyntaxException e) { + e.printStackTrace(); + } + } + + // -------------------------- WsURLConnection Methods ------------------- + @Override + public void close() throws IOException { + _webSocket.close(); + } + + @Override + public void close(int code) throws IOException { + _webSocket.close(code); + } + + @Override + public void close(int code, String reason) throws IOException { + _webSocket.close(code, reason); + } + + @Override + public void connect() throws IOException { + _webSocket.connect(); + } + + @Override + public ChallengeHandler getChallengeHandler() { + return _webSocket.getChallengeHandler(); + } + + @Override + public int getConnectTimeout() { + return _webSocket.getConnectTimeout(); + } + + @Override + public Collection getEnabledExtensions() { + return _webSocket.getEnabledExtensions(); + } + + @Override + public T getEnabledParameter(Parameter parameter) { + return _webSocket.getEnabledParameter(parameter); + } + + @Override + public Collection getEnabledProtocols() { + return _webSocket.getEnabledExtensions(); + } + + @Override + public HttpRedirectPolicy getRedirectPolicy() { + return _webSocket.getRedirectPolicy(); + } + + @Override + public InputStream getInputStream() throws IOException { + return _webSocket.getInputStream(); + } + + @Override + public WebSocketMessageReader getMessageReader() throws IOException { + return _webSocket.getMessageReader(); + } + + @Override + public WebSocketMessageWriter getMessageWriter() throws IOException { + return _webSocket.getMessageWriter(); + } + + @Override + public Collection getNegotiatedExtensions() { + return _webSocket.getNegotiatedExtensions(); + } + + @Override + public T getNegotiatedParameter(Parameter parameter) { + return _webSocket.getNegotiatedParameter(parameter); + } + + @Override + public String getNegotiatedProtocol() { + return _webSocket.getNegotiatedProtocol(); + } + + @Override + public OutputStream getOutputStream() throws IOException { + return _webSocket.getOutputStream(); + } + + @Override + public Reader getReader() throws IOException { + return _webSocket.getReader(); + } + + @Override + public Collection getSupportedExtensions() { + return _webSocket.getSupportedExtensions(); + } + + @Override + public Writer getWriter() throws IOException { + return _webSocket.getWriter(); + } + + @Override + public void setChallengeHandler(ChallengeHandler challengeHandler) { + _webSocket.setChallengeHandler(challengeHandler); + } + + @Override + public void setConnectTimeout(int timeout) { + _webSocket.setConnectTimeout(timeout); + } + + @Override + public void setEnabledExtensions(Collection extensions) { + _webSocket.setEnabledExtensions(extensions); + } + + @Override + public void setEnabledProtocols(Collection protocols) { + _webSocket.setEnabledProtocols(protocols); + } + + @Override + public void setEnabledParameter(Parameter parameter, T value) { + _webSocket.setEnabledParameter(parameter, value); + } + + @Override + public void setRedirectPolicy(HttpRedirectPolicy option) { + _webSocket.setRedirectPolicy(option); + } + + // --------------- Unsupported URLConnection Methods ---------------------- + @Override + public void addRequestProperty(String key, String value) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getReadTimeout() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setReadTimeout(int timeout) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + // @Override -- Not available in JDK 6. + public long getContentLengthLong() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getContentLength() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getContentType() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getContentEncoding() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getExpiration() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getDate() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getLastModified() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getHeaderField(String name) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Map> getHeaderFields() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public int getHeaderFieldInt(String name, int Default) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + // @Override -- Not available in JDK 6. + public long getHeaderFieldLong(String name, long Default) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getHeaderFieldDate(String name, long Default) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getHeaderFieldKey(int n) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getHeaderField(int n) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Object getContent() throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @SuppressWarnings("rawtypes") + @Override + public Object getContent(Class[] classes) throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Permission getPermission() throws IOException { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getDoInput() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setDoInput(boolean doinput) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getDoOutput() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setDoOutput(boolean dooutput) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getAllowUserInteraction() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setAllowUserInteraction(boolean allowuserinteraction) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getUseCaches() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setUseCaches(boolean usecaches) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public long getIfModifiedSince() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setIfModifiedSince(long ifmodifiedsince) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public boolean getDefaultUseCaches() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setDefaultUseCaches(boolean defaultusecaches) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public String getRequestProperty(String key) { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public Map> getRequestProperties() { + throw new UnsupportedOperationException("Unsupported Operation"); + } + + @Override + public void setRequestProperty(String key, String value) { + throw new UnsupportedOperationException("Unsupported Operation"); + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsInputStreamImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsInputStreamImpl.java new file mode 100755 index 0000000..d45eb4a --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsInputStreamImpl.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.ByteBuffer; + +import org.kaazing.gateway.client.util.WrappedByteBuffer; +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketMessageType; + +public class WsInputStreamImpl extends InputStream { + + private WsMessageReaderAdapter _adapter; + private WrappedByteBuffer _buffer; + private boolean _closed = false; + + public WsInputStreamImpl(WsMessageReaderAdapter adapter) throws IOException { + _adapter = adapter; + } + + @Override + public synchronized int available() throws IOException { + checkStreamClosed(); + + if (_buffer == null) { + return 0; + } + + return _buffer.remaining(); + } + + @Override + public void close() throws IOException { + if (_closed) { + return; + } + + if (_buffer != null) { + _buffer.clear(); + } + + _buffer = null; + _closed = true; + } + + @Override + public void mark(int readLimit) { + throw new UnsupportedOperationException("Not supported"); + } + + @Override + public boolean markSupported() { + return false; + } + + @Override + public synchronized int read() throws IOException { + checkStreamClosed(); + + try { + prepareBuffer(); + } + catch (IOException ex) { + WebSocketMessageType type = _adapter.getType(); + if ((type == WebSocketMessageType.EOS) || (type == null)) { + // End of stream. Return -1 as per the javadoc. + return -1; + } + + // InputStream is used to read a text message. + String s = "InvalidMessageType: InputStream must be used to only " + + "receive binary messages"; + throw new WebSocketException(s, ex); + } + + return _buffer.get(); + } + + @Override + public synchronized int read(byte[] b) throws IOException { + return read(b, 0, b.length); + } + + @Override + public synchronized int read(byte[] b, int off, int len) throws IOException { + checkStreamClosed(); + + try { + prepareBuffer(); + } + catch (IOException ex) { + WebSocketMessageType type = _adapter.getType(); + if ((type == WebSocketMessageType.EOS) || (type == null)) { + // End of stream. Return -1 as per the javadoc. + return -1; + } + + // InputStream is used to read a text message. + String s = "InvalidMessageType: InputStream must be used to only " + + "receive binary messages"; + throw new WebSocketException(s, ex); + } + + int remaining = _buffer.remaining(); + int retval = (remaining < len) ? remaining : len; + _buffer.get(b, off, retval); + + return retval; + } + + @Override + public void reset() throws IOException { + checkStreamClosed(); + _buffer.clear(); + _buffer = null; + } + + // ---------------------- Internal Implementation ----------------------- + public boolean isClosed() { + return _closed; + } + + // ---------------------- Private Methods ------------------------------- + private void checkStreamClosed() throws IOException { + if (!_closed) { + return; + } + + String s = "Cannot perform the operation as the InputStream is closed"; + throw new WebSocketException(s); + } + + private void prepareBuffer() throws IOException { + if ((_buffer == null) || (!_buffer.hasRemaining())) { + ByteBuffer byteBuf = _adapter.readBinary(); + + if (_buffer == null) { + _buffer = new WrappedByteBuffer(byteBuf); + } + else { + int pos = _buffer.position(); + int remaining = byteBuf.remaining(); + byte[] bytes = new byte[remaining]; + byteBuf.get(bytes); + _buffer.putBytes(bytes); + _buffer.limit(_buffer.position()); + _buffer.position(pos); + } + } + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessagePullParser.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessagePullParser.java new file mode 100755 index 0000000..ac1578d --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessagePullParser.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.kaazing.net.ws.WebSocketMessageReader; +import org.kaazing.net.ws.WebSocketMessageType; + +/** + * The inspiration of this class is the XmlPullParser. Eventually, we may + * decide to expose this class as part of our public APIs. At that time, we + * should rename it to WebSocketMessagePullParser to conform with the + * naming convention for publicly exposed classes. + */ +public class WsMessagePullParser { + private WebSocketMessageReader _messageReader; + + public WsMessagePullParser(WebSocketMessageReader messageReader) { + _messageReader = messageReader; + } + + /** + * Returns the next text message received on this connection. This method + * will block till a text message is received. Any binary messages that may + * arrive will be ignored. A null is returned when the connection is closed. + *

+ * An IOException is thrown if the connection has not been established + * before invoking this method. + * + * @return CharSequence the payload of the text message + * @throws IOException if the connection has not been established + */ + public CharSequence nextText() throws IOException { + WebSocketMessageType msgType = null; + + while ((msgType = _messageReader.next()) != WebSocketMessageType.EOS) { + if (msgType == WebSocketMessageType.TEXT) { + return _messageReader.getText(); + } + } + + return null; + } + + /** + * Returns the next binary message received on this connection. This method + * will block till a binar message is received. Any text messages that may + * arrive will be ignored. A null is returned when the connection is closed. + *

+ * An IOException is thrown if the connection has not been established + * before invoking this method. + * + * @return ByteBuffer the payload of the binary message + * @throws IOException if the connection has not been established + */ + public ByteBuffer nextBinary() throws IOException { + WebSocketMessageType msgType = null; + + while ((msgType = _messageReader.next()) != WebSocketMessageType.EOS) { + if (msgType == WebSocketMessageType.BINARY) { + return _messageReader.getBinary(); + } + } + + return null; + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderAdapter.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderAdapter.java new file mode 100755 index 0000000..1a89a33 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderAdapter.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.nio.ByteBuffer; + +import org.kaazing.net.ws.WebSocketMessageReader; +import org.kaazing.net.ws.WebSocketMessageType; + +/** + * This is an internal adapter that will be used by our {@link InputStream} and + * {@link Reader}implementations to just invoke {@link #readBinary()} and + * {@link #readText()} methods to retrieve binary and text messages + * respectively. + */ +public class WsMessageReaderAdapter { + private WebSocketMessageReader _messageReader; + + public WsMessageReaderAdapter(WebSocketMessageReader messageReader) { + if (messageReader == null) { + String s = "Null WebSocketMessageReader passed in"; + throw new IllegalArgumentException(s); + } + + _messageReader = messageReader; + } + + /** + * Returns the {@link WebSocketMessageType} of the last received message. + * + * @return WebSocketMessageType + */ + public WebSocketMessageType getType() { + return _messageReader.getType(); + } + + /** + * This method will be used our InputStream implementation to continually + * retrieve binary messages. + * + * @return ByteBuffer payload of a binary message + * @throws IOException if this method is used to retrieve a text + * message or the connection is closed + */ + public ByteBuffer readBinary() throws IOException { + _messageReader.next(); + return _messageReader.getBinary(); + } + + /** + * This method will be used our Reader implementation to continually + * retrieve text messages. + * + * @return CharSequence payload of a text message + * @throws IOException if this method is used to retrieve a binary + * message or the connection is closed + */ + public CharSequence readText() throws IOException { + _messageReader.next(); + return _messageReader.getText(); + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderImpl.java new file mode 100755 index 0000000..3cfe49d --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageReaderImpl.java @@ -0,0 +1,187 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.kaazing.net.impl.util.BlockingQueueImpl; +import org.kaazing.net.ws.WebSocketMessageType; +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketMessageReader; +import org.kaazing.net.ws.impl.WebSocketImpl; + +public class WsMessageReaderImpl extends WebSocketMessageReader { + private static final String _CLASS_NAME = WsMessageReaderImpl.class.getName(); + private static final Logger _LOG = Logger.getLogger(_CLASS_NAME); + + private final BlockingQueueImpl _sharedQueue; + private final WebSocketImpl _webSocket; + private Object _payload; + private WebSocketMessageType _messageType; + private boolean _closed = false; + + public WsMessageReaderImpl(WebSocketImpl webSocket, + BlockingQueueImpl sharedQueue) { + if (webSocket == null) { + String s = "Null webSocket passed in"; + throw new IllegalArgumentException(s); + } + + if (sharedQueue == null) { + String s = "Null sharedQueue passed in"; + throw new IllegalArgumentException(s); + } + + _webSocket = webSocket; + _sharedQueue = sharedQueue; + } + + // --------------------- WebSocketMessageReader Implementation ----------- + @Override + public ByteBuffer getBinary() throws IOException { + if (_messageType == null) { + return null; + } + + if (_messageType == WebSocketMessageType.EOS){ + String s = "End of stream has reached as the connection has been closed"; + throw new WebSocketException(s); + } + + if (_messageType != WebSocketMessageType.BINARY) { + String s = "Invalid WebSocketMessageType: Cannot decode the payload " + + "as a binary message"; + throw new WebSocketException(s); + } + + return ByteBuffer.wrap(((ByteBuffer)_payload).array()); + } + + @Override + public CharSequence getText() throws IOException { + if (_messageType == null) { + return null; + } + + if (_messageType == WebSocketMessageType.EOS){ + String s = "End of stream has reached as the connection has been closed"; + throw new WebSocketException(s); + } + + if (_messageType != WebSocketMessageType.TEXT) { + String s = "Invalid WebSocketMessageType: Cannot decode the payload " + + "as a text message"; + throw new WebSocketException(s); + } + + return String.valueOf(((String)_payload).toCharArray()); + } + + @Override + public WebSocketMessageType getType() { + return _messageType; + } + + @Override + public WebSocketMessageType next() throws IOException { + if (isClosed()) { + String s = "Cannot read as the MessageReader is closed"; + throw new WebSocketException(s); + } + + synchronized (this) { + if ((_sharedQueue.size() == 0) && !_webSocket.isConnected()) { + _messageType = WebSocketMessageType.EOS; + return _messageType; + } + + try { + _payload = null; + _webSocket.setException(null); + _payload = _sharedQueue.take(); + } + catch (InterruptedException ex) { + _LOG.log(Level.FINE, ex.getMessage()); + } + + if (_payload == null) { + String s = "MessageReader has been interrupted maybe the " + + "connection is closed"; + // throw new WebSocketException(s); + _LOG.log(Level.FINE, _CLASS_NAME, s); + + _messageType = WebSocketMessageType.EOS; + return _messageType; + } + + if (_payload.getClass() == String.class) { + _messageType = WebSocketMessageType.TEXT; + } + else { + _messageType = WebSocketMessageType.BINARY; + } + } + + return _messageType; + } + + // ------------------ Package-Private Implementation ---------------------- + // These methods are called from other classes in this package. They are + // not part of the public API. + public void close() throws IOException { + if (isClosed()) { + return; + } + + if (!_webSocket.isDisconnected()) { + String s = "Can't close the MessageReader if the WebSocket is " + + "still connected"; + throw new WebSocketException(s); + } + + _sharedQueue.done(); + _closed = true; + } + + public void reset() throws IOException { + if (isClosed()) { + return; + } + + if (!_webSocket.isDisconnected()) { + String s = "Can't reset the MessageReader if the WebSocket is " + + "still connected"; + throw new WebSocketException(s); + } + + _sharedQueue.reset(); + _payload = null; + _messageType = null; + } + + public boolean isClosed() { + return _closed; + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageWriterImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageWriterImpl.java new file mode 100755 index 0000000..716fb8a --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsMessageWriterImpl.java @@ -0,0 +1,76 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketMessageWriter; +import org.kaazing.net.ws.impl.WebSocketImpl; + +public class WsMessageWriterImpl extends WebSocketMessageWriter { + private WebSocketImpl _webSocket; + private boolean _closed = false; + + public WsMessageWriterImpl(WebSocketImpl webSocket) { + _webSocket = webSocket; + } + + @Override + public void writeText(CharSequence src) throws IOException { + if (isClosed()) { + String s = "Cannot write as the MessageWriter is closed"; + throw new WebSocketException(s); + } + try { + src.toString().getBytes("UTF-8"); + } + catch (UnsupportedEncodingException e) { + String s = "The platform must support UTF-8 encoded text per RFC 6455"; + throw new IOException(s); + } + + _webSocket.send(src.toString()); + } + + @Override + public void writeBinary(ByteBuffer src) throws IOException { + if (isClosed()) { + String s = "Cannot write as the MessageWriter is closed"; + throw new WebSocketException(s); + } + + _webSocket.send(src); + } + + // ----------------- Internal Implementation ---------------------------- + public void close() { + _closed = true; + _webSocket = null; + } + + public boolean isClosed() { + return _closed; + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsOutputStreamImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsOutputStreamImpl.java new file mode 100755 index 0000000..763aa15 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsOutputStreamImpl.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; + +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketMessageWriter; + +public class WsOutputStreamImpl extends OutputStream { + private WsMessageWriterImpl _writer; + private ByteArrayOutputStream _byteStream; + private boolean _streamClosed; + + public WsOutputStreamImpl(WebSocketMessageWriter writer) { + _writer = (WsMessageWriterImpl) writer; + _byteStream = new ByteArrayOutputStream(); + _streamClosed = false; + } + + @Override + public void close() throws IOException { + synchronized (this) { + if (isClosed()) { + return; + } + + _streamClosed = true; + _byteStream.close(); + _byteStream = null; + } + } + + @Override + public void flush() throws IOException { + synchronized (this) { + _checkStreamClosed(); + + if (_byteStream.size() > 0) { + byte[] bytes = _byteStream.toByteArray(); + _writer.writeBinary(ByteBuffer.wrap(bytes)); + + _byteStream.reset(); + } + } + } + + @Override + public void write(int b) throws IOException { + synchronized (this) { + _checkStreamClosed(); + + // The general contract for write(int) is that one byte is written to + // the output stream. The byte to be written is the eight low-order + // bits of the argument b. The 24 high-order bits of b are ignored. + byte a = (byte)(b & 0xff); + _byteStream.write(a); + } + } + + // ------------------------ Internal Methods ----------------------------- + public boolean isClosed() { + return _streamClosed; + } + + // ------------------------ Private Methods ------------------------------ + private void _checkStreamClosed() throws IOException { + String s = "Cannot perform the operation on the OutputStream as it " + + "is closed"; + if (_streamClosed ) { + throw new WebSocketException(s); + } + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsReaderImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsReaderImpl.java new file mode 100755 index 0000000..84a3117 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsReaderImpl.java @@ -0,0 +1,149 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.io.Reader; +import java.nio.CharBuffer; + +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketMessageType; + +public class WsReaderImpl extends Reader { + private WsMessageReaderAdapter _adapter; + private CharBuffer _charBuffer; + private boolean _closed = false; + + public WsReaderImpl(WsMessageReaderAdapter adapter) throws IOException { + _adapter = adapter; + } + + @Override + public void close() throws IOException { + if (_closed) { + return; + } + + if (_charBuffer != null) { + _charBuffer.clear(); + } + + _charBuffer = null; + _closed = true; + } + + @Override + public void mark(int readAheadLimit) throws IOException { + checkStreamClosed(); + super.mark(readAheadLimit); + } + + @Override + public int read() throws IOException { + checkStreamClosed(); + return super.read(); + } + + @Override + public int read(char[] cbuf) throws IOException { + checkStreamClosed(); + return super.read(cbuf); + } + + @Override + public int read(CharBuffer target) throws IOException { + checkStreamClosed(); + return super.read(target); + } + + @Override + public synchronized int read(char[] cbuf, int off, int len) + throws IOException { + checkStreamClosed(); + + // If the buffer doesn't have data, then get the message and populate + // the buffer with it. + if ((_charBuffer == null) || (_charBuffer.remaining() == 0)) { + try { + CharSequence text = _adapter.readText(); + _charBuffer = CharBuffer.wrap(((String)text).toCharArray()); + } + catch (IOException ex) { + WebSocketMessageType type = _adapter.getType(); + if ((type == WebSocketMessageType.EOS) || (type == null)) { + // End of stream. Return -1 as per the javadoc. + return -1; + } + + // Reader is used to read a binary message. + String s = "Invalid message type: Reader must be used to " + + "only receive text messages"; + throw new WebSocketException(s, ex); + } + } + + // Use the remaining and the passed in length to decide how much can + // be copied over into the passed in array. + int remaining = _charBuffer.remaining(); + int retval = (remaining < len) ? remaining : len; + + _charBuffer.get(cbuf, off, retval); + return retval; + } + + @Override + public boolean ready() throws IOException { + checkStreamClosed(); + + if ((_charBuffer == null) || !_charBuffer.hasRemaining()) { + return false; + } + + return true; + } + + @Override + public void reset() throws IOException { + checkStreamClosed(); + super.reset(); + } + + @Override + public long skip(long n) throws IOException { + checkStreamClosed(); + return super.skip(n); + } + + // -------------------- Internal Implementation ------------------------- + public boolean isClosed() { + return _closed; + } + + // ---------------------- Private Methods ------------------------------- + private void checkStreamClosed() throws IOException { + if (!_closed) { + return; + } + String s = "Cannot perform the operation as the Reader is closed"; + throw new WebSocketException(s); + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/io/WsWriterImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/io/WsWriterImpl.java new file mode 100755 index 0000000..f54b5f4 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/io/WsWriterImpl.java @@ -0,0 +1,119 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.io; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.io.Writer; + +import org.kaazing.net.ws.WebSocketException; +import org.kaazing.net.ws.WebSocketMessageWriter; + +public class WsWriterImpl extends Writer { + private WsMessageWriterImpl _writer; + private StringBuffer _stringBuffer; + private boolean _closed; + + public WsWriterImpl(WebSocketMessageWriter writer) { + _writer = (WsMessageWriterImpl) writer; + _stringBuffer = new StringBuffer(""); + _closed = false; + } + + @Override + public void close() throws IOException { + if (isClosed()) { + return; + } + + synchronized (this) { + if (isClosed()) { + return; + } + + _closed = true; + _stringBuffer = null; + _writer = null; + } + } + + @Override + public void write(char[] cbuf, int offset, int length) throws IOException { + if (cbuf == null) { + throw new IllegalArgumentException("Null char array passed to write()"); + } + + if (offset < 0) { + throw new StringIndexOutOfBoundsException(offset); + } + + if (length < 0) { + throw new StringIndexOutOfBoundsException(length); + } + + if (offset > (cbuf.length - length)) { + throw new StringIndexOutOfBoundsException(offset + length); + } + + synchronized (this) { + _checkWriterClosed(); + _stringBuffer.append(cbuf, offset, length); + } + } + + @Override + public void flush() throws IOException { + synchronized (this) { + _checkWriterClosed(); + + if (_stringBuffer.length() > 0) { + try { + _stringBuffer.toString().getBytes("UTF-8"); + } + catch (UnsupportedEncodingException e) { + String s = "The platform must support UTF-8 encoded text per RFC 6455"; + throw new IOException(s); + } + _writer.writeText(_stringBuffer.toString()); + } + + // We don't want to overwrite the buffer that is making it's way + // through the pipeline. So, let's create a brand new instance + // of StringBuffer to deal with future write() invocations. + _stringBuffer = new StringBuffer(""); + } + } + + // ------------------------ Internal Methods ----------------------------- + public boolean isClosed() { + return _closed; + } + + // ------------------------ Private Methods ------------------------------ + private void _checkWriterClosed() throws IOException { + String s = "Cannot perform the operation on the Writer as it " + + "is closed"; + if (_closed ) { + throw new WebSocketException(s); + } + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionFactorySpi.java b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionFactorySpi.java new file mode 100755 index 0000000..a8b1638 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionFactorySpi.java @@ -0,0 +1,64 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.spi; + + +/** + * {@link WebSocketExtensionFactorySpi} is part of Service Provider Interface + * (SPI) for admins/implementors. + *

+ * As part of implementing an extension, the admins/implementors should + * implement the following: + *

    + *
  • a sub-class of {@link WebSocketExtensionFactorySpi} + *
  • a sub-class of {@link WebSocketExtensionSpi} + *
  • a public class with {@link Parameter}s defined as + * constants + *
+ *

+ * In {@link WebSocket#connect()} and {@WsURLConnection#connect()} methods, for + * each of the enabled extensions, the corresponding {@link WebSocketExtensionFactorySpi} + * instance will be used to create a {@link WebSocketExtensionSpi} instance. + *

+ * The extensions that are successfully negotiated between the client and the + * server become part of the WebSocket message processing pipeline. + */ +public abstract class WebSocketExtensionFactorySpi { + + /** + * Returns the name of the extension that this factory will create. + * + * @return String name of the extension + */ + public abstract String getExtensionName(); + + /** + * Creates and returns the singleton{@link WebSocketExtensionSpi} instance for the + * extension that this factory is responsible for. The parameters for the + * extension are specified so that the formatted string that can be put on + * the wire can be supplied by the extension implementor. + * + * @param parameters name-value pairs + * @return WebSocketExtensionSpi singleton instance for the extension + */ + public abstract WebSocketExtensionSpi createWsExtension(WebSocketExtensionParameterValuesSpi parameters); +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionHandlerSpi.java b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionHandlerSpi.java new file mode 100755 index 0000000..06fccae --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionHandlerSpi.java @@ -0,0 +1,108 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.spi; + +/** + * ### TODO: This will be an abstract class. Changed it to get it to compile. + */ +public abstract class WebSocketExtensionHandlerSpi { + public abstract void filterSendHandshakeRequest(NextHandler nextHandler, WsHandshakeRequest message); + public abstract void filterSendBinaryMessage(NextHandler nextHandler, WsBinaryMessage message); + public abstract void filterSendTextMessage(NextHandler nextHandler, WsTextMessage message); + public abstract void filterSendClose(NextHandler nextHandler, WsCloseMessage message); + + public abstract void filterReceiveHandshakeResponse(NextHandler nextHandler, WsHandshakeResponse message); + public abstract void filterReceiveBinaryMessage(NextHandler nextHandler, WsBinaryMessage message); + public abstract void filterReceiveTextMessage(NextHandler nextHandler, WsTextMessage message); + public abstract void filterReceiveClose(NextHandler nextHandler, WsCloseMessage message); + + public static abstract class NextHandler { + public abstract void filterSendHandshakeRequest(WsHandshakeRequest message); + public abstract void filterSendBinaryMessage(WsBinaryMessage message); + public abstract void filterSendTextMessage(WsTextMessage message); + public abstract void filterSendClose(WsCloseMessage message); + + public abstract void filterReceiveHandshakeResponse(WsHandshakeResponse message); + public abstract void filterReceiveBinaryMessage(WsBinaryMessage message); + public abstract void filterReceiveTextMessage(WsTextMessage message); + public abstract void filterReceiveClose(WsCloseMessage message); + } + + // TODO: fix these classes + static class WsHandshakeRequest {} + static class WsHandshakeResponse {} + static class WsBinaryMessage {} + static class WsTextMessage {} + static class WsCloseMessage {} + + public static abstract class Adapter extends WebSocketExtensionHandlerSpi { + + @Override + public void filterSendHandshakeRequest(NextHandler nextHandler, + WsHandshakeRequest message) { + nextHandler.filterSendHandshakeRequest(message); + } + + @Override + public void filterSendBinaryMessage(NextHandler nextHandler, + WsBinaryMessage message) { + nextHandler.filterSendBinaryMessage(message); + } + + @Override + public void filterSendTextMessage(NextHandler nextHandler, + WsTextMessage message) { + nextHandler.filterSendTextMessage(message); + } + + @Override + public void filterSendClose(NextHandler nextHandler, + WsCloseMessage message) { + nextHandler.filterSendClose(message); + } + + @Override + public void filterReceiveHandshakeResponse(NextHandler nextHandler, + WsHandshakeResponse message) { + nextHandler.filterReceiveHandshakeResponse(message); + } + + @Override + public void filterReceiveBinaryMessage(NextHandler nextHandler, + WsBinaryMessage message) { + nextHandler.filterReceiveBinaryMessage(message); + } + + @Override + public void filterReceiveTextMessage(NextHandler nextHandler, + WsTextMessage message) { + nextHandler.filterReceiveTextMessage(message); + } + + @Override + public void filterReceiveClose(NextHandler nextHandler, + WsCloseMessage message) { + nextHandler.filterReceiveClose(message); + } + + } +} \ No newline at end of file diff --git a/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionParameterValuesSpi.java b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionParameterValuesSpi.java new file mode 100755 index 0000000..1ea8e69 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionParameterValuesSpi.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.spi; + +import java.util.Collection; + +import org.kaazing.net.ws.WebSocket; +import org.kaazing.net.ws.WebSocketExtension.Parameter; +import org.kaazing.net.ws.WsURLConnection; + +/** + * WsExtensionParameterValues is part of Service Provider Interface + * (SPI) for admins/implementors. + *

+ * WsExtensionParameterValues is used to cache extension parameters as + * name-value pairs in a very generic type-safe way. The implementations of + * {@link WebSocket#connect()} and {@link WsURLConnection#connect()} invoke + * {@link WebSocketExtensionFactorySpi#createWsExtension(WebSocketExtensionParameterValuesSpi)} + * method and pass in all the extension parameters that have been earlier set + * by the developer for the enabled extensions. + */ +public abstract class WebSocketExtensionParameterValuesSpi { + /** + * Returns the collection of {@link Parameter} objects of a + * {@link WebSocketExtension} that have been set. Returns an empty + * Collection if no parameters belonging to the extension have been set. + * + * @return Collection> + */ + public abstract Collection> getParameters(); + + /** + * Returns the value of type T of the specified parameter. A null is + * returned if value is not set. + * + * @param Generic type T of the parameter's value + * @param parameter extension parameter + * @return value of type T of the specified extension parameter + */ + public abstract T getParameterValue(Parameter parameter); +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionSpi.java b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionSpi.java new file mode 100755 index 0000000..39ceace --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/spi/WebSocketExtensionSpi.java @@ -0,0 +1,52 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.spi; + + +/** + * WebSocketExtensionSpi is part of Service Provider Interface (SPI) + * for admins/implementors. + *

+ * A WebSocket Extension implementation consists of the following: + *

    + *
  • a sub-class of WebSocketExtensionFactorySpi + *
  • a sub-class of WebSocketExtensionSpi + *
  • a sub-class of WebSocketExtension with + * {@link Parameter}s defined as constants + *
+ *

+ * Every supported extension will require implementing the aforementioned + * classes. A subset of the supported extensions will be enabled by the + * application developer. + *

+ * The enabled extensions are included on the wire during the handshake for + * the client and the server to negotiate. + *

+ * The successfully negotiated extensions are then added to the WebSocket + * message processing pipeline. + * + * @see RevalidateExtension + */ +public abstract class WebSocketExtensionSpi { + + public abstract WebSocketExtensionHandlerSpi createHandler(); +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerFactorySpiImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerFactorySpiImpl.java new file mode 100755 index 0000000..b45c3e8 --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerFactorySpiImpl.java @@ -0,0 +1,78 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.url; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; +import static java.util.Collections.unmodifiableMap; + +import java.net.URLStreamHandler; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import java.util.ServiceLoader; + +import org.kaazing.net.URLStreamHandlerFactorySpi; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionFactorySpi; + +public class WsURLStreamHandlerFactorySpiImpl extends URLStreamHandlerFactorySpi { + private static final Collection _supportedProtocols = unmodifiableList(asList("ws", "wse", "wsn")); + private static final Map _extensionFactories; + + static { + Class clazz = WebSocketExtensionFactorySpi.class; + ServiceLoader loader = ServiceLoader.load(clazz); + Map factories = new HashMap(); + + for (WebSocketExtensionFactorySpi factory: loader) { + String extensionName = factory.getExtensionName(); + + if (extensionName != null) + { + factories.put(extensionName, factory); + } + } + _extensionFactories = unmodifiableMap(factories); + } + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if (!_supportedProtocols.contains(protocol)) { + throw new IllegalArgumentException(String.format("Protocol not supported '%s'", protocol)); + } + + return new WsURLStreamHandlerImpl(_extensionFactories); + } + + @Override + public Collection getSupportedProtocols() { + return _supportedProtocols; + } + + // ----------------- Package Private Methods ----------------------------- + Map getExtensionFactories() { + return _extensionFactories; + } + + + // ----------------- Private Methods ------------------------------------- +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerImpl.java new file mode 100755 index 0000000..820840e --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/url/WsURLStreamHandlerImpl.java @@ -0,0 +1,97 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.url; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.net.URLStreamHandler; +import java.util.Map; + +import org.kaazing.net.ws.WsURLConnection; +import org.kaazing.net.ws.impl.WsURLConnectionImpl; +import org.kaazing.net.ws.impl.spi.WebSocketExtensionFactorySpi; + +class WsURLStreamHandlerImpl extends URLStreamHandler { + + private final Map _extensionFactories; + + private String _scheme; + + public WsURLStreamHandlerImpl(Map extensionFactories) { + _extensionFactories = extensionFactories; + } + + @Override + protected int getDefaultPort() { + return 80; + } + + @Override + protected WsURLConnection openConnection(URL location) throws IOException { + return new WsURLConnectionImpl(location, _extensionFactories); + } + + @Override + protected void parseURL(URL location, String spec, int start, int limit) { + _scheme = spec.substring(0, spec.indexOf("://")); + + // start needs to be adjusted for schemes that include a ':' such as + // java:wse, etc. + // int index = spec.indexOf(":/"); + // start = index + 1; + + URI specURI = _getSpecURI(spec); + String host = specURI.getHost(); + int port = specURI.getPort(); + String authority = specURI.getAuthority(); + String userInfo = specURI.getUserInfo(); + String path = specURI.getPath(); + String query = specURI.getQuery(); + + setURL(location, _scheme, host, port, authority, userInfo, path, query, null); + // super.parseURL(location, spec, start, limit); + } + + // ### TODO: + // This method is no longer needed as we are not supporting 'java:' + // prefixes. Keeping it for time being. + @Override + protected String toExternalForm(URL location) { + return super.toExternalForm(location); + } + + // ----------------- Private Methods ----------------------------------- + // Creates a URI that can be used to retrieve various parts such as host, + // port, etc. Based on whether the scheme includes ':', the method returns + // the appropriate URI that can be used to retrieve the needed parts. + private URI _getSpecURI(String spec) { + URI specURI = URI.create(spec); + + if (_scheme.indexOf(':') == -1) { + return specURI; + } + + String schemeSpecificPart = specURI.getSchemeSpecificPart(); + return URI.create(schemeSpecificPart); + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerFactorySpiImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerFactorySpiImpl.java new file mode 100755 index 0000000..7681c3c --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerFactorySpiImpl.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.url; + +import static java.util.Arrays.asList; +import static java.util.Collections.unmodifiableList; + +import java.net.URLStreamHandler; +import java.util.Collection; + +public class WssURLStreamHandlerFactorySpiImpl extends WsURLStreamHandlerFactorySpiImpl { + private static final Collection _supportedProtocols = unmodifiableList(asList("wss", "wse+ssl", "wssn")); + + @Override + public URLStreamHandler createURLStreamHandler(String protocol) { + if (!_supportedProtocols.contains(protocol)) { + throw new IllegalArgumentException(String.format("Protocol not supported '%s'", protocol)); + } + + return new WssURLStreamHandlerImpl(getExtensionFactories()); + } + + @Override + public Collection getSupportedProtocols() { + return _supportedProtocols; + } +} diff --git a/android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerImpl.java b/android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerImpl.java new file mode 100755 index 0000000..f67b92d --- /dev/null +++ b/android/src/main/java/org/kaazing/net/ws/impl/url/WssURLStreamHandlerImpl.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2007-2014 Kaazing Corporation. All rights reserved. + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF 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 org.kaazing.net.ws.impl.url; + +import java.util.Map; + +import org.kaazing.net.ws.impl.spi.WebSocketExtensionFactorySpi; + +public class WssURLStreamHandlerImpl extends WsURLStreamHandlerImpl { + + public WssURLStreamHandlerImpl( + Map extensionFactories) { + super(extensionFactories); + } + + @Override + protected int getDefaultPort() { + return 443; + } +} diff --git a/android/src/main/resources/META-INF/services/org.kaazing.net.URLStreamHandlerFactorySpi b/android/src/main/resources/META-INF/services/org.kaazing.net.URLStreamHandlerFactorySpi new file mode 100755 index 0000000..383208a --- /dev/null +++ b/android/src/main/resources/META-INF/services/org.kaazing.net.URLStreamHandlerFactorySpi @@ -0,0 +1,2 @@ +org.kaazing.net.ws.impl.url.WsURLStreamHandlerFactorySpiImpl +org.kaazing.net.ws.impl.url.WssURLStreamHandlerFactorySpiImpl diff --git a/android/src/main/resources/META-INF/services/org.kaazing.net.auth.BasicChallengeHandler b/android/src/main/resources/META-INF/services/org.kaazing.net.auth.BasicChallengeHandler new file mode 100755 index 0000000..41deca8 --- /dev/null +++ b/android/src/main/resources/META-INF/services/org.kaazing.net.auth.BasicChallengeHandler @@ -0,0 +1 @@ +org.kaazing.net.impl.auth.DefaultBasicChallengeHandler diff --git a/android/src/main/resources/META-INF/services/org.kaazing.net.auth.DispatchChallengeHandler b/android/src/main/resources/META-INF/services/org.kaazing.net.auth.DispatchChallengeHandler new file mode 100755 index 0000000..b51cebe --- /dev/null +++ b/android/src/main/resources/META-INF/services/org.kaazing.net.auth.DispatchChallengeHandler @@ -0,0 +1 @@ +org.kaazing.net.impl.auth.DefaultDispatchChallengeHandler diff --git a/android/src/main/resources/META-INF/services/org.kaazing.net.sse.SseEventSourceFactory b/android/src/main/resources/META-INF/services/org.kaazing.net.sse.SseEventSourceFactory new file mode 100755 index 0000000..60951a7 --- /dev/null +++ b/android/src/main/resources/META-INF/services/org.kaazing.net.sse.SseEventSourceFactory @@ -0,0 +1 @@ +org.kaazing.net.sse.impl.DefaultEventSourceFactory \ No newline at end of file diff --git a/android/src/main/resources/META-INF/services/org.kaazing.net.ws.WebSocketFactory b/android/src/main/resources/META-INF/services/org.kaazing.net.ws.WebSocketFactory new file mode 100755 index 0000000..ecb91e3 --- /dev/null +++ b/android/src/main/resources/META-INF/services/org.kaazing.net.ws.WebSocketFactory @@ -0,0 +1 @@ +org.kaazing.net.ws.impl.DefaultWebSocketFactory \ No newline at end of file diff --git a/ios/RNEventSource.xcodeproj/project.pbxproj b/ios/RNEventSource.xcodeproj/project.pbxproj new file mode 100644 index 0000000..bae23fa --- /dev/null +++ b/ios/RNEventSource.xcodeproj/project.pbxproj @@ -0,0 +1,391 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 46; + objects = { + +/* Begin PBXBuildFile section */ + 55C5EF211EA2A263005345B7 /* TRVSEventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C5EF1D1EA2A263005345B7 /* TRVSEventSource.m */; }; + 55C5EF221EA2A263005345B7 /* TRVSServerSentEvent.m in Sources */ = {isa = PBXBuildFile; fileRef = 55C5EF201EA2A263005345B7 /* TRVSServerSentEvent.m */; }; + 911488971B28A9A700A81437 /* RNEventSource.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = 911488961B28A9A700A81437 /* RNEventSource.h */; }; + 911488991B28A9A700A81437 /* RNEventSource.m in Sources */ = {isa = PBXBuildFile; fileRef = 911488981B28A9A700A81437 /* RNEventSource.m */; }; + 9114889F1B28A9A700A81437 /* libRNEventSource.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 911488931B28A9A700A81437 /* libRNEventSource.a */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 911488A01B28A9A700A81437 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 9114888B1B28A9A700A81437 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 911488921B28A9A700A81437; + remoteInfo = RNEventSource; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 911488911B28A9A700A81437 /* CopyFiles */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = "include/$(PRODUCT_NAME)"; + dstSubfolderSpec = 16; + files = ( + 911488971B28A9A700A81437 /* RNEventSource.h in CopyFiles */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 55C5EF1C1EA2A263005345B7 /* TRVSEventSource.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TRVSEventSource.h; path = TRVSEventSource/TRVSEventSource.h; sourceTree = ""; }; + 55C5EF1D1EA2A263005345B7 /* TRVSEventSource.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TRVSEventSource.m; path = TRVSEventSource/TRVSEventSource.m; sourceTree = ""; }; + 55C5EF1E1EA2A263005345B7 /* TRVSEventSourceDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TRVSEventSourceDelegate.h; path = TRVSEventSource/TRVSEventSourceDelegate.h; sourceTree = ""; }; + 55C5EF1F1EA2A263005345B7 /* TRVSServerSentEvent.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = TRVSServerSentEvent.h; path = TRVSEventSource/TRVSServerSentEvent.h; sourceTree = ""; }; + 55C5EF201EA2A263005345B7 /* TRVSServerSentEvent.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = TRVSServerSentEvent.m; path = TRVSEventSource/TRVSServerSentEvent.m; sourceTree = ""; }; + 911488931B28A9A700A81437 /* libRNEventSource.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libRNEventSource.a; sourceTree = BUILT_PRODUCTS_DIR; }; + 911488961B28A9A700A81437 /* RNEventSource.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RNEventSource.h; sourceTree = ""; }; + 911488981B28A9A700A81437 /* RNEventSource.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RNEventSource.m; sourceTree = ""; }; + 9114889E1B28A9A700A81437 /* RNEventSourceTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RNEventSourceTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 911488A31B28A9A700A81437 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 911488901B28A9A700A81437 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9114889B1B28A9A700A81437 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 9114889F1B28A9A700A81437 /* libRNEventSource.a in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 55C5EF231EA2A26D005345B7 /* TRVSEventSource */ = { + isa = PBXGroup; + children = ( + 55C5EF1C1EA2A263005345B7 /* TRVSEventSource.h */, + 55C5EF1D1EA2A263005345B7 /* TRVSEventSource.m */, + 55C5EF1E1EA2A263005345B7 /* TRVSEventSourceDelegate.h */, + 55C5EF1F1EA2A263005345B7 /* TRVSServerSentEvent.h */, + 55C5EF201EA2A263005345B7 /* TRVSServerSentEvent.m */, + ); + name = TRVSEventSource; + sourceTree = ""; + }; + 9114888A1B28A9A700A81437 = { + isa = PBXGroup; + children = ( + 911488951B28A9A700A81437 /* RNEventSource */, + 911488A21B28A9A700A81437 /* RNEventSourceTests */, + 911488941B28A9A700A81437 /* Products */, + ); + sourceTree = ""; + }; + 911488941B28A9A700A81437 /* Products */ = { + isa = PBXGroup; + children = ( + 911488931B28A9A700A81437 /* libRNEventSource.a */, + 9114889E1B28A9A700A81437 /* RNEventSourceTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 911488951B28A9A700A81437 /* RNEventSource */ = { + isa = PBXGroup; + children = ( + 55C5EF231EA2A26D005345B7 /* TRVSEventSource */, + 911488961B28A9A700A81437 /* RNEventSource.h */, + 911488981B28A9A700A81437 /* RNEventSource.m */, + ); + path = RNEventSource; + sourceTree = ""; + }; + 911488A21B28A9A700A81437 /* RNEventSourceTests */ = { + isa = PBXGroup; + children = ( + 911488A31B28A9A700A81437 /* Info.plist */, + ); + path = RNEventSourceTests; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 911488921B28A9A700A81437 /* RNEventSource */ = { + isa = PBXNativeTarget; + buildConfigurationList = 911488A61B28A9A700A81437 /* Build configuration list for PBXNativeTarget "RNEventSource" */; + buildPhases = ( + 9114888F1B28A9A700A81437 /* Sources */, + 911488901B28A9A700A81437 /* Frameworks */, + 911488911B28A9A700A81437 /* CopyFiles */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = RNEventSource; + productName = RNEventSource; + productReference = 911488931B28A9A700A81437 /* libRNEventSource.a */; + productType = "com.apple.product-type.library.static"; + }; + 9114889D1B28A9A700A81437 /* RNEventSourceTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 911488A91B28A9A700A81437 /* Build configuration list for PBXNativeTarget "RNEventSourceTests" */; + buildPhases = ( + 9114889A1B28A9A700A81437 /* Sources */, + 9114889B1B28A9A700A81437 /* Frameworks */, + 9114889C1B28A9A700A81437 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 911488A11B28A9A700A81437 /* PBXTargetDependency */, + ); + name = RNEventSourceTests; + productName = RNEventSourceTests; + productReference = 9114889E1B28A9A700A81437 /* RNEventSourceTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 9114888B1B28A9A700A81437 /* Project object */ = { + isa = PBXProject; + attributes = { + LastUpgradeCheck = 0700; + ORGANIZATIONNAME = "Jordan Byron"; + TargetAttributes = { + 911488921B28A9A700A81437 = { + CreatedOnToolsVersion = 7.0; + }; + 9114889D1B28A9A700A81437 = { + CreatedOnToolsVersion = 7.0; + }; + }; + }; + buildConfigurationList = 9114888E1B28A9A700A81437 /* Build configuration list for PBXProject "RNEventSource" */; + compatibilityVersion = "Xcode 3.2"; + developmentRegion = English; + hasScannedForEncodings = 0; + knownRegions = ( + en, + ); + mainGroup = 9114888A1B28A9A700A81437; + productRefGroup = 911488941B28A9A700A81437 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 911488921B28A9A700A81437 /* RNEventSource */, + 9114889D1B28A9A700A81437 /* RNEventSourceTests */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 9114889C1B28A9A700A81437 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 9114888F1B28A9A700A81437 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 911488991B28A9A700A81437 /* RNEventSource.m in Sources */, + 55C5EF211EA2A263005345B7 /* TRVSEventSource.m in Sources */, + 55C5EF221EA2A263005345B7 /* TRVSServerSentEvent.m in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 9114889A1B28A9A700A81437 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 911488A11B28A9A700A81437 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 911488921B28A9A700A81437 /* RNEventSource */; + targetProxy = 911488A01B28A9A700A81437 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin XCBuildConfiguration section */ + 911488A41B28A9A700A81437 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + }; + name = Debug; + }; + 911488A51B28A9A700A81437 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu99; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 9.0; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = iphoneos; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 911488A71B28A9A700A81437 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../react-native/React/**", + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Debug; + }; + 911488A81B28A9A700A81437 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + HEADER_SEARCH_PATHS = ( + "$(SRCROOT)/../../react-native/React/**", + "$(SRCROOT)/../node_modules/react-native/React/**", + ); + OTHER_LDFLAGS = "-ObjC"; + PRODUCT_NAME = "$(TARGET_NAME)"; + SKIP_INSTALL = YES; + }; + name = Release; + }; + 911488AA1B28A9A700A81437 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = RNEventSourceTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = pw.RNEventSourceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 911488AB1B28A9A700A81437 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; + INFOPLIST_FILE = RNEventSourceTests/Info.plist; + LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; + PRODUCT_BUNDLE_IDENTIFIER = pw.RNEventSourceTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 9114888E1B28A9A700A81437 /* Build configuration list for PBXProject "RNEventSource" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 911488A41B28A9A700A81437 /* Debug */, + 911488A51B28A9A700A81437 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 911488A61B28A9A700A81437 /* Build configuration list for PBXNativeTarget "RNEventSource" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 911488A71B28A9A700A81437 /* Debug */, + 911488A81B28A9A700A81437 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 911488A91B28A9A700A81437 /* Build configuration list for PBXNativeTarget "RNEventSourceTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 911488AA1B28A9A700A81437 /* Debug */, + 911488AB1B28A9A700A81437 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 9114888B1B28A9A700A81437 /* Project object */; +} diff --git a/ios/RNEventSource/RNEventSource.h b/ios/RNEventSource/RNEventSource.h new file mode 100644 index 0000000..4276274 --- /dev/null +++ b/ios/RNEventSource/RNEventSource.h @@ -0,0 +1,13 @@ +#import "React/RCTBridgeModule.h" +#import "TRVSEventSource/TRVSEventSource.h" + +@interface RNEventSource : NSObject { + TRVSEventSource *eventSource; +} + +- (void)eventSourceDidOpen:(TRVSEventSource *)eventSource; +- (void)eventSource:(TRVSEventSource *)eventSource didReceiveEvent:(TRVSServerSentEvent *)event; +- (void)eventSource:(TRVSEventSource *)eventSource didFailWithError:(NSError *)error; + +@property (nonatomic, retain) TRVSEventSource *eventSource; +@end diff --git a/ios/RNEventSource/RNEventSource.m b/ios/RNEventSource/RNEventSource.m new file mode 100644 index 0000000..39dc413 --- /dev/null +++ b/ios/RNEventSource/RNEventSource.m @@ -0,0 +1,97 @@ +#import "RNEventSource.h" + +#import +#import +#import +#import +#import + +#import "TRVSEventSource/TRVSEventSource.h" + +@implementation TRVSEventSource (React) + +- (NSNumber *)reactTag +{ + return objc_getAssociatedObject(self, _cmd); +} + +- (void)setReactTag:(NSNumber *)reactTag +{ + objc_setAssociatedObject(self, @selector(reactTag), reactTag, OBJC_ASSOCIATION_COPY_NONATOMIC); +} + +@end + +@implementation RNEventSource +{ + NSMutableDictionary *_sources; +} + +@synthesize eventSource; +@synthesize bridge = _bridge; + +RCT_EXPORT_MODULE(); + +- (void)dealloc +{ + for (TRVSEventSource *source in _sources.allValues) { + [source close]; + } +} + +RCT_EXPORT_METHOD(connect:(NSString *)URLString sourceID:(nonnull NSNumber *)sourceID) +{ + NSURL *serverURL = [NSURL URLWithString:URLString]; + + TRVSEventSource *source = [[TRVSEventSource alloc] initWithURL:serverURL]; + source.delegate = self; + source.reactTag = sourceID; + + [source open]; + + if (!_sources) { + _sources = [NSMutableDictionary new]; + } + + _sources[sourceID] = source; +} + + +- (void)eventSourceDidOpen:(TRVSEventSource *)eventSource +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"eventsourceOpen" body:@{ + @"id": eventSource.reactTag + }]; +} + +- (void)eventSource:(TRVSEventSource *)eventSource didFailWithError:(NSError *)error +{ + [_bridge.eventDispatcher sendDeviceEventWithName:@"eventsourceFailed" body:@{ + @"message": error ? error.localizedDescription : @"Empty exception", + @"id": eventSource.reactTag + }]; + + [eventSource close]; +} + +- (void)eventSource:(TRVSEventSource *)eventSource didReceiveEvent:(TRVSServerSentEvent *)event +{ + NSString *data = [[NSString alloc] initWithData:event.data encoding:NSUTF8StringEncoding]; + + [_bridge.eventDispatcher sendDeviceEventWithName:@"eventsourceEvent" body:@{ + @"type": event.event ?: @"message", + @"data": RCTNullIfNil(data), + @"id": eventSource.reactTag + }]; +} + + +RCT_EXPORT_METHOD(close:(nonnull NSNumber *)sourceID) +{ + [_sources[sourceID] close]; + [_sources removeObjectForKey:sourceID]; + + RCTLogInfo(@"RNEventSource: Closed %@", sourceID); +} + +@end diff --git a/ios/RNEventSource/TRVSEventSource/TRVSEventSource.h b/ios/RNEventSource/TRVSEventSource/TRVSEventSource.h new file mode 100755 index 0000000..e516514 --- /dev/null +++ b/ios/RNEventSource/TRVSEventSource/TRVSEventSource.h @@ -0,0 +1,107 @@ +// +// TRVSEventSourceManager.h +// TRVSEventSource +// +// Created by Travis Jeffery on 10/8/13. +// Copyright (c) 2013 Travis Jeffery. All rights reserved. +// + +#import +#import "TRVSEventSourceDelegate.h" +#import "TRVSServerSentEvent.h" + +@class TRVSServerSentEvent; + +typedef void (^TRVSEventSourceEventHandler)(TRVSServerSentEvent *event, NSError *error); + + +/** + `TRVSEventSource` is an Objective-C implementation of the EventSource DOM interface supported by modern browsers. + + An event source opens an HTTP connection, and receives events as they are sent from the server. Each event is encoded as an `TRVSServerSentEvent` object, and dispatched to all listeners for that particular event type. + + @see http://www.w3.org/TR/eventsource/ + */ +@interface TRVSEventSource : NSObject + +// The URL the event source receives events from. +@property (nonatomic, strong, readonly) NSURL *URL; +// The managed session. +@property (nonatomic, strong, readonly) NSURLSession *URLSession; +// The task used to connect to the URL and receive event data. +@property (nonatomic, strong, readonly) NSURLSessionTask *URLSessionTask; +// The operation queue on which delegate callbacks are run. +@property (nonatomic, strong, readonly) NSOperationQueue *operationQueue; +// The delegate you're using that's responsible for what to do when the event source state changes or receives events. +@property (nonatomic, weak) id delegate; + +// @name connection state + +// The connection state can be in only one state at any given time. +- (BOOL)isConnecting; +- (BOOL)isOpen; +- (BOOL)isClosed; +- (BOOL)isClosing; + +// @name initializing an event source + +/** + * Initializes an `TRVSEventSource` object with the specified URL. The event source will open only by calling -[TRVSEventSource open]. + * + * @param URL The url the event source will receive events from. + * + * @return The newly-initialized event source. + */ +- (instancetype)initWithURL:(NSURL *)URL; + +/** + * Initializes an `TRVSEventSource` object with the specified URL and session configuration. The event source will open only by calling -[TRVSEventSource open]. + * + * @param URL The url the event source will receive events from. + * + * @param sessionConfiguration The session configuration that will be used to initialize the session object. + * + * @return The newly-initialized event source. + */ +- (instancetype)initWithURL:(NSURL *)URL sessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration; + +// @name opening and closing an event source + +/** + * Opens a connection to the `URL` to receive events. The request specifies an `Accept` HTTP header field value of `text/event-stream`. + */ +- (void)open; + +/** + * Closes the connection. + */ +- (void)close; + +// @name listening for events + +/** + * Adds a listener to the event source thats runs the `eventHandler` block whenever an event is received with the given `event` name. + * + * @param event The name of the event to listen for. + * @param eventHandler The block to run when events with the given name are received. + * + * @return The identifier associated with the listener for the specified event. Pass this to `-[TRVSEventSource removeEventListenerWithIdentifier:]` to remove the listener. + */ +- (NSUInteger)addListenerForEvent:(NSString *)event + usingEventHandler:(TRVSEventSourceEventHandler)eventHandler; + +/** + Removes the event listener with the given identifier + + @param identifier The identifier associated with the event listener. + + @discussion The event listener identifier is returned when added with `-[TRVSEventSource addListenerForEvent:usingBlock:]`. + */ +- (void)removeEventListenerWithIdentifier:(NSUInteger)identifier; + +/** + * Removes all listeners for events of the given type. + */ +- (void)removeAllListenersForEvent:(NSString *)event; + +@end diff --git a/ios/RNEventSource/TRVSEventSource/TRVSEventSource.m b/ios/RNEventSource/TRVSEventSource/TRVSEventSource.m new file mode 100755 index 0000000..6f0a056 --- /dev/null +++ b/ios/RNEventSource/TRVSEventSource/TRVSEventSource.m @@ -0,0 +1,294 @@ +// +// TRVSEventSourceManager.m +// TRVSEventSource +// +// Created by Travis Jeffery on 10/8/13. +// Copyright (c) 2013 Travis Jeffery. All rights reserved. +// + +#import "TRVSEventSource.h" + +static NSUInteger const TRVSEventSourceListenersCapacity = 100; +static NSString *const TRVSEventSourceOperationQueueName = + @"com.travisjeffery.TRVSEventSource.operationQueue"; + +static NSDictionary *TRVSServerSentEventFieldsFromString( + NSString *string, + NSError *__autoreleasing *error) { + if (!string || [string length] == 0) + return nil; + + NSMutableDictionary *mutableFields = [NSMutableDictionary dictionary]; + + for (NSString *line in [string componentsSeparatedByCharactersInSet: + [NSCharacterSet newlineCharacterSet]]) { + if (!line || [line length] == 0 || [line hasPrefix:@":"]) + continue; + + @autoreleasepool { + NSScanner *scanner = [[NSScanner alloc] initWithString:line]; + scanner.charactersToBeSkipped = [NSCharacterSet whitespaceCharacterSet]; + NSString *key, *value; + [scanner scanUpToString:@":" intoString:&key]; + [scanner scanString:@":" intoString:nil]; + [scanner scanUpToString:@"\n" intoString:&value]; + + if (key && value) { + if (mutableFields[key]) { + mutableFields[key] = + [mutableFields[key] stringByAppendingFormat:@"\n%@", value]; + } else { + mutableFields[key] = value; + } + } + } + } + + return mutableFields; +} + +typedef NS_ENUM(NSUInteger, TRVSEventSourceState) { + TRVSEventSourceConnecting = 0, + TRVSEventSourceOpen, + TRVSEventSourceClosed, + TRVSEventSourceClosing, + TRVSEventSourceFailed +}; + +@interface TRVSEventSource () + +@property (nonatomic, strong, readwrite) NSOperationQueue *operationQueue; +@property (nonatomic, strong, readwrite) NSURL *URL; +@property (nonatomic, strong, readwrite) NSURLSession *URLSession; +@property (nonatomic, strong, readwrite) NSURLSessionTask *URLSessionTask; +@property (nonatomic, readwrite) TRVSEventSourceState state; +@property (nonatomic, strong, readwrite) NSMapTable *listenersKeyedByEvent; +@property (nonatomic, strong) NSMutableString *buffer; + +@end + +@implementation TRVSEventSource + +#pragma mark - Public + +- (instancetype)initWithURL:(NSURL *)URL { + return [self initWithURL:URL + sessionConfiguration:NSURLSessionConfiguration + .defaultSessionConfiguration]; +} + +- (instancetype)initWithURL:(NSURL *)URL + sessionConfiguration:(NSURLSessionConfiguration *)sessionConfiguration { + if (!(self = [super init])) + return nil; + + _operationQueue = [[NSOperationQueue alloc] init]; + _operationQueue.name = TRVSEventSourceOperationQueueName; + _operationQueue.maxConcurrentOperationCount = 1; + _URL = URL; + _listenersKeyedByEvent = + [[NSMapTable alloc] initWithKeyOptions:NSPointerFunctionsCopyIn + valueOptions:NSPointerFunctionsStrongMemory + capacity:TRVSEventSourceListenersCapacity]; + _URLSession = [NSURLSession sessionWithConfiguration:sessionConfiguration + delegate:self + delegateQueue:_operationQueue]; + self.buffer = [NSMutableString stringWithCapacity:4096]; + + return self; +} + +- (void)open { + [self transitionToConnecting]; +} + +- (void)close { + [self transitionToClosing]; +} + +- (NSUInteger)addListenerForEvent:(NSString *)event + usingEventHandler:(TRVSEventSourceEventHandler)eventHandler { + NSMutableDictionary *mutableListenersKeyedByIdentifier = + [self.listenersKeyedByEvent objectForKey:event]; + if (!mutableListenersKeyedByIdentifier) + mutableListenersKeyedByIdentifier = [NSMutableDictionary dictionary]; + + NSUInteger identifier = [[NSUUID UUID] hash]; + mutableListenersKeyedByIdentifier[@(identifier)] = [eventHandler copy]; + + [self.listenersKeyedByEvent setObject:mutableListenersKeyedByIdentifier + forKey:event]; + + return identifier; +} + +- (void)removeEventListenerWithIdentifier:(NSUInteger)identifier { + NSEnumerator *enumerator = [self.listenersKeyedByEvent keyEnumerator]; + id event = nil; + + while ((event = [enumerator nextObject])) { + NSMutableDictionary *mutableListenersKeyedByIdentifier = + [self.listenersKeyedByEvent objectForKey:event]; + + if ([mutableListenersKeyedByIdentifier objectForKey:@(identifier)]) { + [mutableListenersKeyedByIdentifier removeObjectForKey:@(identifier)]; + [self.listenersKeyedByEvent setObject:mutableListenersKeyedByIdentifier + forKey:event]; + return; + } + } +} + +- (void)removeAllListenersForEvent:(NSString *)event { + [self.listenersKeyedByEvent removeObjectForKey:event]; +} + +#pragma mark - State + +- (BOOL)isConnecting { + return self.state == TRVSEventSourceConnecting; +} + +- (BOOL)isOpen { + return self.state == TRVSEventSourceOpen; +} + +- (BOOL)isClosed { + return self.state == TRVSEventSourceClosed; +} + +- (BOOL)isClosing { + return self.state == TRVSEventSourceClosing; +} + +#pragma mark - NSURLSessionDelegate + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveData:(NSData *)data +{ + NSString *string = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; + + if (string == nil) { + return; + } + + [self.buffer appendString:string]; + NSRange range = [self.buffer rangeOfString:@"\n\n"]; + + NSError *error; + TRVSServerSentEvent *event; + while (range.location != NSNotFound) @autoreleasepool { + error = nil; + event = [TRVSServerSentEvent eventWithFields:TRVSServerSentEventFieldsFromString([self.buffer substringToIndex:range.location], &error)]; + [self.buffer deleteCharactersInRange:NSMakeRange(0, range.location + 2)]; + + if (error) + [self transitionToFailedWithError:error]; + + if (error || !event) + return; + + [[self.listenersKeyedByEvent objectForKey:event.event] + enumerateKeysAndObjectsUsingBlock: + ^(id _, TRVSEventSourceEventHandler eventHandler, BOOL *stop) { + eventHandler(event, nil); + }]; + + if ([self.delegate + respondsToSelector:@selector(eventSource:didReceiveEvent:)]) { + [self.delegate eventSource:self didReceiveEvent:event]; + } + + range = [self.buffer rangeOfString:@"\n\n"]; + } +} + +- (void)URLSession:(NSURLSession *)session + dataTask:(NSURLSessionDataTask *)dataTask + didReceiveResponse:(NSURLResponse *)response + completionHandler: + (void (^)(NSURLSessionResponseDisposition))completionHandler { + completionHandler(NSURLSessionResponseAllow); + [self transitionToOpenIfNeeded]; +} + +- (void)URLSession:(NSURLSession *)session + task:(NSURLSessionTask *)task + didCompleteWithError:(NSError *)error { + if (self.isClosing && error.code == NSURLErrorCancelled) { + [self transitionToClosed]; + } else { + [self transitionToFailedWithError:error]; + } +} + +#pragma NSCoding + +- (id)initWithCoder:(NSCoder *)aDecoder { + NSURL *URL = [aDecoder decodeObjectForKey:@"URL"]; + + if (!(self = [self initWithURL:URL])) + return nil; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.URL forKey:@"URL"]; +} + +#pragma NSCopying + +- (id)copyWithZone:(NSZone *)zone { + return [[[self class] allocWithZone:zone] initWithURL:self.URL]; +} + +#pragma mark - Private + +- (void)transitionToOpenIfNeeded { + if (self.state != TRVSEventSourceConnecting) + return; + + self.state = TRVSEventSourceOpen; + + if ([self.delegate respondsToSelector:@selector(eventSourceDidOpen:)]) { + [self.delegate eventSourceDidOpen:self]; + } +} + +- (void)transitionToFailedWithError:(NSError *)error { + self.state = TRVSEventSourceFailed; + + if ([self.delegate + respondsToSelector:@selector(eventSource:didFailWithError:)]) { + [self.delegate eventSource:self didFailWithError:error]; + } +} + +- (void)transitionToClosed { + self.state = TRVSEventSourceClosed; + + if ([self.delegate respondsToSelector:@selector(eventSourceDidClose:)]) { + [self.delegate eventSourceDidClose:self]; + } +} + +- (void)transitionToConnecting { + self.state = TRVSEventSourceConnecting; + [self.operationQueue addOperationWithBlock:^{ + [self.buffer setString:@""]; + }]; + self.URLSessionTask = [self.URLSession dataTaskWithURL:self.URL]; + [self.URLSessionTask resume]; +} + +- (void)transitionToClosing { + self.state = TRVSEventSourceClosing; + [self.operationQueue addOperationWithBlock:^{ + [self.buffer setString:@""]; + }]; + [self.URLSession invalidateAndCancel]; +} + +@end diff --git a/ios/RNEventSource/TRVSEventSource/TRVSEventSourceDelegate.h b/ios/RNEventSource/TRVSEventSource/TRVSEventSourceDelegate.h new file mode 100755 index 0000000..41e9856 --- /dev/null +++ b/ios/RNEventSource/TRVSEventSource/TRVSEventSourceDelegate.h @@ -0,0 +1,46 @@ +// +// TRVSEventSourceDelegate.h +// TRVSEventSource +// +// Created by Travis Jeffery on 10/9/13. +// Copyright (c) 2013 Travis Jeffery. All rights reserved. +// + +#import + +@class TRVSEventSource; +@class TRVSServerSentEvent; + +@protocol TRVSEventSourceDelegate +@optional + +/** + * Lets the delegate know that the event source has opened its connection. + * + * @param eventSource The event source. + */ +- (void)eventSourceDidOpen:(TRVSEventSource *)eventSource; + +/** + * Lets the delegate know that the event source has closed its connection. + * + * @param eventSource The event source. + */ +- (void)eventSourceDidClose:(TRVSEventSource *)eventSource; + +/** + * Lets the delegate know that the event source has received an event. + * + * @param eventSource The event source. + * @param event The received event. + */ +- (void)eventSource:(TRVSEventSource *)eventSource didReceiveEvent:(TRVSServerSentEvent *)event; + +/** + * Lets the delegate know that the event source failed. + * + * @param eventSource The event source. + * @param error The error associated with the failure. + */ +- (void)eventSource:(TRVSEventSource *)eventSource didFailWithError:(NSError *)error; +@end diff --git a/ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.h b/ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.h new file mode 100755 index 0000000..9303ae4 --- /dev/null +++ b/ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.h @@ -0,0 +1,35 @@ +// +// TRVSServerSentEvent.h +// TRVSEventSource +// +// Created by Travis Jeffery on 10/8/13. +// Copyright (c) 2013 Travis Jeffery. All rights reserved. +// + +#import + + +/** + `TRVSServerSentEvent` objects represent events received from the server by an event source. A server-sent event has associated `event` type, an `identifier`, associated `data`, and a `retry` inteval. Any additional fields not defined by the EventSource API spec are stored in a `userInfo` dictionary. + */ +@interface TRVSServerSentEvent : NSObject + +// The event type. +@property (nonatomic, copy, readonly) NSString *event; + +// The event identifier. +@property (nonatomic, copy, readonly) NSString *identifier; + +// The retry interval sent with the event. +@property (nonatomic, readonly) NSTimeInterval retry; + +// The data associated with the event. +@property (nonatomic, copy, readonly) NSData *data; + +// Any additional fields in the event. +@property (nonatomic, copy, readonly) NSDictionary *userInfo; + +// Creates and returns event associated with the given fields. ++ (instancetype)eventWithFields:(NSDictionary *)fields; + +@end diff --git a/ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.m b/ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.m new file mode 100755 index 0000000..fb92ecd --- /dev/null +++ b/ios/RNEventSource/TRVSEventSource/TRVSServerSentEvent.m @@ -0,0 +1,71 @@ +// +// TRVSServerSentEvent.m +// TRVSEventSource +// +// Created by Travis Jeffery on 10/8/13. +// Copyright (c) 2013 Travis Jeffery. All rights reserved. +// + +#import "TRVSServerSentEvent.h" + +@interface TRVSServerSentEvent () + +@property (nonatomic, copy, readwrite) NSString *event; +@property (nonatomic, copy, readwrite) NSString *identifier; +@property (nonatomic, readwrite) NSTimeInterval retry; +@property (nonatomic, copy, readwrite) NSData *data; +@property (nonatomic, copy, readwrite) NSDictionary *userInfo; + +@end + +@implementation TRVSServerSentEvent + ++ (instancetype)eventWithFields:(NSDictionary *)fields { + if (!fields) return nil; + + TRVSServerSentEvent *event = [[self alloc] init]; + + NSMutableDictionary *mutableFields = [NSMutableDictionary dictionaryWithDictionary:fields]; + event.event = mutableFields[@"event"]; + event.identifier = mutableFields[@"id"]; + event.data = [mutableFields[@"data"] dataUsingEncoding:NSUTF8StringEncoding]; + event.retry = [mutableFields[@"retry"] integerValue]; + + [mutableFields removeObjectsForKeys:@[@"event", @"id", @"data", @"retry"]]; + event.userInfo = mutableFields; + + return event; +} + +#pragma mark - NSCoding + +- (id)initWithCoder:(NSCoder *)aDecoder { + if (!(self = [self init])) return nil; + + self.event = [aDecoder decodeObjectForKey:@"event"]; + self.identifier = [aDecoder decodeObjectForKey:@"identifier"]; + self.data = [aDecoder decodeObjectForKey:@"data"]; + self.retry = [aDecoder decodeIntegerForKey:@"retry"]; + + return self; +} + +- (void)encodeWithCoder:(NSCoder *)aCoder { + [aCoder encodeObject:self.event forKey:@"event"]; + [aCoder encodeObject:self.identifier forKey:@"identifier"]; + [aCoder encodeObject:self.data forKey:@"data"]; + [aCoder encodeInteger:self.retry forKey:@"retry"]; +} + +#pragma mark - NSCopying + +- (id)copyWithZone:(NSZone *)zone { + TRVSServerSentEvent *event = [[[self class] allocWithZone:zone] init]; + event.event = self.event; + event.identifier = self.identifier; + event.data = self.data; + event.retry = self.retry; + return event; +} + +@end From 2ed0ad9a30bfa19c4019671edccbe9e5a32ea624 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 6 Jul 2021 13:40:25 -0300 Subject: [PATCH 2/6] src files and podspec --- Split.podspec | 17 ++++ src/platform/EventSource/EventSource.ts | 84 ++++++++++++++++++++ src/platform/EventSource/EventSourceBase.ts | 61 ++++++++++++++ src/platform/EventSource/EventSourceEvent.ts | 18 +++++ src/platform/getEventSource.ts | 13 +++ src/platform/getModules.ts | 4 +- 6 files changed, 196 insertions(+), 1 deletion(-) create mode 100644 Split.podspec create mode 100644 src/platform/EventSource/EventSource.ts create mode 100644 src/platform/EventSource/EventSourceBase.ts create mode 100644 src/platform/EventSource/EventSourceEvent.ts create mode 100644 src/platform/getEventSource.ts diff --git a/Split.podspec b/Split.podspec new file mode 100644 index 0000000..ff1d7f2 --- /dev/null +++ b/Split.podspec @@ -0,0 +1,17 @@ +require "json" + +package = JSON.parse(File.read(File.join(__dir__, "package.json"))) + +Pod::Spec.new do |s| + s.name = "Split" + s.module_name = 'Split' + s.summary = package["description"] + s.version = package["version"] + s.author = package["author"] + s.license = package["license"] + s.homepage = package["homepage"] + s.platform = :ios, "9.0" + s.source = { :git => "https://github.com/splitio/react-native-client.git", :tag => "v#{s.version}" } + s.source_files = "ios/**/*.{h,m}" + s.dependency "React" +end diff --git a/src/platform/EventSource/EventSource.ts b/src/platform/EventSource/EventSource.ts new file mode 100644 index 0000000..fbebb5e --- /dev/null +++ b/src/platform/EventSource/EventSource.ts @@ -0,0 +1,84 @@ +// @ts-nocheck +'use strict'; + +var { NativeModules, DeviceEventEmitter } = require('react-native'); + +var RNEventSource = NativeModules.RNEventSource; + +var EventSourceBase = require('./EventSourceBase'); +var EventSourceEvent = require('./EventSourceEvent'); + +var EventSourceId = 0; +// var CLOSE_NORMAL = 1000; + +/** + * Browser-compatible EventSources implementation. + * + * See https://developer.mozilla.org/en-US/docs/Web/API/EventSource + */ +class EventSource extends EventSourceBase { + _sourceId: number; + _subs: any; + + connectToSourceImpl(url: string): void { + this._sourceId = EventSourceId++; + + RNEventSource.connect(url, this._sourceId); + + this._registerEvents(this._sourceId); + } + + closeConnectionImpl(): void { + this._closeEventSource(this._sourceId); + } + + cancelConnectionImpl(): void { + this._closeEventSource(this._sourceId); + } + + _closeEventSource(id: number): void { + RNEventSource.close(id); + } + + _unregisterEvents(): void { + this._subs.forEach((e) => e.remove()); + this._subs = []; + } + + _registerEvents(id: number): void { + this._subs = [ + DeviceEventEmitter.addListener('eventsourceEvent', (ev) => { + if (ev.id !== id) { + return; + } + var event = new EventSourceEvent(ev.type, { + data: ev.message || ev.data, + }); + if (ev.type === 'message' && this.onmessage) this.onmessage(event); + this.dispatchEvent(event); + }), + DeviceEventEmitter.addListener('eventsourceOpen', (ev) => { + if (ev.id !== id) { + return; + } + this.readyState = this.OPEN; + var event = new EventSourceEvent('open'); + this.onopen && this.onopen(event); + this.dispatchEvent(event); + }), + DeviceEventEmitter.addListener('eventsourceFailed', (ev) => { + if (ev.id !== id) { + return; + } + var event = new EventSourceEvent('error'); + event.message = ev.message; + this.onerror && this.onerror(event); + this.dispatchEvent(event); + this._unregisterEvents(); + this.close(); + }), + ]; + } +} + +module.exports = EventSource; diff --git a/src/platform/EventSource/EventSourceBase.ts b/src/platform/EventSource/EventSourceBase.ts new file mode 100644 index 0000000..d9d06f3 --- /dev/null +++ b/src/platform/EventSource/EventSourceBase.ts @@ -0,0 +1,61 @@ +'use strict'; + +// Package included by react-native +var EventTarget = require('event-target-shim'); + +/** + * Shared base for platform-specific EventSource implementations. + */ +export class EventSourceBase extends EventTarget { + CONNECTING: number; + OPEN: number; + CLOSED: number; + + onerror?: Function; + onmessage?: Function; + onopen?: Function; + + binaryType?: string; + readyState: number; + url?: string; + + constructor(url: string) { + super(); + this.CONNECTING = 0; + this.OPEN = 1; + this.CLOSED = 3; + + this.url = url; + this.readyState = this.CONNECTING; + // @ts-ignore + this.connectToSourceImpl(url); + } + + close(): void { + if (this.readyState === this.CLOSED) { + return; + } + + if (this.readyState === this.CONNECTING) { + this.cancelConnectionImpl(); + } + + this.closeConnectionImpl(); + } + + closeConnectionImpl(): void { + throw new Error('Subclass must define closeConnectionImpl method'); + } + + connectToSourceImpl(): void { + throw new Error('Subclass must define connectToSourceImpl method'); + } + + cancelConnectionImpl(): void { + throw new Error('Subclass must define cancelConnectionImpl method'); + } +} + +EventSourceBase.CONNECTING = 0; +EventSourceBase.OPEN = 1; +EventSourceBase.CLOSED = 3; diff --git a/src/platform/EventSource/EventSourceEvent.ts b/src/platform/EventSource/EventSourceEvent.ts new file mode 100644 index 0000000..dd7807a --- /dev/null +++ b/src/platform/EventSource/EventSourceEvent.ts @@ -0,0 +1,18 @@ +'use strict'; + +/** + * Event object passed to the `onopen`, `onmessage`, `onerror` + * callbacks of `EventSource`. + * + * The `type` property is "open", "message", "error" respectively. + * + * In case of "message", the `data` property contains the incoming data. + */ +export class EventSourceEvent { + type: string; + + constructor(type: any, eventInitDict: any) { + this.type = type.toString(); + Object.assign(this, eventInitDict); + } +} diff --git a/src/platform/getEventSource.ts b/src/platform/getEventSource.ts new file mode 100644 index 0000000..3dce3cb --- /dev/null +++ b/src/platform/getEventSource.ts @@ -0,0 +1,13 @@ +import { NativeModules } from 'react-native'; + +let RNEventSource: EventSource | undefined; +if (NativeModules.RNEventSource) RNEventSource = require('./EventSource/EventSource'); + +/** + * Returns native implementation of EventSource. If not available (e.g., Expo or other runtime than Android and iOS), + * checks if global EventSource is available and returns it. + */ +export function getEventSource() { + // eslint-disable-next-line no-undef + return RNEventSource || typeof EventSource === 'function' ? EventSource : undefined; +} diff --git a/src/platform/getModules.ts b/src/platform/getModules.ts index 6c2ed5b..774e161 100644 --- a/src/platform/getModules.ts +++ b/src/platform/getModules.ts @@ -12,13 +12,15 @@ import { shouldAddPt } from '@splitsoftware/splitio-commons/src/trackers/impress import { ISettingsInternal } from '@splitsoftware/splitio-commons/src/utils/settingsValidation/types'; import { ISdkFactoryParams } from '@splitsoftware/splitio-commons/src/sdkFactory/types'; +import { getEventSource } from './getEventSource'; + const rnPlatform = { // Return global fetch which is always available in RN runtime getFetch() { return fetch; }, EventEmitter, - // @TODO provide `getEventSource` implementation + getEventSource, }; const syncManagerOnlineCSFactory = syncManagerOnlineFactory(pollingManagerCSFactory, pushManagerFactory); From 4a5e110cbdfa3291f303a2be9b825619fbc1fa1c Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Tue, 6 Jul 2021 15:44:14 -0300 Subject: [PATCH 3/6] added license copy --- src/platform/EventSource/EventSource.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/platform/EventSource/EventSource.ts b/src/platform/EventSource/EventSource.ts index fbebb5e..92189f7 100644 --- a/src/platform/EventSource/EventSource.ts +++ b/src/platform/EventSource/EventSource.ts @@ -1,4 +1,21 @@ // @ts-nocheck + +/** +EventSource implementation for React Native, based on "react-native-eventsource" library (https://www.npmjs.com/package/react-native-eventsource). + +See @license https://github.com/neilco/EventSource/blob/master/LICENSE.txt for additional license details. + +See @license https://github.com/kaazing/java.client/blob/master/LICENSE.txt for additional license details. + +Copyright (c) 2015 João Ribeiro (https://github.com/JonnyBGod/) + +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. +*/ + 'use strict'; var { NativeModules, DeviceEventEmitter } = require('react-native'); From 8b5463cc8c9e8797fbd8e781f8d14d1e6950fbbc Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 8 Jul 2021 20:16:39 -0300 Subject: [PATCH 4/6] prepare rc --- .travis.yml | 2 +- package-lock.json | 8 ++++---- package.json | 4 ++-- src/settings/defaults.ts | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 33d383c..9a88ea1 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ script: - npm run test - npm run build # @TODO remove next command and `publishConfig` field in package.json when publishing as a public package in NPM registry - - if [[ $TRAVIS_BRANCH == "development" && $TRAVIS_EVENT_TYPE != "pull_request" ]]; then echo "publishing" ; curl -u${ARTIFACTORY_USER}:${ARTIFACTORY_PASSWORD} https://splitio.jfrog.io/artifactory/api/npm/auth >> ~/.npmrc ; npm publish --tag rc ; else echo "no publishing" ; fi + - if [[ $TRAVIS_BRANCH == "event_source" && $TRAVIS_EVENT_TYPE != "pull_request" ]]; then echo "publishing" ; curl -u${ARTIFACTORY_USER}:${ARTIFACTORY_PASSWORD} https://splitio.jfrog.io/artifactory/api/npm/auth >> ~/.npmrc ; npm publish --tag rc ; else echo "no publishing" ; fi diff --git a/package-lock.json b/package-lock.json index 503d92c..e1497b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "splitio-react-native-private", - "version": "0.0.1-beta.2", + "version": "0.0.1-beta.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -2453,9 +2453,9 @@ } }, "@splitsoftware/splitio-commons": { - "version": "0.1.1-canary.7", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-0.1.1-canary.7.tgz", - "integrity": "sha512-dYfVsb+S9SB3/UTe6gGcsOLEHvEsrDPdEqw0aAmvGkVNX55bRffJdk4DecQosU7AeUFMqFPogpcBVM0ZfpJBZg==", + "version": "0.1.1-canary.10", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-0.1.1-canary.10.tgz", + "integrity": "sha512-jwZ594xucI/dTyXaRgzBXp9VpRVKF4BWzkIjMKhZ0ChFxTGfCjrslKffyOdvtoZPlyzEVFaJoBWF47Hh2Hw2vw==", "requires": { "object-assign": "^4.1.1", "tslib": "^2.1.0" diff --git a/package.json b/package.json index 43dac0e..a0857fd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitio-react-native-private", - "version": "0.0.1-beta.2", + "version": "0.0.1-beta.3", "description": "Split SDK for React Native", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -51,7 +51,7 @@ "registry": "https://splitio.jfrog.io/artifactory/api/npm/npm-split/" }, "dependencies": { - "@splitsoftware/splitio-commons": "0.1.1-canary.7" + "@splitsoftware/splitio-commons": "0.1.1-canary.10" }, "devDependencies": { "@react-native-community/eslint-config": "^2.0.0", diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index f5583c7..8a7495f 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -1,4 +1,4 @@ -const packageVersion = '0.0.1-beta.2'; +const packageVersion = '0.0.1-beta.3'; export const defaults = { startup: { From d181243b7cd6fa131002e6b93b304e1c88dcc5e0 Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Mon, 12 Jul 2021 14:10:18 -0300 Subject: [PATCH 5/6] polishing --- Split.podspec | 2 +- package-lock.json | 2 +- package.json | 3 +- src/platform/EventSource/EventSource.ts | 16 ++++----- src/platform/EventSource/EventSourceBase.ts | 34 ++++++++++--------- src/platform/EventSource/EventSourceEvent.ts | 9 +++-- .../getEventSource.nativeModule.spec.ts | 34 +++++++++++++++++++ src/platform/__tests__/getEventSource.spec.ts | 17 ++++++++++ src/platform/getEventSource.ts | 10 +++--- src/settings/defaults.ts | 2 +- 10 files changed, 93 insertions(+), 36 deletions(-) create mode 100644 src/platform/__tests__/getEventSource.nativeModule.spec.ts create mode 100644 src/platform/__tests__/getEventSource.spec.ts diff --git a/Split.podspec b/Split.podspec index ff1d7f2..ad1948c 100644 --- a/Split.podspec +++ b/Split.podspec @@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json"))) Pod::Spec.new do |s| s.name = "Split" - s.module_name = 'Split' + s.module_name = "Split" s.summary = package["description"] s.version = package["version"] s.author = package["author"] diff --git a/package-lock.json b/package-lock.json index e1497b6..f27d14d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "splitio-react-native-private", - "version": "0.0.1-beta.3", + "version": "0.0.1-beta.4", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index a0857fd..d3205ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "splitio-react-native-private", - "version": "0.0.1-beta.3", + "version": "0.0.1-beta.4", "description": "Split SDK for React Native", "main": "lib/commonjs/index", "module": "lib/module/index", @@ -13,6 +13,7 @@ "types", "android", "ios", + "Split.podspec", "!android/build", "!ios/build", "!**/__tests__", diff --git a/src/platform/EventSource/EventSource.ts b/src/platform/EventSource/EventSource.ts index 92189f7..930d7fe 100644 --- a/src/platform/EventSource/EventSource.ts +++ b/src/platform/EventSource/EventSource.ts @@ -34,10 +34,10 @@ var EventSourceId = 0; * See https://developer.mozilla.org/en-US/docs/Web/API/EventSource */ class EventSource extends EventSourceBase { - _sourceId: number; - _subs: any; + // _sourceId: number; + // _subs: any; - connectToSourceImpl(url: string): void { + connectToSourceImpl(url) { this._sourceId = EventSourceId++; RNEventSource.connect(url, this._sourceId); @@ -45,24 +45,24 @@ class EventSource extends EventSourceBase { this._registerEvents(this._sourceId); } - closeConnectionImpl(): void { + closeConnectionImpl() { this._closeEventSource(this._sourceId); } - cancelConnectionImpl(): void { + cancelConnectionImpl() { this._closeEventSource(this._sourceId); } - _closeEventSource(id: number): void { + _closeEventSource(id) { RNEventSource.close(id); } - _unregisterEvents(): void { + _unregisterEvents() { this._subs.forEach((e) => e.remove()); this._subs = []; } - _registerEvents(id: number): void { + _registerEvents(id) { this._subs = [ DeviceEventEmitter.addListener('eventsourceEvent', (ev) => { if (ev.id !== id) { diff --git a/src/platform/EventSource/EventSourceBase.ts b/src/platform/EventSource/EventSourceBase.ts index d9d06f3..9726aed 100644 --- a/src/platform/EventSource/EventSourceBase.ts +++ b/src/platform/EventSource/EventSourceBase.ts @@ -1,3 +1,4 @@ +// @ts-nocheck 'use strict'; // Package included by react-native @@ -6,20 +7,20 @@ var EventTarget = require('event-target-shim'); /** * Shared base for platform-specific EventSource implementations. */ -export class EventSourceBase extends EventTarget { - CONNECTING: number; - OPEN: number; - CLOSED: number; +class EventSourceBase extends EventTarget { + // CONNECTING: number; + // OPEN: number; + // CLOSED: number; - onerror?: Function; - onmessage?: Function; - onopen?: Function; + // onerror?: Function; + // onmessage?: Function; + // onopen?: Function; - binaryType?: string; - readyState: number; - url?: string; + // binaryType?: string; + // readyState: number; + // url?: string; - constructor(url: string) { + constructor(url) { super(); this.CONNECTING = 0; this.OPEN = 1; @@ -27,11 +28,10 @@ export class EventSourceBase extends EventTarget { this.url = url; this.readyState = this.CONNECTING; - // @ts-ignore this.connectToSourceImpl(url); } - close(): void { + close() { if (this.readyState === this.CLOSED) { return; } @@ -43,15 +43,15 @@ export class EventSourceBase extends EventTarget { this.closeConnectionImpl(); } - closeConnectionImpl(): void { + closeConnectionImpl() { throw new Error('Subclass must define closeConnectionImpl method'); } - connectToSourceImpl(): void { + connectToSourceImpl() { throw new Error('Subclass must define connectToSourceImpl method'); } - cancelConnectionImpl(): void { + cancelConnectionImpl() { throw new Error('Subclass must define cancelConnectionImpl method'); } } @@ -59,3 +59,5 @@ export class EventSourceBase extends EventTarget { EventSourceBase.CONNECTING = 0; EventSourceBase.OPEN = 1; EventSourceBase.CLOSED = 3; + +module.exports = EventSourceBase; diff --git a/src/platform/EventSource/EventSourceEvent.ts b/src/platform/EventSource/EventSourceEvent.ts index dd7807a..75a09d3 100644 --- a/src/platform/EventSource/EventSourceEvent.ts +++ b/src/platform/EventSource/EventSourceEvent.ts @@ -1,3 +1,4 @@ +// @ts-nocheck 'use strict'; /** @@ -8,11 +9,13 @@ * * In case of "message", the `data` property contains the incoming data. */ -export class EventSourceEvent { - type: string; +class EventSourceEvent { + // type: string; - constructor(type: any, eventInitDict: any) { + constructor(type, eventInitDict) { this.type = type.toString(); Object.assign(this, eventInitDict); } } + +module.exports = EventSourceEvent; diff --git a/src/platform/__tests__/getEventSource.nativeModule.spec.ts b/src/platform/__tests__/getEventSource.nativeModule.spec.ts new file mode 100644 index 0000000..01093ac --- /dev/null +++ b/src/platform/__tests__/getEventSource.nativeModule.spec.ts @@ -0,0 +1,34 @@ +// Mock RNEventSource native module +jest.mock('react-native', () => { + return { + NativeModules: { + RNEventSource: { + connect: jest.fn(), + close: jest.fn(), + }, + }, + DeviceEventEmitter: { + addListener: jest.fn(), + }, + }; +}); + +const EventSourceImpl = require('../EventSource/EventSource'); +const { + NativeModules: { RNEventSource }, +} = require('react-native'); + +// Test target: +import { getEventSource } from '../getEventSource'; + +test('Returns EventSource implementation if RNEventSource native module is available', () => { + const EventSource = getEventSource(); + expect(EventSource).toBe(EventSourceImpl); + + // @ts-expect-error. EventSource is not undefined + const connection = new EventSource('fake-url'); + expect(RNEventSource.connect).toBeCalledTimes(1); + + connection.close(); + expect(RNEventSource.close).toBeCalled(); +}); diff --git a/src/platform/__tests__/getEventSource.spec.ts b/src/platform/__tests__/getEventSource.spec.ts new file mode 100644 index 0000000..6511805 --- /dev/null +++ b/src/platform/__tests__/getEventSource.spec.ts @@ -0,0 +1,17 @@ +// No mocking RNEventSource native module. + +import { getEventSource } from '../getEventSource'; + +test('Returns global EventSource if native module RNEventSource is not available', () => { + const mockEventSource = jest.fn(); + const original = global.EventSource; // @ts-ignore + global.EventSource = mockEventSource; + + expect(getEventSource()).toBe(mockEventSource); + + global.EventSource = original; +}); + +test('Returns undefined if global EventSource and native module RNEventSource are not available', () => { + expect(getEventSource()).toBe(undefined); +}); diff --git a/src/platform/getEventSource.ts b/src/platform/getEventSource.ts index 3dce3cb..3a2503f 100644 --- a/src/platform/getEventSource.ts +++ b/src/platform/getEventSource.ts @@ -1,13 +1,13 @@ +/* eslint-disable no-undef */ import { NativeModules } from 'react-native'; -let RNEventSource: EventSource | undefined; -if (NativeModules.RNEventSource) RNEventSource = require('./EventSource/EventSource'); +let _RNEventSource: typeof EventSource | undefined; +if (NativeModules.RNEventSource) _RNEventSource = require('./EventSource/EventSource'); /** * Returns native implementation of EventSource. If not available (e.g., Expo or other runtime than Android and iOS), * checks if global EventSource is available and returns it. */ -export function getEventSource() { - // eslint-disable-next-line no-undef - return RNEventSource || typeof EventSource === 'function' ? EventSource : undefined; +export function getEventSource(): typeof EventSource | undefined { + return _RNEventSource || (typeof EventSource === 'function' ? EventSource : undefined); } diff --git a/src/settings/defaults.ts b/src/settings/defaults.ts index 8a7495f..7086314 100644 --- a/src/settings/defaults.ts +++ b/src/settings/defaults.ts @@ -1,4 +1,4 @@ -const packageVersion = '0.0.1-beta.3'; +const packageVersion = '0.0.1-beta.4'; export const defaults = { startup: { From f9e21c77be7367331dabaec0aa2ad79e883b5d0e Mon Sep 17 00:00:00 2001 From: Emiliano Sanchez Date: Thu, 15 Jul 2021 15:23:28 -0300 Subject: [PATCH 6/6] Emi's feedback --- .travis.yml | 2 +- .../jonnybgod/RNEventSource/RNEventSourceModule.java | 8 ++++---- src/platform/getModules.ts | 3 --- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9a88ea1..33d383c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,4 +16,4 @@ script: - npm run test - npm run build # @TODO remove next command and `publishConfig` field in package.json when publishing as a public package in NPM registry - - if [[ $TRAVIS_BRANCH == "event_source" && $TRAVIS_EVENT_TYPE != "pull_request" ]]; then echo "publishing" ; curl -u${ARTIFACTORY_USER}:${ARTIFACTORY_PASSWORD} https://splitio.jfrog.io/artifactory/api/npm/auth >> ~/.npmrc ; npm publish --tag rc ; else echo "no publishing" ; fi + - if [[ $TRAVIS_BRANCH == "development" && $TRAVIS_EVENT_TYPE != "pull_request" ]]; then echo "publishing" ; curl -u${ARTIFACTORY_USER}:${ARTIFACTORY_PASSWORD} https://splitio.jfrog.io/artifactory/api/npm/auth >> ~/.npmrc ; npm publish --tag rc ; else echo "no publishing" ; fi diff --git a/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java b/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java index f5ac4e3..b5c82ae 100644 --- a/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java +++ b/android/src/main/java/com/github/jonnybgod/RNEventSource/RNEventSourceModule.java @@ -65,7 +65,7 @@ public void connect(final String url, final int id) { public void run() { try { SseEventReader reader = source.getEventReader(); - + SseEventType type = null; while ((type = reader.next()) != SseEventType.EOS) { switch (type) { @@ -90,7 +90,7 @@ public void run() { break; } } - + notifyEventSourceFailed(id, "Connection with the event source was closed."); close(id); } @@ -115,7 +115,7 @@ public void run() { @ReactMethod public void close(int id) { SseEventSource source = mEventSourceConnections.get(id); - Thread thead = mEventReaderThreads.get(id); + Thread thread = mEventReaderThreads.get(id); if (source == null) { // EventSource is already closed // Don't do anything, mirror the behaviour on web @@ -126,7 +126,7 @@ public void close(int id) { return; } try { - thead.interrupt(); + thread.interrupt(); source.close(); mEventSourceConnections.remove(id); mEventReaderThreads.remove(id); diff --git a/src/platform/getModules.ts b/src/platform/getModules.ts index 774e161..c839925 100644 --- a/src/platform/getModules.ts +++ b/src/platform/getModules.ts @@ -5,7 +5,6 @@ import pollingManagerCSFactory from '@splitsoftware/splitio-commons/src/sync/pol import { sdkManagerFactory } from '@splitsoftware/splitio-commons/src/sdkManager/index'; import { sdkClientMethodCSFactory } from '@splitsoftware/splitio-commons/src/sdkClient/sdkClientMethodCS'; import { impressionObserverCSFactory } from '@splitsoftware/splitio-commons/src/trackers/impressionObserver/impressionObserverCS'; -// import integrationsManagerFactory from '@splitsoftware/splitio-commons/src/integrations/pluggable'; import EventEmitter from '@splitsoftware/splitio-commons/src/utils/MinEvents'; import { shouldAddPt } from '@splitsoftware/splitio-commons/src/trackers/impressionObserver/utils'; @@ -48,8 +47,6 @@ export function getModules(settings: ISettingsInternal): ISdkFactoryParams { // @ts-ignore impressionListener: settings.impressionListener, - // integrationsManagerFactory: settings.integrations && settings.integrations.length > 0 ? integrationsManagerFactory.bind(null, settings.integrations) : undefined, - impressionsObserverFactory: shouldAddPt(settings) ? impressionObserverCSFactory : undefined, }; }