diff --git a/Split.podspec b/Split.podspec
new file mode 100644
index 0000000..ad1948c
--- /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/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..b5c82ae
--- /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 thread = 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 {
+ thread.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}.
+ *
+ *
+ * @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:
+ *
+ *
scheme
default port
Sample locationDescription
+ *
http
80
foo.example.com or http://foo.example.com
+ *
ws
80
foo.example.com or ws://foo.example.com
+ *
https
443
https://foo.example.com
+ *
wss
443
wss://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:
+ *
+ *
{@code *}/ -- matches all requests to any host on port 80 (default port), with no user info or path specified.
+ *
{@code *.hostname.com:8000} -- matches all requests to port 8000 on any sub-domain of {@code hostname.com},
+ * but not {@code hostname.com} itself.
+ *
{@code server.hostname.com:*}/{@code *} -- matches all requests to a particular server on any port on any path but not the empty path.
+ *
+ * @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:
+ *
+ */
+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.
+ *
+ *
+ * @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.
+ *
+ *
One is not permitted to add values to the root node. Use a wildcard node instead.
+ *
One is not permitted to create Node instances with null or empty names.
+ *
+ *
+ * @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