diff options
Diffstat (limited to 'plugins/cordova-plugin-websocket/src/android')
78 files changed, 16625 insertions, 0 deletions
diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/ConnectionTask.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/ConnectionTask.java new file mode 100644 index 00000000..f2a3b138 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/ConnectionTask.java @@ -0,0 +1,150 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import java.net.URI; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; +import org.apache.cordova.PluginResult.Status; +import org.eclipse.jetty.websocket.PerMessageDeflateExtension; +import org.eclipse.jetty.websocket.WebSocket.Connection; +import org.eclipse.jetty.websocket.WebSocketClient; +import org.eclipse.jetty.websocket.WebSocketClientFactory; +import org.json.JSONArray; +import org.json.JSONObject; + +import com.knowledgecode.cordova.websocket.TaskRunner.Task; +import com.knowledgecode.cordova.websocket.WebSocketGenerator.OnCloseListener; +import com.knowledgecode.cordova.websocket.WebSocketGenerator.OnOpenListener; + +import android.util.SparseArray; +import android.webkit.CookieManager; + +/** + * Connect to server. + */ +class ConnectionTask implements Task { + + private static final long MAX_CONNECT_TIME = 75000; + private static final int MAX_TEXT_MESSAGE_SIZE = -1; + private static final int MAX_BINARY_MESSAGE_SIZE = -1; + + private final WebSocketClientFactory _factory; + private final SparseArray<Connection> _map; + + /** + * Constructor + * + * @param factory + * @param map + */ + public ConnectionTask(WebSocketClientFactory factory, SparseArray<Connection> map) { + _factory = factory; + _map = map; + + if (!_factory.isRunning()) { + try { + _factory.start(); + } catch (Exception e) { + } + } + } + + /** + * Set cookies, if any. + * + * @param cookies + * @param url + */ + private static void setCookie(Map<String, String> cookies, String url) { + String cookie = CookieManager.getInstance().getCookie(url); + + if (cookie != null) { + for (String c : cookie.split(";")) { + String[] pair = c.split("="); + + if (pair.length == 2) { + cookies.put(pair[0], pair[1]); + } + } + } + } + + @Override + public void execute(String rawArgs, CallbackContext ctx) { + try { + WebSocketClient client = _factory.newWebSocketClient(); + + JSONArray args = new JSONArray(rawArgs); + int id = Integer.parseInt(args.getString(0), 16); + URI uri = new URI(args.getString(1)); + String protocol = args.getString(2); + JSONObject options = args.getJSONObject(5); + String origin = options.optString("origin", args.getString(3)); + String agent = options.optString("agent", args.getString(4)); + boolean deflate = options.optBoolean("perMessageDeflate", true); + long maxConnectTime = options.optLong("maxConnectTime", MAX_CONNECT_TIME); + + client.setMaxTextMessageSize(options.optInt("maxTextMessageSize", MAX_TEXT_MESSAGE_SIZE)); + client.setMaxBinaryMessageSize(options.optInt("maxBinaryMessageSize", MAX_BINARY_MESSAGE_SIZE)); + if (protocol.length() > 0) { + client.setProtocol(protocol); + } + if (origin.length() > 0) { + client.setOrigin(origin); + } + if (agent.length() > 0) { + client.setAgent(agent); + } + if (deflate) { + client.getExtensions().add(new PerMessageDeflateExtension()); + } + + setCookie(client.getCookies(), uri.getHost()); + + WebSocketGenerator gen = new WebSocketGenerator(id, ctx); + + gen.setOnOpenListener(new OnOpenListener() { + @Override + public void onOpen(int id, Connection conn) { + _map.put(id, conn); + } + }); + gen.setOnCloseListener(new OnCloseListener() { + @Override + public void onClose(int id) { + if (_map.indexOfKey(id) >= 0) { + _map.remove(id); + } + } + }); + client.open(uri, gen, maxConnectTime, TimeUnit.MILLISECONDS); + } catch (Exception e) { + if (!ctx.isFinished()) { + PluginResult result = new PluginResult(Status.ERROR); + result.setKeepCallback(true); + ctx.sendPluginResult(result); + } + } + } +} + diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/DestroyTask.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/DestroyTask.java new file mode 100644 index 00000000..dcf13445 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/DestroyTask.java @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import org.apache.cordova.CallbackContext; +import org.eclipse.jetty.websocket.WebSocket.Connection; +import org.eclipse.jetty.websocket.WebSocketClientFactory; + +import com.knowledgecode.cordova.websocket.TaskRunner.Task; + +import android.util.SparseArray; + +/** + * Stop WebSocket client. + */ +class DestroyTask implements Task { + + private final WebSocketClientFactory _factory; + private final SparseArray<Connection> _map; + + /** + * Constructor + * + * @param factory + * @param map + */ + public DestroyTask(WebSocketClientFactory factory, SparseArray<Connection> map) { + _factory = factory; + _map = map; + } + + @Override + public void execute(String rawArgs, CallbackContext ctx) { + for (int i = 0; i < _map.size(); i++) { + int key = _map.keyAt(i); + + if (_map.get(key).isOpen()) { + _map.get(key).close(); + } + } + _map.clear(); + + if (_factory.isRunning()) { + try { + _factory.stop(); + } catch (Exception e) { + } + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/DisconnectionTask.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/DisconnectionTask.java new file mode 100644 index 00000000..18502488 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/DisconnectionTask.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; +import org.apache.cordova.PluginResult.Status; +import org.eclipse.jetty.websocket.WebSocket.Connection; +import org.json.JSONArray; + +import com.knowledgecode.cordova.websocket.TaskRunner.Task; + +import android.util.SparseArray; + +/** + * Close a connection. + */ +class DisconnectionTask implements Task { + + private final SparseArray<Connection> _map; + + /** + * Constructor + * + * @param map + */ + public DisconnectionTask(SparseArray<Connection> map) { + _map = map; + } + + @Override + public void execute(String rawArgs, CallbackContext ctx) { + try { + JSONArray args = new JSONArray(rawArgs); + int id = Integer.parseInt(args.getString(0), 16); + int code = args.getInt(1); + String reason = args.getString(2); + Connection conn = _map.get(id); + + if (conn != null) { + if (code > 0) { + conn.close(code, reason); + } else { + conn.close(); + } + } + } catch (Exception e) { + if (!ctx.isFinished()) { + PluginResult result = new PluginResult(Status.ERROR); + result.setKeepCallback(true); + ctx.sendPluginResult(result); + } + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/ResetTask.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/ResetTask.java new file mode 100644 index 00000000..16d4267b --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/ResetTask.java @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import org.apache.cordova.CallbackContext; +import org.eclipse.jetty.websocket.WebSocket.Connection; + +import com.knowledgecode.cordova.websocket.TaskRunner.Task; + +import android.util.SparseArray; + +/** + * Close all connections. + */ +class ResetTask implements Task { + + private final SparseArray<Connection> _map; + + /** + * Constructor + * + * @param map + */ + public ResetTask(SparseArray<Connection> map) { + _map = map; + } + + @Override + public void execute(String rawArgs, CallbackContext ctx) { + for (int i = 0; i < _map.size(); i++) { + int key = _map.keyAt(i); + + if (_map.get(key).isOpen()) { + _map.get(key).close(); + } + } + _map.clear(); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/SendingTask.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/SendingTask.java new file mode 100644 index 00000000..ae73da2b --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/SendingTask.java @@ -0,0 +1,69 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; +import org.apache.cordova.PluginResult.Status; +import org.eclipse.jetty.websocket.WebSocket.Connection; + +import com.knowledgecode.cordova.websocket.TaskRunner.Task; + +import android.util.Base64; +import android.util.SparseArray; + +/** + * Send text/binary data. + */ +class SendingTask implements Task { + + private final SparseArray<Connection> _map; + + /** + * Constructor + * + * @param map + */ + public SendingTask(SparseArray<Connection> map) { + _map = map; + } + + @Override + public void execute(String rawArgs, CallbackContext ctx) { + try { + Connection conn = _map.get(Integer.parseInt(rawArgs.substring(2, 10), 16)); + + if (conn != null) { + if (rawArgs.charAt(10) == '1') { + byte[] binary = Base64.decode(rawArgs.substring(rawArgs.indexOf(',') + 1, rawArgs.length() - 2), + Base64.NO_WRAP); + conn.sendMessage(binary, 0, binary.length); + } else { + conn.sendMessage(rawArgs.substring(11, rawArgs.length() - 2)); + } + } + } catch (Exception e) { + if (!ctx.isFinished()) { + PluginResult result = new PluginResult(Status.ERROR); + result.setKeepCallback(true); + ctx.sendPluginResult(result); + } + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/TaskBean.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/TaskBean.java new file mode 100644 index 00000000..444f3824 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/TaskBean.java @@ -0,0 +1,33 @@ +package com.knowledgecode.cordova.websocket; + +import org.apache.cordova.CallbackContext; + +class TaskBean { + private final String _action; + private final String _rawArgs; + private final CallbackContext _ctx; + + public TaskBean(final String action) { + _action = action; + _rawArgs = "[]"; + _ctx = null; + } + + public TaskBean(final String action, final String rawArgs, final CallbackContext ctx) { + _action = action; + _rawArgs = rawArgs; + _ctx = ctx; + } + + public String getAction() { + return _action; + } + + public String getRawArgs() { + return _rawArgs; + } + + public CallbackContext getCtx() { + return _ctx; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/TaskRunner.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/TaskRunner.java new file mode 100644 index 00000000..9fe2ad59 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/TaskRunner.java @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.LinkedBlockingQueue; + +import org.apache.cordova.CallbackContext; + +class TaskRunner implements Runnable { + + interface Task { + public void execute(String rawArgs, CallbackContext ctx); + } + + private BlockingQueue<TaskBean> _queue; + private Map<String, Task> _map; + + public TaskRunner() { + _queue = new LinkedBlockingQueue<TaskBean>(); + _map = new HashMap<String, Task>(); + } + + public void setTask(String action, Task task) { + _map.put(action, task); + } + + public boolean addTaskQueue(TaskBean bean) { + try { + _queue.put(bean); + } catch (InterruptedException e) { + return false; + } + return true; + } + + @Override + public void run() { + while (true) { + TaskBean task; + + try { + task = _queue.take(); + } catch (InterruptedException e) { + break; + } + String action = task.getAction(); + + _map.get(action).execute(task.getRawArgs(), task.getCtx()); + if (WebSocket.DESTROY_TASK.equals(action)) { + break; + } + } + _queue.clear(); + _map.clear(); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/WebSocket.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/WebSocket.java new file mode 100644 index 00000000..479de7ee --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/WebSocket.java @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; + +import org.apache.cordova.CordovaWebView; +import org.apache.cordova.CallbackContext; +import org.apache.cordova.CordovaInterface; +import org.apache.cordova.CordovaPlugin; +import org.eclipse.jetty.websocket.WebSocket.Connection; +import org.eclipse.jetty.websocket.WebSocketClientFactory; + +import android.util.SparseArray; + +/** + * Cordova WebSocket Plugin for Android + * This plugin is using Jetty under the terms of the Apache License v2.0. + * + * @author KNOWLEDGECODE <knowledgecode@gmail.com> + * @version 0.11.0 + */ +public class WebSocket extends CordovaPlugin { + + static final String CREATE_TASK = "create"; + static final String SEND_TASK = "send"; + static final String CLOSE_TASK = "close"; + static final String RESET_TASK = "reset"; + static final String DESTROY_TASK = "destroy"; + + private WebSocketClientFactory _factory; + private SparseArray<Connection> _conn; + private ExecutorService _executor; + private TaskRunner _runner; + + @Override + public void initialize(CordovaInterface cordova, final CordovaWebView webView) { + super.initialize(cordova, webView); + _factory = new WebSocketClientFactory(); + _conn = new SparseArray<Connection>(); + _executor = Executors.newSingleThreadExecutor(); + _runner = new TaskRunner(); + _runner.setTask(CREATE_TASK, new ConnectionTask(_factory, _conn)); + _runner.setTask(SEND_TASK, new SendingTask(_conn)); + _runner.setTask(CLOSE_TASK, new DisconnectionTask(_conn)); + _runner.setTask(RESET_TASK, new ResetTask(_conn)); + _runner.setTask(DESTROY_TASK, new DestroyTask(_factory, _conn)); + _executor.execute(_runner); + } + + @Override + public boolean execute(String action, String rawArgs, CallbackContext ctx) { + return _runner.addTaskQueue(new TaskBean(action, rawArgs, ctx)); + }; + + @Override + public void onReset() { + _runner.addTaskQueue(new TaskBean(RESET_TASK)); + super.onReset(); + } + + @Override + public void onDestroy() { + _runner.addTaskQueue(new TaskBean(DESTROY_TASK)); + _executor.shutdown(); + try { + _executor.awaitTermination(Long.MAX_VALUE, TimeUnit.MILLISECONDS); + } catch (InterruptedException e) { + } + super.onDestroy(); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/WebSocketGenerator.java b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/WebSocketGenerator.java new file mode 100644 index 00000000..3132240a --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/com/knowledgecode/cordova/websocket/WebSocketGenerator.java @@ -0,0 +1,128 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package com.knowledgecode.cordova.websocket; + +import org.apache.cordova.CallbackContext; +import org.apache.cordova.PluginResult; +import org.apache.cordova.PluginResult.Status; + +import android.util.Base64; + +class WebSocketGenerator implements + org.eclipse.jetty.websocket.WebSocket.OnTextMessage, + org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage { + + interface OnOpenListener { + public void onOpen(int id, Connection conn); + } + + interface OnCloseListener { + public void onClose(int id); + } + + private final int _id; + private final CallbackContext _ctx; + private OnOpenListener _openListener; + private OnCloseListener _closeListener; + + /** + * Constructor + * + * @param id + * @param ctx + */ + public WebSocketGenerator(int id, CallbackContext ctx) { + _id = id; + _ctx = ctx; + _openListener = new OnOpenListener() { + @Override + public void onOpen(int id, Connection conn) { + // NOP + } + }; + _closeListener = new OnCloseListener() { + @Override + public void onClose(int id) { + // NOP + } + }; + } + + /** + * Set OnOpen listener. + * + * @param l + */ + public void setOnOpenListener(OnOpenListener l) { + _openListener = l; + } + + /** + * Set OnClose listener. + * + * @param l + */ + public void setOnCloseListener(OnCloseListener l) { + _closeListener = l; + } + + @Override + public void onOpen(Connection conn) { + _openListener.onOpen(_id, conn); + + String protocol = conn.getProtocol(); + String extensions = conn.getExtensions(); + protocol = protocol == null ? "" : protocol; + extensions = extensions == null ? "" : extensions; + sendCallback(String.format("O[\"%s\",\"%s\"]", protocol, extensions), true); + } + + @Override + public void onMessage(String data) { + sendCallback("T" + data, true); + } + + @Override + public void onMessage(byte[] data, int offset, int length) { + sendCallback("B" + Base64.encodeToString(data, offset, length, Base64.NO_WRAP), true); + } + + @Override + public void onClose(int code, String reason) { + _closeListener.onClose(_id); + + String wasClean = code == 1000 ? "1" : "0"; + reason = reason == null ? "" : reason; + sendCallback(String.format("C%s%4d%s", wasClean, code, reason), false); + } + + /** + * Send plugin result. + * + * @param callbackString + * @param keepCallback + */ + private void sendCallback(String callbackString, boolean keepCallback) { + if (!_ctx.isFinished()) { + PluginResult result = new PluginResult(Status.OK, callbackString); + result.setKeepCallback(keepCallback); + _ctx.sendPluginResult(result); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpException.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpException.java new file mode 100644 index 00000000..4ae60c02 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpException.java @@ -0,0 +1,49 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.io.IOException; + +@SuppressWarnings("serial") +public class HttpException extends IOException +{ + int _status; + String _reason; + + /* ------------------------------------------------------------ */ + public HttpException(int status) + { + _status=status; + _reason=null; + } + + /* ------------------------------------------------------------ */ + public HttpException(int status,String reason) + { + _status=status; + _reason=reason; + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return ("HttpException("+_status+","+_reason+","+super.getCause()+")"); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpHeaderValues.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpHeaderValues.java new file mode 100644 index 00000000..c8d5ed58 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpHeaderValues.java @@ -0,0 +1,75 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferCache; +import org.eclipse.jetty.io.ByteArrayBuffer; + +/** + * Cached HTTP Header values. + * This class caches the conversion of common HTTP Header values to and from {@link ByteArrayBuffer} instances. + * The resource "/org/eclipse/jetty/useragents" is checked for a list of common user agents, so that repeated + * creation of strings for these agents can be avoided. + * + * + */ +public class HttpHeaderValues extends BufferCache +{ + public final static String + CLOSE="close", + CHUNKED="chunked", + GZIP="gzip", + IDENTITY="identity", + KEEP_ALIVE="keep-alive", + CONTINUE="100-continue", + PROCESSING="102-processing", + TE="TE", + BYTES="bytes", + NO_CACHE="no-cache", + UPGRADE="Upgrade"; + + public final static int + CLOSE_ORDINAL=1, + CHUNKED_ORDINAL=2, + GZIP_ORDINAL=3, + IDENTITY_ORDINAL=4, + KEEP_ALIVE_ORDINAL=5, + CONTINUE_ORDINAL=6, + PROCESSING_ORDINAL=7, + TE_ORDINAL=8, + BYTES_ORDINAL=9, + NO_CACHE_ORDINAL=10, + UPGRADE_ORDINAL=11; + + public final static HttpHeaderValues CACHE= new HttpHeaderValues(); + + public final static Buffer + CLOSE_BUFFER=CACHE.add(CLOSE,CLOSE_ORDINAL), + CHUNKED_BUFFER=CACHE.add(CHUNKED,CHUNKED_ORDINAL), + GZIP_BUFFER=CACHE.add(GZIP,GZIP_ORDINAL), + IDENTITY_BUFFER=CACHE.add(IDENTITY,IDENTITY_ORDINAL), + KEEP_ALIVE_BUFFER=CACHE.add(KEEP_ALIVE,KEEP_ALIVE_ORDINAL), + CONTINUE_BUFFER=CACHE.add(CONTINUE, CONTINUE_ORDINAL), + PROCESSING_BUFFER=CACHE.add(PROCESSING, PROCESSING_ORDINAL), + TE_BUFFER=CACHE.add(TE,TE_ORDINAL), + BYTES_BUFFER=CACHE.add(BYTES,BYTES_ORDINAL), + NO_CACHE_BUFFER=CACHE.add(NO_CACHE,NO_CACHE_ORDINAL), + UPGRADE_BUFFER=CACHE.add(UPGRADE,UPGRADE_ORDINAL); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpHeaders.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpHeaders.java new file mode 100644 index 00000000..87f5d1e3 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpHeaders.java @@ -0,0 +1,238 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferCache; + +/* ------------------------------------------------------------------------------- */ +/** + */ +public class HttpHeaders extends BufferCache +{ + /* ------------------------------------------------------------ */ + /** General Fields. + */ + public final static String + CONNECTION= "Connection", + CACHE_CONTROL= "Cache-Control", + DATE= "Date", + PRAGMA= "Pragma", + PROXY_CONNECTION = "Proxy-Connection", + TRAILER= "Trailer", + TRANSFER_ENCODING= "Transfer-Encoding", + UPGRADE= "Upgrade", + VIA= "Via", + WARNING= "Warning", + NEGOTIATE= "Negotiate"; + + /* ------------------------------------------------------------ */ + /** Entity Fields. + */ + public final static String ALLOW= "Allow", + CONTENT_ENCODING= "Content-Encoding", + CONTENT_LANGUAGE= "Content-Language", + CONTENT_LENGTH= "Content-Length", + CONTENT_LOCATION= "Content-Location", + CONTENT_MD5= "Content-MD5", + CONTENT_RANGE= "Content-Range", + CONTENT_TYPE= "Content-Type", + EXPIRES= "Expires", + LAST_MODIFIED= "Last-Modified"; + + /* ------------------------------------------------------------ */ + /** Request Fields. + */ + public final static String ACCEPT= "Accept", + ACCEPT_CHARSET= "Accept-Charset", + ACCEPT_ENCODING= "Accept-Encoding", + ACCEPT_LANGUAGE= "Accept-Language", + AUTHORIZATION= "Authorization", + EXPECT= "Expect", + FORWARDED= "Forwarded", + FROM= "From", + HOST= "Host", + IF_MATCH= "If-Match", + IF_MODIFIED_SINCE= "If-Modified-Since", + IF_NONE_MATCH= "If-None-Match", + IF_RANGE= "If-Range", + IF_UNMODIFIED_SINCE= "If-Unmodified-Since", + KEEP_ALIVE= "Keep-Alive", + MAX_FORWARDS= "Max-Forwards", + PROXY_AUTHORIZATION= "Proxy-Authorization", + RANGE= "Range", + REQUEST_RANGE= "Request-Range", + REFERER= "Referer", + TE= "TE", + USER_AGENT= "User-Agent", + X_FORWARDED_FOR= "X-Forwarded-For", + X_FORWARDED_PROTO= "X-Forwarded-Proto", + X_FORWARDED_SERVER= "X-Forwarded-Server", + X_FORWARDED_HOST= "X-Forwarded-Host"; + + /* ------------------------------------------------------------ */ + /** Response Fields. + */ + public final static String ACCEPT_RANGES= "Accept-Ranges", + AGE= "Age", + ETAG= "ETag", + LOCATION= "Location", + PROXY_AUTHENTICATE= "Proxy-Authenticate", + RETRY_AFTER= "Retry-After", + SERVER= "Server", + SERVLET_ENGINE= "Servlet-Engine", + VARY= "Vary", + WWW_AUTHENTICATE= "WWW-Authenticate"; + + /* ------------------------------------------------------------ */ + /** Other Fields. + */ + public final static String COOKIE= "Cookie", + SET_COOKIE= "Set-Cookie", + SET_COOKIE2= "Set-Cookie2", + MIME_VERSION= "MIME-Version", + IDENTITY= "identity"; + + public final static int CONNECTION_ORDINAL= 1, + DATE_ORDINAL= 2, + PRAGMA_ORDINAL= 3, + TRAILER_ORDINAL= 4, + TRANSFER_ENCODING_ORDINAL= 5, + UPGRADE_ORDINAL= 6, + VIA_ORDINAL= 7, + WARNING_ORDINAL= 8, + ALLOW_ORDINAL= 9, + CONTENT_ENCODING_ORDINAL= 10, + CONTENT_LANGUAGE_ORDINAL= 11, + CONTENT_LENGTH_ORDINAL= 12, + CONTENT_LOCATION_ORDINAL= 13, + CONTENT_MD5_ORDINAL= 14, + CONTENT_RANGE_ORDINAL= 15, + CONTENT_TYPE_ORDINAL= 16, + EXPIRES_ORDINAL= 17, + LAST_MODIFIED_ORDINAL= 18, + ACCEPT_ORDINAL= 19, + ACCEPT_CHARSET_ORDINAL= 20, + ACCEPT_ENCODING_ORDINAL= 21, + ACCEPT_LANGUAGE_ORDINAL= 22, + AUTHORIZATION_ORDINAL= 23, + EXPECT_ORDINAL= 24, + FORWARDED_ORDINAL= 25, + FROM_ORDINAL= 26, + HOST_ORDINAL= 27, + IF_MATCH_ORDINAL= 28, + IF_MODIFIED_SINCE_ORDINAL= 29, + IF_NONE_MATCH_ORDINAL= 30, + IF_RANGE_ORDINAL= 31, + IF_UNMODIFIED_SINCE_ORDINAL= 32, + KEEP_ALIVE_ORDINAL= 33, + MAX_FORWARDS_ORDINAL= 34, + PROXY_AUTHORIZATION_ORDINAL= 35, + RANGE_ORDINAL= 36, + REQUEST_RANGE_ORDINAL= 37, + REFERER_ORDINAL= 38, + TE_ORDINAL= 39, + USER_AGENT_ORDINAL= 40, + X_FORWARDED_FOR_ORDINAL= 41, + ACCEPT_RANGES_ORDINAL= 42, + AGE_ORDINAL= 43, + ETAG_ORDINAL= 44, + LOCATION_ORDINAL= 45, + PROXY_AUTHENTICATE_ORDINAL= 46, + RETRY_AFTER_ORDINAL= 47, + SERVER_ORDINAL= 48, + SERVLET_ENGINE_ORDINAL= 49, + VARY_ORDINAL= 50, + WWW_AUTHENTICATE_ORDINAL= 51, + COOKIE_ORDINAL= 52, + SET_COOKIE_ORDINAL= 53, + SET_COOKIE2_ORDINAL= 54, + MIME_VERSION_ORDINAL= 55, + IDENTITY_ORDINAL= 56, + CACHE_CONTROL_ORDINAL=57, + PROXY_CONNECTION_ORDINAL=58, + X_FORWARDED_PROTO_ORDINAL=59, + X_FORWARDED_SERVER_ORDINAL=60, + X_FORWARDED_HOST_ORDINAL=61; + + public final static HttpHeaders CACHE= new HttpHeaders(); + + public final static Buffer + HOST_BUFFER=CACHE.add(HOST,HOST_ORDINAL), + ACCEPT_BUFFER=CACHE.add(ACCEPT,ACCEPT_ORDINAL), + ACCEPT_CHARSET_BUFFER=CACHE.add(ACCEPT_CHARSET,ACCEPT_CHARSET_ORDINAL), + ACCEPT_ENCODING_BUFFER=CACHE.add(ACCEPT_ENCODING,ACCEPT_ENCODING_ORDINAL), + ACCEPT_LANGUAGE_BUFFER=CACHE.add(ACCEPT_LANGUAGE,ACCEPT_LANGUAGE_ORDINAL), + CONTENT_LENGTH_BUFFER=CACHE.add(CONTENT_LENGTH,CONTENT_LENGTH_ORDINAL), + CONNECTION_BUFFER=CACHE.add(CONNECTION,CONNECTION_ORDINAL), + CACHE_CONTROL_BUFFER=CACHE.add(CACHE_CONTROL,CACHE_CONTROL_ORDINAL), + DATE_BUFFER=CACHE.add(DATE,DATE_ORDINAL), + PRAGMA_BUFFER=CACHE.add(PRAGMA,PRAGMA_ORDINAL), + TRAILER_BUFFER=CACHE.add(TRAILER,TRAILER_ORDINAL), + TRANSFER_ENCODING_BUFFER=CACHE.add(TRANSFER_ENCODING,TRANSFER_ENCODING_ORDINAL), + UPGRADE_BUFFER=CACHE.add(UPGRADE,UPGRADE_ORDINAL), + VIA_BUFFER=CACHE.add(VIA,VIA_ORDINAL), + WARNING_BUFFER=CACHE.add(WARNING,WARNING_ORDINAL), + ALLOW_BUFFER=CACHE.add(ALLOW,ALLOW_ORDINAL), + CONTENT_ENCODING_BUFFER=CACHE.add(CONTENT_ENCODING,CONTENT_ENCODING_ORDINAL), + CONTENT_LANGUAGE_BUFFER=CACHE.add(CONTENT_LANGUAGE,CONTENT_LANGUAGE_ORDINAL), + CONTENT_LOCATION_BUFFER=CACHE.add(CONTENT_LOCATION,CONTENT_LOCATION_ORDINAL), + CONTENT_MD5_BUFFER=CACHE.add(CONTENT_MD5,CONTENT_MD5_ORDINAL), + CONTENT_RANGE_BUFFER=CACHE.add(CONTENT_RANGE,CONTENT_RANGE_ORDINAL), + CONTENT_TYPE_BUFFER=CACHE.add(CONTENT_TYPE,CONTENT_TYPE_ORDINAL), + EXPIRES_BUFFER=CACHE.add(EXPIRES,EXPIRES_ORDINAL), + LAST_MODIFIED_BUFFER=CACHE.add(LAST_MODIFIED,LAST_MODIFIED_ORDINAL), + AUTHORIZATION_BUFFER=CACHE.add(AUTHORIZATION,AUTHORIZATION_ORDINAL), + EXPECT_BUFFER=CACHE.add(EXPECT,EXPECT_ORDINAL), + FORWARDED_BUFFER=CACHE.add(FORWARDED,FORWARDED_ORDINAL), + FROM_BUFFER=CACHE.add(FROM,FROM_ORDINAL), + IF_MATCH_BUFFER=CACHE.add(IF_MATCH,IF_MATCH_ORDINAL), + IF_MODIFIED_SINCE_BUFFER=CACHE.add(IF_MODIFIED_SINCE,IF_MODIFIED_SINCE_ORDINAL), + IF_NONE_MATCH_BUFFER=CACHE.add(IF_NONE_MATCH,IF_NONE_MATCH_ORDINAL), + IF_RANGE_BUFFER=CACHE.add(IF_RANGE,IF_RANGE_ORDINAL), + IF_UNMODIFIED_SINCE_BUFFER=CACHE.add(IF_UNMODIFIED_SINCE,IF_UNMODIFIED_SINCE_ORDINAL), + KEEP_ALIVE_BUFFER=CACHE.add(KEEP_ALIVE,KEEP_ALIVE_ORDINAL), + MAX_FORWARDS_BUFFER=CACHE.add(MAX_FORWARDS,MAX_FORWARDS_ORDINAL), + PROXY_AUTHORIZATION_BUFFER=CACHE.add(PROXY_AUTHORIZATION,PROXY_AUTHORIZATION_ORDINAL), + RANGE_BUFFER=CACHE.add(RANGE,RANGE_ORDINAL), + REQUEST_RANGE_BUFFER=CACHE.add(REQUEST_RANGE,REQUEST_RANGE_ORDINAL), + REFERER_BUFFER=CACHE.add(REFERER,REFERER_ORDINAL), + TE_BUFFER=CACHE.add(TE,TE_ORDINAL), + USER_AGENT_BUFFER=CACHE.add(USER_AGENT,USER_AGENT_ORDINAL), + X_FORWARDED_FOR_BUFFER=CACHE.add(X_FORWARDED_FOR,X_FORWARDED_FOR_ORDINAL), + X_FORWARDED_PROTO_BUFFER=CACHE.add(X_FORWARDED_PROTO,X_FORWARDED_PROTO_ORDINAL), + X_FORWARDED_SERVER_BUFFER=CACHE.add(X_FORWARDED_SERVER,X_FORWARDED_SERVER_ORDINAL), + X_FORWARDED_HOST_BUFFER=CACHE.add(X_FORWARDED_HOST,X_FORWARDED_HOST_ORDINAL), + ACCEPT_RANGES_BUFFER=CACHE.add(ACCEPT_RANGES,ACCEPT_RANGES_ORDINAL), + AGE_BUFFER=CACHE.add(AGE,AGE_ORDINAL), + ETAG_BUFFER=CACHE.add(ETAG,ETAG_ORDINAL), + LOCATION_BUFFER=CACHE.add(LOCATION,LOCATION_ORDINAL), + PROXY_AUTHENTICATE_BUFFER=CACHE.add(PROXY_AUTHENTICATE,PROXY_AUTHENTICATE_ORDINAL), + RETRY_AFTER_BUFFER=CACHE.add(RETRY_AFTER,RETRY_AFTER_ORDINAL), + SERVER_BUFFER=CACHE.add(SERVER,SERVER_ORDINAL), + SERVLET_ENGINE_BUFFER=CACHE.add(SERVLET_ENGINE,SERVLET_ENGINE_ORDINAL), + VARY_BUFFER=CACHE.add(VARY,VARY_ORDINAL), + WWW_AUTHENTICATE_BUFFER=CACHE.add(WWW_AUTHENTICATE,WWW_AUTHENTICATE_ORDINAL), + COOKIE_BUFFER=CACHE.add(COOKIE,COOKIE_ORDINAL), + SET_COOKIE_BUFFER=CACHE.add(SET_COOKIE,SET_COOKIE_ORDINAL), + SET_COOKIE2_BUFFER=CACHE.add(SET_COOKIE2,SET_COOKIE2_ORDINAL), + MIME_VERSION_BUFFER=CACHE.add(MIME_VERSION,MIME_VERSION_ORDINAL), + IDENTITY_BUFFER=CACHE.add(IDENTITY,IDENTITY_ORDINAL), + PROXY_CONNECTION_BUFFER=CACHE.add(PROXY_CONNECTION,PROXY_CONNECTION_ORDINAL); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpMethods.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpMethods.java new file mode 100644 index 00000000..2c91fde9 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpMethods.java @@ -0,0 +1,63 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferCache; + +/* ------------------------------------------------------------------------------- */ +/** + * + * + */ +public class HttpMethods +{ + public final static String GET= "GET", + POST= "POST", + HEAD= "HEAD", + PUT= "PUT", + OPTIONS= "OPTIONS", + DELETE= "DELETE", + TRACE= "TRACE", + CONNECT= "CONNECT", + MOVE= "MOVE"; + + public final static int GET_ORDINAL= 1, + POST_ORDINAL= 2, + HEAD_ORDINAL= 3, + PUT_ORDINAL= 4, + OPTIONS_ORDINAL= 5, + DELETE_ORDINAL= 6, + TRACE_ORDINAL= 7, + CONNECT_ORDINAL= 8, + MOVE_ORDINAL= 9; + + public final static BufferCache CACHE= new BufferCache(); + + public final static Buffer + GET_BUFFER= CACHE.add(GET, GET_ORDINAL), + POST_BUFFER= CACHE.add(POST, POST_ORDINAL), + HEAD_BUFFER= CACHE.add(HEAD, HEAD_ORDINAL), + PUT_BUFFER= CACHE.add(PUT, PUT_ORDINAL), + OPTIONS_BUFFER= CACHE.add(OPTIONS, OPTIONS_ORDINAL), + DELETE_BUFFER= CACHE.add(DELETE, DELETE_ORDINAL), + TRACE_BUFFER= CACHE.add(TRACE, TRACE_ORDINAL), + CONNECT_BUFFER= CACHE.add(CONNECT, CONNECT_ORDINAL), + MOVE_BUFFER= CACHE.add(MOVE, MOVE_ORDINAL); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpParser.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpParser.java new file mode 100644 index 00000000..0547ea93 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpParser.java @@ -0,0 +1,1033 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.util.Locale; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferCache.CachedBuffer; +import org.eclipse.jetty.io.BufferUtil; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.View; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class HttpParser implements Parser +{ + private static final Logger LOG = Log.getLogger(HttpParser.class); + + // States + public static final int STATE_START=-14; + public static final int STATE_FIELD0=-13; + public static final int STATE_SPACE1=-12; + public static final int STATE_STATUS=-11; + public static final int STATE_URI=-10; + public static final int STATE_SPACE2=-9; + public static final int STATE_END0=-8; + public static final int STATE_END1=-7; + public static final int STATE_FIELD2=-6; + public static final int STATE_HEADER=-5; + public static final int STATE_HEADER_NAME=-4; + public static final int STATE_HEADER_IN_NAME=-3; + public static final int STATE_HEADER_VALUE=-2; + public static final int STATE_HEADER_IN_VALUE=-1; + public static final int STATE_END=0; + public static final int STATE_EOF_CONTENT=1; + public static final int STATE_CONTENT=2; + public static final int STATE_CHUNKED_CONTENT=3; + public static final int STATE_CHUNK_SIZE=4; + public static final int STATE_CHUNK_PARAMS=5; + public static final int STATE_CHUNK=6; + public static final int STATE_SEEKING_EOF=7; + + public static final Charset __ISO_8859_1 = Charset.forName("ISO-8859-1"); + + public static final int BAD_REQUEST_400 = 400; + public static final int REQUEST_ENTITY_TOO_LARGE_413 = 413; + + static final byte COLON= (byte)':'; + static final byte SPACE= 0x20; + static final byte CARRIAGE_RETURN= 0x0D; + static final byte LINE_FEED= 0x0A; + static final byte SEMI_COLON= (byte)';'; + static final byte TAB= 0x09; + + public static final int UNKNOWN_CONTENT= -3; + public static final int CHUNKED_CONTENT= -2; + public static final int EOF_CONTENT= -1; + public static final int NO_CONTENT= 0; + + private final EventHandler _handler; + private final Buffers _buffers; // source of buffers + private final EndPoint _endp; + private Buffer _header; // Buffer for header data (and small _content) + private Buffer _body; // Buffer for large content + private Buffer _buffer; // The current buffer in use (either _header or _content) + private CachedBuffer _cached; + private final View.CaseInsensitive _tok0; // Saved token: header name, request method or response version + private final View.CaseInsensitive _tok1; // Saved token: header value, request URI or response code + private String _multiLineValue; + private int _responseStatus; // If >0 then we are parsing a response + private boolean _forceContentBuffer; + private boolean _persistent; + + /* ------------------------------------------------------------------------------- */ + protected final View _contentView=new View(); // View of the content in the buffer for {@link Input} + protected int _state=STATE_START; + protected byte _eol; + protected int _length; + protected long _contentLength; + protected long _contentPosition; + protected int _chunkLength; + protected int _chunkPosition; + private boolean _headResponse; + + /* ------------------------------------------------------------------------------- */ + /** + * Constructor. + * @param buffers the buffers to use + * @param endp the endpoint + * @param handler the even handler + */ + public HttpParser(Buffers buffers, EndPoint endp, EventHandler handler) + { + _buffers=buffers; + _endp=endp; + _handler=handler; + _tok0=new View.CaseInsensitive(); + _tok1=new View.CaseInsensitive(); + } + + /* ------------------------------------------------------------ */ + public boolean isIdle() + { + return isState(STATE_START); + } + + /* ------------------------------------------------------------ */ + public boolean isComplete() + { + if (_responseStatus > 0) + return isState(STATE_END) || isState(STATE_SEEKING_EOF); + return isState(STATE_END); + } + + /* ------------------------------------------------------------------------------- */ + public boolean isState(int state) + { + return _state == state; + } + + /* ------------------------------------------------------------------------------- */ + /** + * Parse until END state. + * This method will parse any remaining content in the current buffer as long as there is + * no unconsumed content. It does not care about the {@link #getState current state} of the parser. + * @see #parse + * @see #parseNext + */ + public boolean parseAvailable() throws IOException + { + boolean progress=parseNext()>0; + + // continue parsing + while (!isComplete() && _buffer!=null && _buffer.length()>0 && !_contentView.hasContent()) + { + progress |= parseNext()>0; + } + return progress; + } + + /* ------------------------------------------------------------------------------- */ + /** + * Parse until next Event. + * @return an indication of progress <0 EOF, 0 no progress, >0 progress. + */ + public int parseNext() throws IOException + { + try + { + int progress=0; + + if (_state == STATE_END) + return 0; + + if (_buffer==null) + _buffer=getHeaderBuffer(); + + + if (_state == STATE_CONTENT && _contentPosition == _contentLength) + { + _state=STATE_END; + _handler.messageComplete(_contentPosition); + return 1; + } + + int length=_buffer.length(); + + // Fill buffer if we can + if (length == 0) + { + int filled=-1; + IOException ex=null; + try + { + filled=fill(); + LOG.debug("filled {}/{}",filled,_buffer.length()); + } + catch(IOException e) + { + LOG.debug(this.toString(),e); + ex=e; + } + + if (filled > 0 ) + progress++; + else if (filled < 0 ) + { + _persistent=false; + + // do we have content to deliver? + if (_state>STATE_END) + { + if (_buffer.length()>0 && !_headResponse) + { + Buffer chunk=_buffer.get(_buffer.length()); + _contentPosition += chunk.length(); + _contentView.update(chunk); + _handler.content(chunk); // May recurse here + } + } + + // was this unexpected? + switch(_state) + { + case STATE_END: + case STATE_SEEKING_EOF: + _state=STATE_END; + break; + + case STATE_EOF_CONTENT: + _state=STATE_END; + _handler.messageComplete(_contentPosition); + break; + + default: + _state=STATE_END; + if (!_headResponse) + _handler.earlyEOF(); + _handler.messageComplete(_contentPosition); + } + + if (ex!=null) + throw ex; + + if (!isComplete() && !isIdle()) + throw new EofException(); + + return -1; + } + length=_buffer.length(); + } + + + // Handle header states + byte ch; + byte[] array=_buffer.array(); + int last=_state; + while (_state<STATE_END && length-->0) + { + if (last!=_state) + { + progress++; + last=_state; + } + + ch=_buffer.get(); + + if (_eol == CARRIAGE_RETURN) + { + if (ch == LINE_FEED) + { + _eol=LINE_FEED; + continue; + } + throw new HttpException(BAD_REQUEST_400); + } + _eol=0; + + switch (_state) + { + case STATE_START: + _contentLength=UNKNOWN_CONTENT; + _cached=null; + if (ch > SPACE || ch<0) + { + _buffer.mark(); + _state=STATE_FIELD0; + } + break; + + case STATE_FIELD0: + if (ch == SPACE) + { + _tok0.update(_buffer.markIndex(), _buffer.getIndex() - 1); + _responseStatus=HttpVersions.CACHE.get(_tok0)==null?-1:0; + _state=STATE_SPACE1; + continue; + } + else if (ch < SPACE && ch>=0) + { + throw new HttpException(BAD_REQUEST_400); + } + break; + + case STATE_SPACE1: + if (ch > SPACE || ch<0) + { + _buffer.mark(); + if (_responseStatus>=0) + { + _state=STATE_STATUS; + _responseStatus=ch-'0'; + } + else + _state=STATE_URI; + } + else if (ch < SPACE) + { + throw new HttpException(BAD_REQUEST_400); + } + break; + + case STATE_STATUS: + if (ch == SPACE) + { + _tok1.update(_buffer.markIndex(), _buffer.getIndex() - 1); + _state=STATE_SPACE2; + continue; + } + else if (ch>='0' && ch<='9') + { + _responseStatus=_responseStatus*10+(ch-'0'); + continue; + } + else if (ch < SPACE && ch>=0) + { + _handler.startResponse(HttpMethods.CACHE.lookup(_tok0), _responseStatus, null); + _eol=ch; + _state=STATE_HEADER; + _tok0.setPutIndex(_tok0.getIndex()); + _tok1.setPutIndex(_tok1.getIndex()); + _multiLineValue=null; + continue; + } + // not a digit, so must be a URI + _state=STATE_URI; + _responseStatus=-1; + break; + + case STATE_URI: + if (ch == SPACE) + { + _tok1.update(_buffer.markIndex(), _buffer.getIndex() - 1); + _state=STATE_SPACE2; + continue; + } + else if (ch < SPACE && ch>=0) + { + // HTTP/0.9 + _handler.startRequest(HttpMethods.CACHE.lookup(_tok0), _buffer.sliceFromMark(), null); + _persistent=false; + _state=STATE_SEEKING_EOF; + _handler.headerComplete(); + _handler.messageComplete(_contentPosition); + return 1; + } + break; + + case STATE_SPACE2: + if (ch > SPACE || ch<0) + { + _buffer.mark(); + _state=STATE_FIELD2; + } + else if (ch < SPACE) + { + if (_responseStatus>0) + { + _handler.startResponse(HttpMethods.CACHE.lookup(_tok0), _responseStatus, null); + _eol=ch; + _state=STATE_HEADER; + _tok0.setPutIndex(_tok0.getIndex()); + _tok1.setPutIndex(_tok1.getIndex()); + _multiLineValue=null; + } + else + { + // HTTP/0.9 + _handler.startRequest(HttpMethods.CACHE.lookup(_tok0), _tok1, null); + _persistent=false; + _state=STATE_SEEKING_EOF; + _handler.headerComplete(); + _handler.messageComplete(_contentPosition); + return 1; + } + } + break; + + case STATE_FIELD2: + if (ch == CARRIAGE_RETURN || ch == LINE_FEED) + { + Buffer version; + if (_responseStatus>0) + _handler.startResponse(version=HttpVersions.CACHE.lookup(_tok0), _responseStatus,_buffer.sliceFromMark()); + else + _handler.startRequest(HttpMethods.CACHE.lookup(_tok0), _tok1, version=HttpVersions.CACHE.lookup(_buffer.sliceFromMark())); + _eol=ch; + _persistent=HttpVersions.CACHE.getOrdinal(version)>=HttpVersions.HTTP_1_1_ORDINAL; + _state=STATE_HEADER; + _tok0.setPutIndex(_tok0.getIndex()); + _tok1.setPutIndex(_tok1.getIndex()); + _multiLineValue=null; + continue; + } + break; + + case STATE_HEADER: + switch(ch) + { + case COLON: + case SPACE: + case TAB: + { + // header value without name - continuation? + _length=-1; + _state=STATE_HEADER_VALUE; + break; + } + + default: + { + // handler last header if any + if (_cached!=null || _tok0.length() > 0 || _tok1.length() > 0 || _multiLineValue != null) + { + Buffer header=_cached!=null?_cached:HttpHeaders.CACHE.lookup(_tok0); + _cached=null; + Buffer value=_multiLineValue == null ? _tok1 : new ByteArrayBuffer(_multiLineValue); + + int ho=HttpHeaders.CACHE.getOrdinal(header); + if (ho >= 0) + { + int vo; + + switch (ho) + { + case HttpHeaders.CONTENT_LENGTH_ORDINAL: + if (_contentLength != CHUNKED_CONTENT ) + { + try + { + _contentLength=BufferUtil.toLong(value); + } + catch(NumberFormatException e) + { + LOG.ignore(e); + throw new HttpException(BAD_REQUEST_400); + } + if (_contentLength <= 0) + _contentLength=NO_CONTENT; + } + break; + + case HttpHeaders.TRANSFER_ENCODING_ORDINAL: + value=HttpHeaderValues.CACHE.lookup(value); + vo=HttpHeaderValues.CACHE.getOrdinal(value); + if (HttpHeaderValues.CHUNKED_ORDINAL == vo) + _contentLength=CHUNKED_CONTENT; + else + { + String c=value.toString(__ISO_8859_1); + if (c.endsWith(HttpHeaderValues.CHUNKED)) + _contentLength=CHUNKED_CONTENT; + + else if (c.indexOf(HttpHeaderValues.CHUNKED) >= 0) + throw new HttpException(400,null); + } + break; + + case HttpHeaders.CONNECTION_ORDINAL: + switch(HttpHeaderValues.CACHE.getOrdinal(value)) + { + case HttpHeaderValues.CLOSE_ORDINAL: + _persistent=false; + break; + + case HttpHeaderValues.KEEP_ALIVE_ORDINAL: + _persistent=true; + break; + + case -1: // No match, may be multi valued + { + for (String v : value.toString().split(",")) + { + switch(HttpHeaderValues.CACHE.getOrdinal(v.trim())) + { + case HttpHeaderValues.CLOSE_ORDINAL: + _persistent=false; + break; + + case HttpHeaderValues.KEEP_ALIVE_ORDINAL: + _persistent=true; + break; + } + } + break; + } + } + } + } + + _handler.parsedHeader(header, value); + _tok0.setPutIndex(_tok0.getIndex()); + _tok1.setPutIndex(_tok1.getIndex()); + _multiLineValue=null; + } + _buffer.setMarkIndex(-1); + + // now handle ch + if (ch == CARRIAGE_RETURN || ch == LINE_FEED) + { + // is it a response that cannot have a body? + if (_responseStatus > 0 && // response + (_responseStatus == 304 || // not-modified response + _responseStatus == 204 || // no-content response + _responseStatus < 200)) // 1xx response + _contentLength=NO_CONTENT; // ignore any other headers set + // else if we don't know framing + else if (_contentLength == UNKNOWN_CONTENT) + { + if (_responseStatus == 0 // request + || _responseStatus == 304 // not-modified response + || _responseStatus == 204 // no-content response + || _responseStatus < 200) // 1xx response + _contentLength=NO_CONTENT; + else + _contentLength=EOF_CONTENT; + } + + _contentPosition=0; + _eol=ch; + if (_eol==CARRIAGE_RETURN && _buffer.hasContent() && _buffer.peek()==LINE_FEED) + _eol=_buffer.get(); + + // We convert _contentLength to an int for this switch statement because + // we don't care about the amount of data available just whether there is some. + switch (_contentLength > Integer.MAX_VALUE ? Integer.MAX_VALUE : (int) _contentLength) + { + case EOF_CONTENT: + _state=STATE_EOF_CONTENT; + _handler.headerComplete(); // May recurse here ! + break; + + case CHUNKED_CONTENT: + _state=STATE_CHUNKED_CONTENT; + _handler.headerComplete(); // May recurse here ! + break; + + case NO_CONTENT: + _handler.headerComplete(); + _state=_persistent||(_responseStatus>=100&&_responseStatus<200)?STATE_END:STATE_SEEKING_EOF; + _handler.messageComplete(_contentPosition); + return 1; + + default: + _state=STATE_CONTENT; + _handler.headerComplete(); // May recurse here ! + break; + } + return 1; + } + else + { + // New header + _length=1; + _buffer.mark(); + _state=STATE_HEADER_NAME; + + // try cached name! + if (array!=null) + { + _cached=HttpHeaders.CACHE.getBest(array, _buffer.markIndex(), length+1); + + if (_cached!=null) + { + _length=_cached.length(); + _buffer.setGetIndex(_buffer.markIndex()+_length); + length=_buffer.length(); + } + } + } + } + } + + break; + + case STATE_HEADER_NAME: + switch(ch) + { + case CARRIAGE_RETURN: + case LINE_FEED: + if (_length > 0) + _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length); + _eol=ch; + _state=STATE_HEADER; + break; + case COLON: + if (_length > 0 && _cached==null) + _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length); + _length=-1; + _state=STATE_HEADER_VALUE; + break; + case SPACE: + case TAB: + break; + default: + { + _cached=null; + if (_length == -1) + _buffer.mark(); + _length=_buffer.getIndex() - _buffer.markIndex(); + _state=STATE_HEADER_IN_NAME; + } + } + + break; + + case STATE_HEADER_IN_NAME: + switch(ch) + { + case CARRIAGE_RETURN: + case LINE_FEED: + if (_length > 0) + _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length); + _eol=ch; + _state=STATE_HEADER; + break; + case COLON: + if (_length > 0 && _cached==null) + _tok0.update(_buffer.markIndex(), _buffer.markIndex() + _length); + _length=-1; + _state=STATE_HEADER_VALUE; + break; + case SPACE: + case TAB: + _state=STATE_HEADER_NAME; + break; + default: + { + _cached=null; + _length++; + } + } + break; + + case STATE_HEADER_VALUE: + switch(ch) + { + case CARRIAGE_RETURN: + case LINE_FEED: + if (_length > 0) + { + if (_tok1.length() == 0) + _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length); + else + { + // Continuation line! + if (_multiLineValue == null) _multiLineValue=_tok1.toString(__ISO_8859_1); + _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length); + _multiLineValue += " " + _tok1.toString(__ISO_8859_1); + } + } + _eol=ch; + _state=STATE_HEADER; + break; + case SPACE: + case TAB: + break; + default: + { + if (_length == -1) + _buffer.mark(); + _length=_buffer.getIndex() - _buffer.markIndex(); + _state=STATE_HEADER_IN_VALUE; + } + } + break; + + case STATE_HEADER_IN_VALUE: + switch(ch) + { + case CARRIAGE_RETURN: + case LINE_FEED: + if (_length > 0) + { + if (_tok1.length() == 0) + _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length); + else + { + // Continuation line! + if (_multiLineValue == null) _multiLineValue=_tok1.toString(__ISO_8859_1); + _tok1.update(_buffer.markIndex(), _buffer.markIndex() + _length); + _multiLineValue += " " + _tok1.toString(__ISO_8859_1); + } + } + _eol=ch; + _state=STATE_HEADER; + break; + case SPACE: + case TAB: + _state=STATE_HEADER_VALUE; + break; + default: + _length++; + } + break; + } + } // end of HEADER states loop + + // ========================== + + // Handle HEAD response + if (_responseStatus>0 && _headResponse) + { + _state=_persistent||(_responseStatus>=100&&_responseStatus<200)?STATE_END:STATE_SEEKING_EOF; + _handler.messageComplete(_contentLength); + } + + + // ========================== + + // Handle _content + length=_buffer.length(); + Buffer chunk; + last=_state; + while (_state > STATE_END && length > 0) + { + if (last!=_state) + { + progress++; + last=_state; + } + + if (_eol == CARRIAGE_RETURN && _buffer.peek() == LINE_FEED) + { + _eol=_buffer.get(); + length=_buffer.length(); + continue; + } + _eol=0; + switch (_state) + { + case STATE_EOF_CONTENT: + chunk=_buffer.get(_buffer.length()); + _contentPosition += chunk.length(); + _contentView.update(chunk); + _handler.content(chunk); // May recurse here + // TODO adjust the _buffer to keep unconsumed content + return 1; + + case STATE_CONTENT: + { + long remaining=_contentLength - _contentPosition; + if (remaining == 0) + { + _state=_persistent?STATE_END:STATE_SEEKING_EOF; + _handler.messageComplete(_contentPosition); + return 1; + } + + if (length > remaining) + { + // We can cast reamining to an int as we know that it is smaller than + // or equal to length which is already an int. + length=(int)remaining; + } + + chunk=_buffer.get(length); + _contentPosition += chunk.length(); + _contentView.update(chunk); + _handler.content(chunk); // May recurse here + + if(_contentPosition == _contentLength) + { + _state=_persistent?STATE_END:STATE_SEEKING_EOF; + _handler.messageComplete(_contentPosition); + } + // TODO adjust the _buffer to keep unconsumed content + return 1; + } + + case STATE_CHUNKED_CONTENT: + { + ch=_buffer.peek(); + if (ch == CARRIAGE_RETURN || ch == LINE_FEED) + _eol=_buffer.get(); + else if (ch <= SPACE) + _buffer.get(); + else + { + _chunkLength=0; + _chunkPosition=0; + _state=STATE_CHUNK_SIZE; + } + break; + } + + case STATE_CHUNK_SIZE: + { + ch=_buffer.get(); + if (ch == CARRIAGE_RETURN || ch == LINE_FEED) + { + _eol=ch; + + if (_chunkLength == 0) + { + if (_eol==CARRIAGE_RETURN && _buffer.hasContent() && _buffer.peek()==LINE_FEED) + _eol=_buffer.get(); + _state=_persistent?STATE_END:STATE_SEEKING_EOF; + _handler.messageComplete(_contentPosition); + return 1; + } + else + _state=STATE_CHUNK; + } + else if (ch <= SPACE || ch == SEMI_COLON) + _state=STATE_CHUNK_PARAMS; + else if (ch >= '0' && ch <= '9') + _chunkLength=_chunkLength * 16 + (ch - '0'); + else if (ch >= 'a' && ch <= 'f') + _chunkLength=_chunkLength * 16 + (10 + ch - 'a'); + else if (ch >= 'A' && ch <= 'F') + _chunkLength=_chunkLength * 16 + (10 + ch - 'A'); + else + throw new IOException("bad chunk char: " + ch); + break; + } + + case STATE_CHUNK_PARAMS: + { + ch=_buffer.get(); + if (ch == CARRIAGE_RETURN || ch == LINE_FEED) + { + _eol=ch; + if (_chunkLength == 0) + { + if (_eol==CARRIAGE_RETURN && _buffer.hasContent() && _buffer.peek()==LINE_FEED) + _eol=_buffer.get(); + _state=_persistent?STATE_END:STATE_SEEKING_EOF; + _handler.messageComplete(_contentPosition); + return 1; + } + else + _state=STATE_CHUNK; + } + break; + } + + case STATE_CHUNK: + { + int remaining=_chunkLength - _chunkPosition; + if (remaining == 0) + { + _state=STATE_CHUNKED_CONTENT; + break; + } + else if (length > remaining) + length=remaining; + chunk=_buffer.get(length); + _contentPosition += chunk.length(); + _chunkPosition += chunk.length(); + _contentView.update(chunk); + _handler.content(chunk); // May recurse here + // TODO adjust the _buffer to keep unconsumed content + return 1; + } + + case STATE_SEEKING_EOF: + { + // Close if there is more data than CRLF + if (_buffer.length()>2) + { + _state=STATE_END; + _endp.close(); + } + else + { + // or if the data is not white space + while (_buffer.length()>0) + if (!Character.isWhitespace(_buffer.get())) + { + _state=STATE_END; + _endp.close(); + _buffer.clear(); + } + } + + _buffer.clear(); + break; + } + } + + length=_buffer.length(); + } + + return progress; + } + catch(HttpException e) + { + _persistent=false; + _state=STATE_SEEKING_EOF; + throw e; + } + } + + /* ------------------------------------------------------------------------------- */ + /** fill the buffers from the endpoint + * + */ + protected int fill() throws IOException + { + // Do we have a buffer? + if (_buffer==null) + _buffer=getHeaderBuffer(); + + // Is there unconsumed content in body buffer + if (_state>STATE_END && _buffer==_header && _header!=null && !_header.hasContent() && _body!=null && _body.hasContent()) + { + _buffer=_body; + return _buffer.length(); + } + + // Shall we switch to a body buffer? + if (_buffer==_header && _state>STATE_END && _header.length()==0 && (_forceContentBuffer || (_contentLength-_contentPosition)>_header.capacity()) && (_body!=null||_buffers!=null)) + { + if (_body==null) + _body=_buffers.getBuffer(); + _buffer=_body; + } + + // Do we have somewhere to fill from? + if (_endp != null ) + { + // Shall we compact the body? + if (_buffer==_body || _state>STATE_END) + { + _buffer.compact(); + } + + // Are we full? + if (_buffer.space() == 0) + { + LOG.warn("HttpParser Full for {} ",_endp); + _buffer.clear(); + throw new HttpException(REQUEST_ENTITY_TOO_LARGE_413, "Request Entity Too Large: "+(_buffer==_body?"body":"head")); + } + + try + { + int filled = _endp.fill(_buffer); + return filled; + } + catch(IOException e) + { + LOG.debug(e); + throw (e instanceof EofException) ? e:new EofException(e); + } + } + + return -1; + } + + /* ------------------------------------------------------------------------------- */ + @Override + public String toString() + { + return String.format(Locale.getDefault(), "%s{s=%d,l=%d,c=%d}", + getClass().getSimpleName(), + _state, + _length, + _contentLength); + } + + /* ------------------------------------------------------------ */ + public Buffer getHeaderBuffer() + { + if (_header == null) + { + _header=_buffers.getHeader(); + _tok0.update(_header); + _tok1.update(_header); + } + return _header; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public static abstract class EventHandler + { + public abstract void content(Buffer ref) throws IOException; + + public void headerComplete() throws IOException + { + } + + public void messageComplete(long contentLength) throws IOException + { + } + + /** + * This is the method called by parser when a HTTP Header name and value is found + */ + public void parsedHeader(Buffer name, Buffer value) throws IOException + { + } + + /** + * This is the method called by parser when the HTTP request line is parsed + */ + public abstract void startRequest(Buffer method, Buffer url, Buffer version) + throws IOException; + + /** + * This is the method called by parser when the HTTP request line is parsed + */ + public abstract void startResponse(Buffer version, int status, Buffer reason) + throws IOException; + + public void earlyEOF() + {} + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpVersions.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpVersions.java new file mode 100644 index 00000000..95d21ceb --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/HttpVersions.java @@ -0,0 +1,47 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.BufferCache; + +/* ------------------------------------------------------------------------------- */ +/** + * + * + */ +public class HttpVersions +{ + public final static String + HTTP_0_9 = "", + HTTP_1_0 = "HTTP/1.0", + HTTP_1_1 = "HTTP/1.1"; + + public final static int + HTTP_0_9_ORDINAL=9, + HTTP_1_0_ORDINAL=10, + HTTP_1_1_ORDINAL=11; + + public final static BufferCache CACHE = new BufferCache(); + + public final static Buffer + HTTP_0_9_BUFFER=CACHE.add(HTTP_0_9,HTTP_0_9_ORDINAL), + HTTP_1_0_BUFFER=CACHE.add(HTTP_1_0,HTTP_1_0_ORDINAL), + HTTP_1_1_BUFFER=CACHE.add(HTTP_1_1,HTTP_1_1_ORDINAL); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/Parser.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/Parser.java new file mode 100644 index 00000000..68b433b9 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/http/Parser.java @@ -0,0 +1,37 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.http; + +import java.io.IOException; + +/** + * Abstract interface for a connection Parser for use by Jetty. + */ +public interface Parser +{ + boolean isComplete(); + + /** + * @return True if progress made + * @throws IOException + */ + boolean parseAvailable() throws IOException; + + boolean isIdle(); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractBuffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractBuffer.java new file mode 100644 index 00000000..400c088e --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractBuffer.java @@ -0,0 +1,557 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.nio.charset.Charset; + +import org.eclipse.jetty.util.TypeUtil; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * + * + */ +public abstract class AbstractBuffer implements Buffer +{ + private static final Logger LOG = Log.getLogger(AbstractBuffer.class); + + protected final static String + __IMMUTABLE = "IMMUTABLE", + __READONLY = "READONLY", + __READWRITE = "READWRITE", + __VOLATILE = "VOLATILE"; + + protected int _access; + protected boolean _volatile; + + protected int _get; + protected int _put; + protected int _hash; + protected int _hashGet; + protected int _hashPut; + protected int _mark; + protected String _string; + protected View _view; + + /** + * Constructor for BufferView + * + * @param access 0==IMMUTABLE, 1==READONLY, 2==READWRITE + */ + public AbstractBuffer(int access, boolean isVolatile) + { + if (access == IMMUTABLE && isVolatile) + throw new IllegalArgumentException("IMMUTABLE && VOLATILE"); + setMarkIndex(-1); + _access = access; + _volatile = isVolatile; + } + + /* + * @see org.eclipse.io.Buffer#toArray() + */ + public byte[] asArray() + { + byte[] bytes = new byte[length()]; + byte[] array = array(); + if (array != null) + System.arraycopy(array, getIndex(), bytes, 0, bytes.length); + else + peek(getIndex(), bytes, 0, length()); + return bytes; + } + + public ByteArrayBuffer duplicate(int access) + { + Buffer b=this.buffer(); + if (this instanceof Buffer.CaseInsensitve || b instanceof Buffer.CaseInsensitve) + return new ByteArrayBuffer.CaseInsensitive(asArray(), 0, length(),access); + else + return new ByteArrayBuffer(asArray(), 0, length(), access); + } + + public Buffer asMutableBuffer() + { + if (!isImmutable()) return this; + + Buffer b=this.buffer(); + if (b.isReadOnly()) + { + return duplicate(READWRITE); + } + return new View(b, markIndex(), getIndex(), putIndex(), _access); + } + + public Buffer buffer() + { + return this; + } + + public void clear() + { + setMarkIndex(-1); + setGetIndex(0); + setPutIndex(0); + } + + public void compact() + { + if (isReadOnly()) throw new IllegalStateException(__READONLY); + int s = markIndex() >= 0 ? markIndex() : getIndex(); + if (s > 0) + { + byte array[] = array(); + int length = putIndex() - s; + if (length > 0) + { + if (array != null) + System.arraycopy(array(), s, array(), 0, length); + else + poke(0, peek(s, length)); + } + if (markIndex() > 0) setMarkIndex(markIndex() - s); + setGetIndex(getIndex() - s); + setPutIndex(putIndex() - s); + } + } + + @Override + public boolean equals(Object obj) + { + if (obj==this) + return true; + + // reject non buffers; + if (obj == null || !(obj instanceof Buffer)) return false; + Buffer b = (Buffer) obj; + + if (this instanceof Buffer.CaseInsensitve || b instanceof Buffer.CaseInsensitve) + return equalsIgnoreCase(b); + + // reject different lengths + if (b.length() != length()) return false; + + // reject AbstractBuffer with different hash value + if (_hash != 0 && obj instanceof AbstractBuffer) + { + AbstractBuffer ab = (AbstractBuffer) obj; + if (ab._hash != 0 && _hash != ab._hash) return false; + } + + // Nothing for it but to do the hard grind. + int get=getIndex(); + int bi=b.putIndex(); + for (int i = putIndex(); i-->get;) + { + byte b1 = peek(i); + byte b2 = b.peek(--bi); + if (b1 != b2) return false; + } + return true; + } + + public boolean equalsIgnoreCase(Buffer b) + { + if (b==this) + return true; + + // reject different lengths + if (b.length() != length()) return false; + + // reject AbstractBuffer with different hash value + if (_hash != 0 && b instanceof AbstractBuffer) + { + AbstractBuffer ab = (AbstractBuffer) b; + if (ab._hash != 0 && _hash != ab._hash) return false; + } + + // Nothing for it but to do the hard grind. + int get=getIndex(); + int bi=b.putIndex(); + + byte[] array = array(); + byte[] barray= b.array(); + if (array!=null && barray!=null) + { + for (int i = putIndex(); i-->get;) + { + byte b1 = array[i]; + byte b2 = barray[--bi]; + if (b1 != b2) + { + if ('a' <= b1 && b1 <= 'z') b1 = (byte) (b1 - 'a' + 'A'); + if ('a' <= b2 && b2 <= 'z') b2 = (byte) (b2 - 'a' + 'A'); + if (b1 != b2) return false; + } + } + } + else + { + for (int i = putIndex(); i-->get;) + { + byte b1 = peek(i); + byte b2 = b.peek(--bi); + if (b1 != b2) + { + if ('a' <= b1 && b1 <= 'z') b1 = (byte) (b1 - 'a' + 'A'); + if ('a' <= b2 && b2 <= 'z') b2 = (byte) (b2 - 'a' + 'A'); + if (b1 != b2) return false; + } + } + } + return true; + } + + public byte get() + { + return peek(_get++); + } + + public int get(byte[] b, int offset, int length) + { + int gi = getIndex(); + int l=length(); + if (l==0) + return -1; + + if (length>l) + length=l; + + length = peek(gi, b, offset, length); + if (length>0) + setGetIndex(gi + length); + return length; + } + + public Buffer get(int length) + { + int gi = getIndex(); + Buffer view = peek(gi, length); + setGetIndex(gi + length); + return view; + } + + public final int getIndex() + { + return _get; + } + + public boolean hasContent() + { + return _put > _get; + } + + @Override + public int hashCode() + { + if (_hash == 0 || _hashGet!=_get || _hashPut!=_put) + { + int get=getIndex(); + byte[] array = array(); + if (array==null) + { + for (int i = putIndex(); i-- >get;) + { + byte b = peek(i); + if ('a' <= b && b <= 'z') + b = (byte) (b - 'a' + 'A'); + _hash = 31 * _hash + b; + } + } + else + { + for (int i = putIndex(); i-- >get;) + { + byte b = array[i]; + if ('a' <= b && b <= 'z') + b = (byte) (b - 'a' + 'A'); + _hash = 31 * _hash + b; + } + } + if (_hash == 0) + _hash = -1; + _hashGet=_get; + _hashPut=_put; + + } + return _hash; + } + + public boolean isImmutable() + { + return _access <= IMMUTABLE; + } + + public boolean isReadOnly() + { + return _access <= READONLY; + } + + public boolean isVolatile() + { + return _volatile; + } + + public int length() + { + return _put - _get; + } + + public void mark() + { + setMarkIndex(_get - 1); + } + + public int markIndex() + { + return _mark; + } + + public byte peek() + { + return peek(_get); + } + + public Buffer peek(int index, int length) + { + if (_view == null) + { + _view = new View(this, -1, index, index + length, isReadOnly() ? READONLY : READWRITE); + } + else + { + _view.update(this.buffer()); + _view.setMarkIndex(-1); + _view.setGetIndex(0); + _view.setPutIndex(index + length); + _view.setGetIndex(index); + + } + return _view; + } + + public int poke(int index, Buffer src) + { + _hash=0; + + int length=src.length(); + if (index + length > capacity()) + { + length=capacity()-index; + } + + byte[] src_array = src.array(); + byte[] dst_array = array(); + if (src_array != null && dst_array != null) + System.arraycopy(src_array, src.getIndex(), dst_array, index, length); + else if (src_array != null) + { + int s=src.getIndex(); + for (int i=0;i<length;i++) + poke(index++,src_array[s++]); + } + else if (dst_array != null) + { + int s=src.getIndex(); + for (int i=0;i<length;i++) + dst_array[index++]=src.peek(s++); + } + else + { + int s=src.getIndex(); + for (int i=0;i<length;i++) + poke(index++,src.peek(s++)); + } + + return length; + } + + + public int poke(int index, byte[] b, int offset, int length) + { + _hash=0; + if (index + length > capacity()) + { + length=capacity()-index; + } + + byte[] dst_array = array(); + if (dst_array != null) + System.arraycopy(b, offset, dst_array, index, length); + else + { + int s=offset; + for (int i=0;i<length;i++) + poke(index++,b[s++]); + } + return length; + } + + public int put(Buffer src) + { + int pi = putIndex(); + int l=poke(pi, src); + setPutIndex(pi + l); + return l; + } + + public void put(byte b) + { + int pi = putIndex(); + poke(pi, b); + setPutIndex(pi + 1); + } + + public int put(byte[] b, int offset, int length) + { + int pi = putIndex(); + int l = poke(pi, b, offset, length); + setPutIndex(pi + l); + return l; + } + + public int put(byte[] b) + { + int pi = putIndex(); + int l = poke(pi, b, 0, b.length); + setPutIndex(pi + l); + return l; + } + + public final int putIndex() + { + return _put; + } + + public void setGetIndex(int getIndex) + { + _get = getIndex; + _hash=0; + } + + public void setMarkIndex(int index) + { + _mark = index; + } + + public void setPutIndex(int putIndex) + { + _put = putIndex; + _hash=0; + } + + public int skip(int n) + { + if (length() < n) n = length(); + setGetIndex(getIndex() + n); + return n; + } + + public Buffer sliceFromMark() + { + return sliceFromMark(getIndex() - markIndex() - 1); + } + + public Buffer sliceFromMark(int length) + { + if (markIndex() < 0) return null; + Buffer view = peek(markIndex(), length); + setMarkIndex(-1); + return view; + } + + public int space() + { + return capacity() - _put; + } + + public String toDetailString() + { + StringBuilder buf = new StringBuilder(); + buf.append("["); + buf.append(super.hashCode()); + buf.append(","); + buf.append(this.buffer().hashCode()); + buf.append(",m="); + buf.append(markIndex()); + buf.append(",g="); + buf.append(getIndex()); + buf.append(",p="); + buf.append(putIndex()); + buf.append(",c="); + buf.append(capacity()); + buf.append("]={"); + if (markIndex() >= 0) + { + for (int i = markIndex(); i < getIndex(); i++) + { + byte b = peek(i); + TypeUtil.toHex(b,buf); + } + buf.append("}{"); + } + int count = 0; + for (int i = getIndex(); i < putIndex(); i++) + { + byte b = peek(i); + TypeUtil.toHex(b,buf); + if (count++ == 50) + { + if (putIndex() - i > 20) + { + buf.append(" ... "); + i = putIndex() - 20; + } + } + } + buf.append('}'); + return buf.toString(); + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + if (isImmutable()) + { + if (_string == null) + _string = new String(asArray(), 0, length()); + return _string; + } + return new String(asArray(), 0, length()); + } + + /* ------------------------------------------------------------ */ + public String toString(Charset charset) + { + try + { + byte[] bytes=array(); + if (bytes!=null) + return new String(bytes,getIndex(),length(),charset); + return new String(asArray(), 0, length(),charset); + } + catch(Exception e) + { + LOG.warn(e); + return new String(asArray(), 0, length()); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractBuffers.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractBuffers.java new file mode 100644 index 00000000..ebbd13d4 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractBuffers.java @@ -0,0 +1,148 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import org.eclipse.jetty.io.nio.DirectNIOBuffer; +import org.eclipse.jetty.io.nio.IndirectNIOBuffer; + +public abstract class AbstractBuffers implements Buffers +{ + protected final Buffers.Type _headerType; + protected final int _headerSize; + protected final Buffers.Type _bufferType; + protected final int _bufferSize; + protected final Buffers.Type _otherType; + + /* ------------------------------------------------------------ */ + public AbstractBuffers(Buffers.Type headerType, int headerSize, Buffers.Type bufferType, int bufferSize, Buffers.Type otherType) + { + _headerType=headerType; + _headerSize=headerSize; + _bufferType=bufferType; + _bufferSize=bufferSize; + _otherType=otherType; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the buffer size in bytes. + */ + public int getBufferSize() + { + return _bufferSize; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the header size in bytes. + */ + public int getHeaderSize() + { + return _headerSize; + } + + + /* ------------------------------------------------------------ */ + /** + * Create a new header Buffer + * @return new Buffer + */ + final protected Buffer newHeader() + { + switch(_headerType) + { + case BYTE_ARRAY: + return new ByteArrayBuffer(_headerSize); + case DIRECT: + return new DirectNIOBuffer(_headerSize); + case INDIRECT: + return new IndirectNIOBuffer(_headerSize); + } + throw new IllegalStateException(); + } + + /* ------------------------------------------------------------ */ + /** + * Create a new content Buffer + * @return new Buffer + */ + final protected Buffer newBuffer() + { + switch(_bufferType) + { + case BYTE_ARRAY: + return new ByteArrayBuffer(_bufferSize); + case DIRECT: + return new DirectNIOBuffer(_bufferSize); + case INDIRECT: + return new IndirectNIOBuffer(_bufferSize); + } + throw new IllegalStateException(); + } + + /* ------------------------------------------------------------ */ + /** + * @param buffer + * @return True if the buffer is the correct type to be a Header buffer + */ + public final boolean isHeader(Buffer buffer) + { + if (buffer.capacity()==_headerSize) + { + switch(_headerType) + { + case BYTE_ARRAY: + return buffer instanceof ByteArrayBuffer && !(buffer instanceof IndirectNIOBuffer); + case DIRECT: + return buffer instanceof DirectNIOBuffer; + case INDIRECT: + return buffer instanceof IndirectNIOBuffer; + } + } + return false; + } + + /* ------------------------------------------------------------ */ + /** + * @param buffer + * @return True if the buffer is the correct type to be a Header buffer + */ + public final boolean isBuffer(Buffer buffer) + { + if (buffer.capacity()==_bufferSize) + { + switch(_bufferType) + { + case BYTE_ARRAY: + return buffer instanceof ByteArrayBuffer && !(buffer instanceof IndirectNIOBuffer); + case DIRECT: + return buffer instanceof DirectNIOBuffer; + case INDIRECT: + return buffer instanceof IndirectNIOBuffer; + } + } + return false; + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return String.format("%s [%d,%d]", getClass().getSimpleName(), _headerSize, _bufferSize); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractConnection.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractConnection.java new file mode 100644 index 00000000..2acbffb7 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AbstractConnection.java @@ -0,0 +1,67 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.io.IOException; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + + +public abstract class AbstractConnection implements Connection +{ + private static final Logger LOG = Log.getLogger(AbstractConnection.class); + + protected final EndPoint _endp; + + public AbstractConnection(EndPoint endp,long timestamp) + { + _endp=(EndPoint)endp; + } + + public void onIdleExpired(long idleForMs) + { + try + { + LOG.debug("onIdleExpired {}ms {} {}",idleForMs,this,_endp); + if (_endp.isInputShutdown() || _endp.isOutputShutdown()) + _endp.close(); + else + _endp.shutdownOutput(); + } + catch(IOException e) + { + LOG.ignore(e); + + try + { + _endp.close(); + } + catch(IOException e2) + { + LOG.ignore(e2); + } + } + } + + public String toString() + { + return String.format("%s@%x", getClass().getSimpleName(), hashCode()); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AsyncEndPoint.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AsyncEndPoint.java new file mode 100644 index 00000000..904d9cbe --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/AsyncEndPoint.java @@ -0,0 +1,42 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +public interface AsyncEndPoint extends ConnectedEndPoint +{ + /* ------------------------------------------------------------ */ + /** + * Dispatch the endpoint if it is not already dispatched + * + */ + public void dispatch(); + + /* ------------------------------------------------------------ */ + /** Schedule a write dispatch. + * Set the endpoint to not be writable and schedule a dispatch when + * it becomes writable. + */ + public void scheduleWrite(); + + /* ------------------------------------------------------------ */ + /** + * @return True if IO has been successfully performed since the last call to {@link #hasProgressed()} + */ + public boolean hasProgressed(); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Buffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Buffer.java new file mode 100644 index 00000000..d5aa765f --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Buffer.java @@ -0,0 +1,313 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.nio.charset.Charset; + + +/** + * Byte Buffer interface. + * + * This is a byte buffer that is designed to work like a FIFO for bytes. Puts and Gets operate on different + * pointers into the buffer and the valid _content of the buffer is always between the getIndex and the putIndex. + * + * This buffer interface is designed to be similar, but not dependent on the java.nio buffers, which may + * be used to back an implementation of this Buffer. The main difference is that NIO buffer after a put have + * their valid _content before the position and a flip is required to access that data. + * + * For this buffer it is always true that: + * markValue <= getIndex <= putIndex <= capacity + * + * + * @version 1.0 + */ +public interface Buffer extends Cloneable +{ + public final static int + IMMUTABLE=0, // neither indexes or contexts can be changed + READONLY=1, // indexes may be changed, but not content + READWRITE=2; // anything can be changed + public final boolean VOLATILE=true; // The buffer may change outside of current scope. + public final boolean NON_VOLATILE=false; + + /** + * Get the underlying array, if one exists. + * @return a <code>byte[]</code> backing this buffer or null if none exists. + */ + byte[] array(); + + /** + * + * @return a <code>byte[]</code> value of the bytes from the getIndex to the putIndex. + */ + byte[] asArray(); + + /** + * Get the underlying buffer. If this buffer wraps a backing buffer. + * @return The root backing buffer or this if there is no backing buffer; + */ + Buffer buffer(); + + /** + * + * @return an immutable version of this <code>Buffer</code>. + */ + Buffer asMutableBuffer(); + + /** + * + * The capacity of the buffer. This is the maximum putIndex that may be set. + * @return an <code>int</code> value + */ + int capacity(); + + /** + * the space remaining in the buffer. + * @return capacity - putIndex + */ + int space(); + + /** + * Clear the buffer. getIndex=0, putIndex=0. + */ + void clear(); + + /** + * Compact the buffer by discarding bytes before the postion (or mark if set). + * Bytes from the getIndex (or mark) to the putIndex are moved to the beginning of + * the buffer and the values adjusted accordingly. + */ + void compact(); + + /** + * Get the byte at the current getIndex and increment it. + * @return The <code>byte</code> value from the current getIndex. + */ + byte get(); + + /** + * Get bytes from the current postion and put them into the passed byte array. + * The getIndex is incremented by the number of bytes copied into the array. + * @param b The byte array to fill. + * @param offset Offset in the array. + * @param length The max number of bytes to read. + * @return The number of bytes actually read. + */ + int get(byte[] b, int offset, int length); + + /** + * + * @param length an <code>int</code> value + * @return a <code>Buffer</code> value + */ + Buffer get(int length); + + /** + * The index within the buffer that will next be read or written. + * @return an <code>int</code> value >=0 <= putIndex() + */ + int getIndex(); + + /** + * @return true of putIndex > getIndex + */ + boolean hasContent(); + + /** + * + * @return a <code>boolean</code> value true if case sensitive comparison on this buffer + */ + boolean equalsIgnoreCase(Buffer buffer); + + /** + * + * @return a <code>boolean</code> value true if the buffer is immutable and that neither + * the buffer contents nor the indexes may be changed. + */ + boolean isImmutable(); + + /** + * + * @return a <code>boolean</code> value true if the buffer is readonly. The buffer indexes may + * be modified, but the buffer contents may not. For example a View onto an immutable Buffer will be + * read only. + */ + boolean isReadOnly(); + + /** + * + * @return a <code>boolean</code> value true if the buffer contents may change + * via alternate paths than this buffer. If the contents of this buffer are to be used outside of the + * current context, then a copy must be made. + */ + boolean isVolatile(); + + /** + * The number of bytes from the getIndex to the putIndex + * @return an <code>int</code> == putIndex()-getIndex() + */ + int length(); + + /** + * Set the mark to the current getIndex. + */ + void mark(); + + /** + * The current index of the mark. + * @return an <code>int</code> index in the buffer or -1 if the mark is not set. + */ + int markIndex(); + + /** + * Get the byte at the current getIndex without incrementing the getIndex. + * @return The <code>byte</code> value from the current getIndex. + */ + byte peek(); + + /** + * Get the byte at a specific index in the buffer. + * @param index an <code>int</code> value + * @return a <code>byte</code> value + */ + byte peek(int index); + + /** + * + * @param index an <code>int</code> value + * @param b The byte array to peek into + * @param offset The offset into the array to start peeking + * @param length an <code>int</code> value + * @return The number of bytes actually peeked + */ + int peek(int index, byte[] b, int offset, int length); + + /** + * Put the contents of the buffer at the specific index. + * @param index an <code>int</code> value + * @param src a <code>Buffer</code>. If the source buffer is not modified + + * @return The number of bytes actually poked + */ + int poke(int index, Buffer src); + + /** + * Put a specific byte to a specific getIndex. + * @param index an <code>int</code> value + * @param b a <code>byte</code> value + */ + void poke(int index, byte b); + + /** + * Put a specific byte to a specific getIndex. + * @param index an <code>int</code> value + * @param b a <code>byte array</code> value + * @return The number of bytes actually poked + */ + int poke(int index, byte b[], int offset, int length); + + /** + * Write the bytes from the source buffer to the current getIndex. + * @param src The source <code>Buffer</code> it is not modified. + * @return The number of bytes actually poked + */ + int put(Buffer src); + + /** + * Put a byte to the current getIndex and increment the getIndex. + * @param b a <code>byte</code> value + */ + void put(byte b); + + /** + * Put a byte to the current getIndex and increment the getIndex. + * @param b a <code>byte</code> value + * @return The number of bytes actually poked + */ + int put(byte[] b,int offset, int length); + + /** + * Put a byte to the current getIndex and increment the getIndex. + * @param b a <code>byte</code> value + * @return The number of bytes actually poked + */ + int put(byte[] b); + + /** + * The index of the first element that should not be read. + * @return an <code>int</code> value >= getIndex() + */ + int putIndex(); + + /** + * Set the buffers start getIndex. + * @param newStart an <code>int</code> value + */ + void setGetIndex(int newStart); + + /** + * Set a specific value for the mark. + * @param newMark an <code>int</code> value + */ + void setMarkIndex(int newMark); + + /** + * + * @param newLimit an <code>int</code> value + */ + void setPutIndex(int newLimit); + + /** + * Skip _content. The getIndex is updated by min(remaining(), n) + * @param n The number of bytes to skip + * @return the number of bytes skipped. + */ + int skip(int n); + + /** + * + * + * @return a volitile <code>Buffer</code> value from the mark to the putIndex + */ + Buffer sliceFromMark(); + + /** + * + * + * @param length an <code>int</code> value + * @return a valitile <code>Buffer</code> value from the mark of the length requested. + */ + Buffer sliceFromMark(int length); + + /** + * + * @return a <code>String</code> value describing the state and contents of the buffer. + */ + String toDetailString(); + + /* ------------------------------------------------------------ */ + String toString(Charset charset); + + /* + * Buffers implementing this interface should be compared with case insensitive equals + * + */ + public interface CaseInsensitve + {} +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/BufferCache.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/BufferCache.java new file mode 100644 index 00000000..03e81a8d --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/BufferCache.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map.Entry; + +import org.eclipse.jetty.util.StringMap; + +/* ------------------------------------------------------------------------------- */ +/** + * Stores a collection of {@link Buffer} objects. + * Buffers are stored in an ordered collection and can retreived by index or value + * + */ +public class BufferCache +{ + private final HashMap<CachedBuffer, CachedBuffer> _bufferMap=new HashMap<CachedBuffer, CachedBuffer>(); + private final StringMap _stringMap=new StringMap(StringMap.CASE_INSENSTIVE); + private final ArrayList<CachedBuffer> _index= new ArrayList<CachedBuffer>(); + + /* ------------------------------------------------------------------------------- */ + /** Add a buffer to the cache at the specified index. + * @param value The content of the buffer. + */ + public CachedBuffer add(String value, int ordinal) + { + CachedBuffer buffer= new CachedBuffer(value, ordinal); + _bufferMap.put(buffer, buffer); + _stringMap.put(value, buffer); + while ((ordinal - _index.size()) >= 0) + _index.add(null); + if (_index.get(ordinal)==null) + _index.add(ordinal, buffer); + return buffer; + } + + public CachedBuffer get(Buffer buffer) + { + return (CachedBuffer)_bufferMap.get(buffer); + } + + public CachedBuffer get(String value) + { + return (CachedBuffer)_stringMap.get(value); + } + + public Buffer lookup(Buffer buffer) + { + if (buffer instanceof CachedBuffer) + return buffer; + + Buffer b= get(buffer); + if (b == null) + { + if (buffer instanceof Buffer.CaseInsensitve) + return buffer; + return new ByteArrayBuffer.CaseInsensitive(buffer.asArray(),0,buffer.length(),Buffer.IMMUTABLE); + } + + return b; + } + + public CachedBuffer getBest(byte[] value, int offset, int maxLength) + { + Entry<?, ?> entry = _stringMap.getBestEntry(value, offset, maxLength); + if (entry!=null) + return (CachedBuffer)entry.getValue(); + return null; + } + + public int getOrdinal(String value) + { + CachedBuffer buffer = (CachedBuffer)_stringMap.get(value); + return buffer==null?-1:buffer.getOrdinal(); + } + + public int getOrdinal(Buffer buffer) + { + if (buffer instanceof CachedBuffer) + return ((CachedBuffer)buffer).getOrdinal(); + buffer=lookup(buffer); + if (buffer!=null && buffer instanceof CachedBuffer) + return ((CachedBuffer)buffer).getOrdinal(); + return -1; + } + + public static class CachedBuffer extends ByteArrayBuffer.CaseInsensitive + { + private final int _ordinal; + + public CachedBuffer(String value, int ordinal) + { + super(value); + _ordinal= ordinal; + } + + public int getOrdinal() + { + return _ordinal; + } + } + + @Override + public String toString() + { + return "CACHE["+ + "bufferMap="+_bufferMap+ + ",stringMap="+_stringMap+ + ",index="+_index+ + "]"; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/BufferUtil.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/BufferUtil.java new file mode 100644 index 00000000..e213c225 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/BufferUtil.java @@ -0,0 +1,68 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +/* ------------------------------------------------------------------------------- */ +/** Buffer utility methods. + * + * + */ +public class BufferUtil +{ + static final byte SPACE= 0x20; + static final byte MINUS= '-'; + + /** + * Convert buffer to an long. + * Parses up to the first non-numeric character. If no number is found an + * IllegalArgumentException is thrown + * @param buffer A buffer containing an integer. The position is not changed. + * @return an int + */ + public static long toLong(Buffer buffer) + { + long val= 0; + boolean started= false; + boolean minus= false; + for (int i= buffer.getIndex(); i < buffer.putIndex(); i++) + { + byte b= buffer.peek(i); + if (b <= SPACE) + { + if (started) + break; + } + else if (b >= '0' && b <= '9') + { + val= val * 10L + (b - '0'); + started= true; + } + else if (b == MINUS && !started) + { + minus= true; + } + else + break; + } + + if (started) + return minus ? (-val) : val; + throw new NumberFormatException(buffer.toString()); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Buffers.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Buffers.java new file mode 100644 index 00000000..85170540 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Buffers.java @@ -0,0 +1,37 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + + +/* ------------------------------------------------------------ */ +/** BufferSource. + * Represents a pool or other source of buffers and abstracts the creation + * of specific types of buffers (eg NIO). The concept of big and little buffers + * is supported, but these terms have no absolute meaning and must be determined by context. + * + */ +public interface Buffers +{ + enum Type { BYTE_ARRAY, DIRECT, INDIRECT } ; + + Buffer getHeader(); + Buffer getBuffer(); + + void returnBuffer(Buffer buffer); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ByteArrayBuffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ByteArrayBuffer.java new file mode 100644 index 00000000..da4bf4f5 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ByteArrayBuffer.java @@ -0,0 +1,325 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.nio.charset.Charset; + +/* ------------------------------------------------------------------------------- */ +/** + * + */ +public class ByteArrayBuffer extends AbstractBuffer +{ + static final Charset __ISO_8859_1 = Charset.forName("ISO-8859-1"); + final protected byte[] _bytes; + + protected ByteArrayBuffer(int size, int access, boolean isVolatile) + { + this(new byte[size],0,0,access, isVolatile); + } + + public ByteArrayBuffer(byte[] bytes, int index, int length, int access) + { + super(READWRITE, NON_VOLATILE); + _bytes = bytes; + setPutIndex(index + length); + setGetIndex(index); + _access = access; + } + + public ByteArrayBuffer(byte[] bytes, int index, int length, int access, boolean isVolatile) + { + super(READWRITE, isVolatile); + _bytes = bytes; + setPutIndex(index + length); + setGetIndex(index); + _access = access; + } + + public ByteArrayBuffer(int size) + { + this(new byte[size], 0, 0, READWRITE); + setPutIndex(0); + } + + public ByteArrayBuffer(String value) + { + super(READWRITE,NON_VOLATILE); + _bytes = value.getBytes(__ISO_8859_1); + setGetIndex(0); + setPutIndex(_bytes.length); + _access=IMMUTABLE; + _string = value; + } + + public ByteArrayBuffer(String value,boolean immutable) + { + super(READWRITE,NON_VOLATILE); + _bytes = value.getBytes(__ISO_8859_1); + setGetIndex(0); + setPutIndex(_bytes.length); + if (immutable) + { + _access=IMMUTABLE; + _string = value; + } + } + + public byte[] array() + { + return _bytes; + } + + public int capacity() + { + return _bytes.length; + } + + @Override + public void compact() + { + if (isReadOnly()) + throw new IllegalStateException(__READONLY); + int s = markIndex() >= 0 ? markIndex() : getIndex(); + if (s > 0) + { + int length = putIndex() - s; + if (length > 0) + { + System.arraycopy(_bytes, s,_bytes, 0, length); + } + if (markIndex() > 0) setMarkIndex(markIndex() - s); + setGetIndex(getIndex() - s); + setPutIndex(putIndex() - s); + } + } + + @Override + public boolean equals(Object obj) + { + if (obj==this) + return true; + + if (obj == null || !(obj instanceof Buffer)) + return false; + + if (obj instanceof Buffer.CaseInsensitve) + return equalsIgnoreCase((Buffer)obj); + + + Buffer b = (Buffer) obj; + + // reject different lengths + if (b.length() != length()) + return false; + + // reject AbstractBuffer with different hash value + if (_hash != 0 && obj instanceof AbstractBuffer) + { + AbstractBuffer ab = (AbstractBuffer) obj; + if (ab._hash != 0 && _hash != ab._hash) + return false; + } + + // Nothing for it but to do the hard grind. + int get=getIndex(); + int bi=b.putIndex(); + for (int i = putIndex(); i-->get;) + { + byte b1 = _bytes[i]; + byte b2 = b.peek(--bi); + if (b1 != b2) return false; + } + return true; + } + + @Override + public boolean equalsIgnoreCase(Buffer b) + { + if (b==this) + return true; + + // reject different lengths + if (b==null || b.length() != length()) + return false; + + // reject AbstractBuffer with different hash value + if (_hash != 0 && b instanceof AbstractBuffer) + { + AbstractBuffer ab = (AbstractBuffer) b; + if (ab._hash != 0 && _hash != ab._hash) return false; + } + + // Nothing for it but to do the hard grind. + int get=getIndex(); + int bi=b.putIndex(); + byte[] barray=b.array(); + if (barray==null) + { + for (int i = putIndex(); i-->get;) + { + byte b1 = _bytes[i]; + byte b2 = b.peek(--bi); + if (b1 != b2) + { + if ('a' <= b1 && b1 <= 'z') b1 = (byte) (b1 - 'a' + 'A'); + if ('a' <= b2 && b2 <= 'z') b2 = (byte) (b2 - 'a' + 'A'); + if (b1 != b2) return false; + } + } + } + else + { + for (int i = putIndex(); i-->get;) + { + byte b1 = _bytes[i]; + byte b2 = barray[--bi]; + if (b1 != b2) + { + if ('a' <= b1 && b1 <= 'z') b1 = (byte) (b1 - 'a' + 'A'); + if ('a' <= b2 && b2 <= 'z') b2 = (byte) (b2 - 'a' + 'A'); + if (b1 != b2) return false; + } + } + } + return true; + } + + @Override + public byte get() + { + return _bytes[_get++]; + } + + @Override + public int hashCode() + { + if (_hash == 0 || _hashGet!=_get || _hashPut!=_put) + { + int get=getIndex(); + for (int i = putIndex(); i-- >get;) + { + byte b = _bytes[i]; + if ('a' <= b && b <= 'z') + b = (byte) (b - 'a' + 'A'); + _hash = 31 * _hash + b; + } + if (_hash == 0) + _hash = -1; + _hashGet=_get; + _hashPut=_put; + } + return _hash; + } + + public byte peek(int index) + { + return _bytes[index]; + } + + public int peek(int index, byte[] b, int offset, int length) + { + int l = length; + if (index + l > capacity()) + { + l = capacity() - index; + if (l==0) + return -1; + } + + if (l < 0) + return -1; + + System.arraycopy(_bytes, index, b, offset, l); + return l; + } + + public void poke(int index, byte b) + { + _bytes[index] = b; + } + + @Override + public int poke(int index, Buffer src) + { + _hash=0; + + int length=src.length(); + if (index + length > capacity()) + { + length=capacity()-index; + } + + byte[] src_array = src.array(); + if (src_array != null) + System.arraycopy(src_array, src.getIndex(), _bytes, index, length); + else + { + int s=src.getIndex(); + for (int i=0;i<length;i++) + _bytes[index++]=src.peek(s++); + } + + return length; + } + + @Override + public int poke(int index, byte[] b, int offset, int length) + { + _hash=0; + + if (index + length > capacity()) + { + length=capacity()-index; + } + + System.arraycopy(b, offset, _bytes, index, length); + + return length; + } + + /* ------------------------------------------------------------ */ + @Override + public int space() + { + return _bytes.length - _put; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public static class CaseInsensitive extends ByteArrayBuffer implements Buffer.CaseInsensitve + { + public CaseInsensitive(String s) + { + super(s); + } + + public CaseInsensitive(byte[] b, int o, int l, int rw) + { + super(b,o,l,rw); + } + + @Override + public boolean equals(Object obj) + { + return obj instanceof Buffer && equalsIgnoreCase((Buffer)obj); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ConnectedEndPoint.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ConnectedEndPoint.java new file mode 100644 index 00000000..8959c0b7 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ConnectedEndPoint.java @@ -0,0 +1,25 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +public interface ConnectedEndPoint extends EndPoint +{ + Connection getConnection(); + void setConnection(Connection connection); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Connection.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Connection.java new file mode 100644 index 00000000..8795f5e3 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/Connection.java @@ -0,0 +1,66 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.io.IOException; + +/* ------------------------------------------------------------ */ +/** Abstract Connection used by Jetty Connectors. + * <p> + * Jetty will call the handle method of a connection when there is work + * to be done on the connection. For blocking connections, this is soon + * as the connection is open and handle will keep being called until the + * connection is closed. For non-blocking connections, handle will only + * be called if there are bytes to be read or the connection becomes writable + * after being write blocked. + * + * @see org.eclipse.jetty.io.nio.SelectorManager + */ +public interface Connection +{ + /* ------------------------------------------------------------ */ + /** + * Handle the connection. + * @return The Connection to use for the next handling of the connection. + * This allows protocol upgrades and support for CONNECT. + * @throws IOException if the handling of I/O operations fail + */ + Connection handle() throws IOException; + + /** + * <p>The semantic of this method is to return true to indicate interest in further reads, + * or false otherwise, but it is misnamed and should be really called <code>isReadInterested()</code>.</p> + * + * @return true to indicate interest in further reads, false otherwise + */ + // TODO: rename to isReadInterested() in the next release + boolean isSuspended(); + + /** + * Called after the connection is closed + */ + void onClose(); + + /** + * Called when the connection idle timeout expires + * @param idleForMs how long the connection has been idle + * @see #isIdle() + */ + void onIdleExpired(long idleForMs); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/EndPoint.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/EndPoint.java new file mode 100644 index 00000000..5c183fdc --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/EndPoint.java @@ -0,0 +1,131 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.io.IOException; + +/** + * + * A transport EndPoint + */ +public interface EndPoint +{ + /** + * Shutdown any backing output stream associated with the endpoint + */ + void shutdownOutput() throws IOException; + + boolean isOutputShutdown(); + + /** + * Shutdown any backing input stream associated with the endpoint + */ + void shutdownInput() throws IOException; + + boolean isInputShutdown(); + + /** + * Close any backing stream associated with the endpoint + */ + void close() throws IOException; + + /** + * Fill the buffer from the current putIndex to it's capacity from whatever + * byte source is backing the buffer. The putIndex is increased if bytes filled. + * The buffer may chose to do a compact before filling. + * @return an <code>int</code> value indicating the number of bytes + * filled or -1 if EOF is reached. + * @throws EofException If input is shutdown or the endpoint is closed. + */ + int fill(Buffer buffer) throws IOException; + + /** + * Flush the buffer from the current getIndex to it's putIndex using whatever byte + * sink is backing the buffer. The getIndex is updated with the number of bytes flushed. + * Any mark set is cleared. + * If the entire contents of the buffer are flushed, then an implicit empty() is done. + * + * @param buffer The buffer to flush. This buffers getIndex is updated. + * @return the number of bytes written + * @throws EofException If the endpoint is closed or output is shutdown. + */ + int flush(Buffer buffer) throws IOException; + + /* ------------------------------------------------------------ */ + /** + * @return The local IP address to which this <code>EndPoint</code> is bound, or <code>null</code> + * if this <code>EndPoint</code> does not represent a network connection. + */ + public String getLocalAddr(); + + /* ------------------------------------------------------------ */ + /** + * @return The local port number on which this <code>EndPoint</code> is listening, or <code>0</code> + * if this <code>EndPoint</code> does not represent a network connection. + */ + public int getLocalPort(); + + /* ------------------------------------------------------------ */ + /** + * @return The remote IP address to which this <code>EndPoint</code> is connected, or <code>null</code> + * if this <code>EndPoint</code> does not represent a network connection. + */ + public String getRemoteAddr(); + + /* ------------------------------------------------------------ */ + /** + * @return The remote port number to which this <code>EndPoint</code> is connected, or <code>0</code> + * if this <code>EndPoint</code> does not represent a network connection. + */ + public int getRemotePort(); + + /* ------------------------------------------------------------ */ + public boolean isBlocking(); + + /* ------------------------------------------------------------ */ + public boolean blockWritable(long millisecs) throws IOException; + + /* ------------------------------------------------------------ */ + public boolean isOpen(); + + /* ------------------------------------------------------------ */ + /** Flush any buffered output. + * May fail to write all data if endpoint is non-blocking + * @throws EofException If the endpoint is closed or output is shutdown. + */ + public void flush() throws IOException; + + /* ------------------------------------------------------------ */ + /** Get the max idle time in ms. + * <p>The max idle time is the time the endpoint can be idle before + * extraordinary handling takes place. This loosely corresponds to + * the {@link java.net.Socket#getSoTimeout()} for blocking connections, + * but {@link AsyncEndPoint} implementations must use other mechanisms + * to implement the max idle time. + * @return the max idle time in ms or if ms <= 0 implies an infinite timeout + */ + public int getMaxIdleTime(); + + /* ------------------------------------------------------------ */ + /** Set the max idle time. + * @param timeMs the max idle time in MS. Timeout <= 0 implies an infinite timeout + * @throws IOException if the timeout cannot be set. + */ + public void setMaxIdleTime(int timeMs) throws IOException; +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/EofException.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/EofException.java new file mode 100644 index 00000000..0e6722a1 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/EofException.java @@ -0,0 +1,47 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +import java.io.EOFException; + + +/* ------------------------------------------------------------ */ +/** A Jetty specialization of EOFException. + * <p> This is thrown by Jetty to distinguish between EOF received from + * the connection, vs and EOF thrown by some application talking to some other file/socket etc. + * The only difference in handling is that Jetty EOFs are logged less verbosely. + */ +@SuppressWarnings("serial") +public class EofException extends EOFException +{ + public EofException() + { + } + + public EofException(String reason) + { + super(reason); + } + + public EofException(Throwable th) + { + if (th!=null) + initCause(th); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/SimpleBuffers.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/SimpleBuffers.java new file mode 100644 index 00000000..7d95d473 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/SimpleBuffers.java @@ -0,0 +1,102 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +/* ------------------------------------------------------------ */ +/** SimpleBuffers. + * Simple implementation of Buffers holder. + * + * + */ +public class SimpleBuffers implements Buffers +{ + final Buffer _header; + final Buffer _buffer; + boolean _headerOut; + boolean _bufferOut; + + /* ------------------------------------------------------------ */ + /** + * + */ + public SimpleBuffers(Buffer header, Buffer buffer) + { + _header=header; + _buffer=buffer; + } + + /* ------------------------------------------------------------ */ + public Buffer getBuffer() + { + synchronized(this) + { + if (_buffer!=null && !_bufferOut) + { + _bufferOut=true; + return _buffer; + } + + if (_buffer!=null && _header!=null && _header.capacity()==_buffer.capacity() && !_headerOut) + { + _headerOut=true; + return _header; + } + + if (_buffer!=null) + return new ByteArrayBuffer(_buffer.capacity()); + return new ByteArrayBuffer(4096); + } + } + + /* ------------------------------------------------------------ */ + public Buffer getHeader() + { + synchronized(this) + { + if (_header!=null && !_headerOut) + { + _headerOut=true; + return _header; + } + + if (_buffer!=null && _header!=null && _header.capacity()==_buffer.capacity() && !_bufferOut) + { + _bufferOut=true; + return _buffer; + } + + if (_header!=null) + return new ByteArrayBuffer(_header.capacity()); + return new ByteArrayBuffer(4096); + } + } + + /* ------------------------------------------------------------ */ + public void returnBuffer(Buffer buffer) + { + synchronized(this) + { + buffer.clear(); + if (buffer==_header) + _headerOut=false; + if (buffer==_buffer) + _bufferOut=false; + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ThreadLocalBuffers.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ThreadLocalBuffers.java new file mode 100644 index 00000000..c5e518b1 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/ThreadLocalBuffers.java @@ -0,0 +1,120 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + + + +/* ------------------------------------------------------------ */ +/** Abstract Buffer pool. + * simple unbounded pool of buffers for header, request and response sizes. + * + */ +public class ThreadLocalBuffers extends AbstractBuffers +{ + /* ------------------------------------------------------------ */ + private final ThreadLocal<ThreadBuffers> _buffers=new ThreadLocal<ThreadBuffers>() + { + @Override + protected ThreadBuffers initialValue() + { + return new ThreadBuffers(); + } + }; + + /* ------------------------------------------------------------ */ + public ThreadLocalBuffers(Buffers.Type headerType, int headerSize, Buffers.Type bufferType, int bufferSize, Buffers.Type otherType) + { + super(headerType,headerSize,bufferType,bufferSize,otherType); + } + + /* ------------------------------------------------------------ */ + public Buffer getBuffer() + { + ThreadBuffers buffers = _buffers.get(); + if (buffers._buffer!=null) + { + Buffer b=buffers._buffer; + buffers._buffer=null; + return b; + } + + if (buffers._other!=null && isBuffer(buffers._other)) + { + Buffer b=buffers._other; + buffers._other=null; + return b; + } + + return newBuffer(); + } + + /* ------------------------------------------------------------ */ + public Buffer getHeader() + { + ThreadBuffers buffers = _buffers.get(); + if (buffers._header!=null) + { + Buffer b=buffers._header; + buffers._header=null; + return b; + } + + if (buffers._other!=null && isHeader(buffers._other)) + { + Buffer b=buffers._other; + buffers._other=null; + return b; + } + + return newHeader(); + } + + /* ------------------------------------------------------------ */ + public void returnBuffer(Buffer buffer) + { + buffer.clear(); + if (buffer.isVolatile() || buffer.isImmutable()) + return; + + ThreadBuffers buffers = _buffers.get(); + + if (buffers._header==null && isHeader(buffer)) + buffers._header=buffer; + else if (buffers._buffer==null && isBuffer(buffer)) + buffers._buffer=buffer; + else + buffers._other=buffer; + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return "{{"+getHeaderSize()+","+getBufferSize()+"}}"; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + protected static class ThreadBuffers + { + Buffer _buffer; + Buffer _header; + Buffer _other; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/View.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/View.java new file mode 100644 index 00000000..0ad7200f --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/View.java @@ -0,0 +1,232 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io; + +/** + * A View on another buffer. Allows operations that do not change the _content or + * indexes of the backing buffer. + * + * + * + */ +public class View extends AbstractBuffer +{ + Buffer _buffer; + + /** + * @param buffer The <code>Buffer</code> on which we are presenting a <code>View</code>. + * @param mark The initial value of the {@link Buffer#markIndex mark index} + * @param get The initial value of the {@link Buffer#getIndex get index} + * @param put The initial value of the {@link Buffer#putIndex put index} + * @param access The access level - one of the constants from {@link Buffer}. + */ + public View(Buffer buffer, int mark, int get, int put,int access) + { + super(READWRITE,!buffer.isImmutable()); + _buffer=buffer.buffer(); + setPutIndex(put); + setGetIndex(get); + setMarkIndex(mark); + _access=access; + } + + public View(Buffer buffer) + { + super(READWRITE,!buffer.isImmutable()); + _buffer=buffer.buffer(); + setPutIndex(buffer.putIndex()); + setGetIndex(buffer.getIndex()); + setMarkIndex(buffer.markIndex()); + _access=buffer.isReadOnly()?READONLY:READWRITE; + } + + public View() + { + super(READWRITE,true); + } + + /** + * Update view to buffer + */ + public void update(Buffer buffer) + { + _access=READWRITE; + _buffer=buffer.buffer(); + setGetIndex(0); + setPutIndex(buffer.putIndex()); + setGetIndex(buffer.getIndex()); + setMarkIndex(buffer.markIndex()); + _access=buffer.isReadOnly()?READONLY:READWRITE; + } + + public void update(int get, int put) + { + int a=_access; + _access=READWRITE; + setGetIndex(0); + setPutIndex(put); + setGetIndex(get); + setMarkIndex(-1); + _access=a; + } + + /** + * @return The {@link Buffer#array()} from the underlying buffer. + */ + public byte[] array() + { + return _buffer.array(); + } + + /** + * @return The {@link Buffer#buffer()} from the underlying buffer. + */ + @Override + public Buffer buffer() + { + return _buffer.buffer(); + } + + /** + * @return The {@link Buffer#capacity} of the underlying buffer. + */ + public int capacity() + { + return _buffer.capacity(); + } + + /** + * + */ + @Override + public void clear() + { + setMarkIndex(-1); + setGetIndex(0); + setPutIndex(_buffer.getIndex()); + setGetIndex(_buffer.getIndex()); + } + + /** + * + */ + @Override + public void compact() + { + // TODO + } + + /* + * (non-Javadoc) + * + * @see java.lang.Object#equals(java.lang.Object) + */ + @Override + public boolean equals(Object obj) + { + return this==obj ||((obj instanceof Buffer)&& obj.equals(this)) || super.equals(obj); + } + + /** + * @return Whether the underlying buffer is {@link Buffer#isReadOnly read only} + */ + @Override + public boolean isReadOnly() + { + return _buffer.isReadOnly(); + } + + /** + * @return Whether the underlying buffer is {@link Buffer#isVolatile volatile} + */ + @Override + public boolean isVolatile() + { + return true; + } + + /** + * @return The result of calling {@link Buffer#peek(int)} on the underlying buffer + */ + public byte peek(int index) + { + return _buffer.peek(index); + } + + /** + * @return The result of calling {@link Buffer#peek(int, byte[], int, int)} on the underlying buffer + */ + public int peek(int index, byte[] b, int offset, int length) + { + return _buffer.peek(index,b,offset,length); + } + + /** + * @param index + * @param src + */ + @Override + public int poke(int index, Buffer src) + { + return _buffer.poke(index,src); + } + + /** + * @param index + * @param b + */ + public void poke(int index, byte b) + { + _buffer.poke(index,b); + } + + /** + * @param index + * @param b + * @param offset + * @param length + */ + @Override + public int poke(int index, byte[] b, int offset, int length) + { + return _buffer.poke(index,b,offset,length); + } + + @Override + public String toString() + { + if (_buffer==null) + return "INVALID"; + return super.toString(); + } + + public static class CaseInsensitive extends View implements Buffer.CaseInsensitve + { + public CaseInsensitive() + { + super(); + } + + @Override + public boolean equals(Object obj) + { + return this==obj ||((obj instanceof Buffer)&&((Buffer)obj).equalsIgnoreCase(this)) || super.equals(obj); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/AsyncConnection.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/AsyncConnection.java new file mode 100644 index 00000000..3282fe51 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/AsyncConnection.java @@ -0,0 +1,28 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.io.IOException; + +import org.eclipse.jetty.io.Connection; + +public interface AsyncConnection extends Connection +{ + void onInputShutdown() throws IOException; +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/ChannelEndPoint.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/ChannelEndPoint.java new file mode 100644 index 00000000..baf456d5 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/ChannelEndPoint.java @@ -0,0 +1,478 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.net.SocketException; +import java.nio.ByteBuffer; +import java.nio.channels.ByteChannel; +import java.nio.channels.GatheringByteChannel; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SocketChannel; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Channel End Point. + * <p>Holds the channel and socket for an NIO endpoint. + * + */ +public class ChannelEndPoint implements EndPoint +{ + private static final Logger LOG = Log.getLogger(ChannelEndPoint.class); + + private static final String ALL_INTERFACES = "0.0.0.0"; + + protected final ByteChannel _channel; + protected final ByteBuffer[] _gather2=new ByteBuffer[2]; + protected final Socket _socket; + protected final InetSocketAddress _local; + protected final InetSocketAddress _remote; + protected volatile int _maxIdleTime; + private volatile boolean _ishut; + private volatile boolean _oshut; + + protected ChannelEndPoint(ByteChannel channel, int maxIdleTime) throws IOException + { + this._channel = channel; + _maxIdleTime=maxIdleTime; + _socket=(channel instanceof SocketChannel)?((SocketChannel)channel).socket():null; + if (_socket!=null) + { + _local=(InetSocketAddress)_socket.getLocalSocketAddress(); + _remote=(InetSocketAddress)_socket.getRemoteSocketAddress(); + _socket.setSoTimeout(_maxIdleTime); + } + else + { + _local=_remote=null; + } + } + + public boolean isBlocking() + { + return !(_channel instanceof SelectableChannel) || ((SelectableChannel)_channel).isBlocking(); + } + + public boolean blockReadable(long millisecs) throws IOException + { + return true; + } + + public boolean blockWritable(long millisecs) throws IOException + { + return true; + } + + /* + * @see org.eclipse.io.EndPoint#isOpen() + */ + public boolean isOpen() + { + return _channel.isOpen(); + } + + /** Shutdown the channel Input. + * Cannot be overridden. To override, see {@link #shutdownInput()} + * @throws IOException + */ + protected final void shutdownChannelInput() throws IOException + { + LOG.debug("ishut {}", this); + _ishut = true; + if (_channel.isOpen()) + { + if (_socket != null) + { + try + { + if (!_socket.isInputShutdown()) + { + _socket.shutdownInput(); + } + } + catch (SocketException e) + { + LOG.debug(e.toString()); + LOG.ignore(e); + } + finally + { + if (_oshut) + { + close(); + } + } + } + } + } + + /* (non-Javadoc) + * @see org.eclipse.io.EndPoint#close() + */ + public void shutdownInput() throws IOException + { + shutdownChannelInput(); + } + + protected final void shutdownChannelOutput() throws IOException + { + LOG.debug("oshut {}",this); + _oshut = true; + if (_channel.isOpen()) + { + if (_socket != null) + { + try + { + if (!_socket.isOutputShutdown()) + { + _socket.shutdownOutput(); + } + } + catch (SocketException e) + { + LOG.debug(e.toString()); + LOG.ignore(e); + } + finally + { + if (_ishut) + { + close(); + } + } + } + } + } + + /* (non-Javadoc) + * @see org.eclipse.io.EndPoint#close() + */ + public void shutdownOutput() throws IOException + { + shutdownChannelOutput(); + } + + public boolean isOutputShutdown() + { + return _oshut || !_channel.isOpen() || _socket != null && _socket.isOutputShutdown(); + } + + public boolean isInputShutdown() + { + return _ishut || !_channel.isOpen() || _socket != null && _socket.isInputShutdown(); + } + + /* (non-Javadoc) + * @see org.eclipse.io.EndPoint#close() + */ + public void close() throws IOException + { + LOG.debug("close {}",this); + _channel.close(); + } + + /* (non-Javadoc) + * @see org.eclipse.io.EndPoint#fill(org.eclipse.io.Buffer) + */ + public int fill(Buffer buffer) throws IOException + { + if (_ishut) + return -1; + Buffer buf = buffer.buffer(); + int len=0; + if (buf instanceof NIOBuffer) + { + final NIOBuffer nbuf = (NIOBuffer)buf; + final ByteBuffer bbuf=nbuf.getByteBuffer(); + + //noinspection SynchronizationOnLocalVariableOrMethodParameter + try + { + synchronized(bbuf) + { + try + { + bbuf.position(buffer.putIndex()); + len=_channel.read(bbuf); + } + finally + { + buffer.setPutIndex(bbuf.position()); + bbuf.position(0); + } + } + + if (len<0 && isOpen()) + { + if (!isInputShutdown()) + shutdownInput(); + if (isOutputShutdown()) + _channel.close(); + } + } + catch (IOException x) + { + LOG.debug("Exception while filling", x); + try + { + if (_channel.isOpen()) + _channel.close(); + } + catch (Exception xx) + { + LOG.ignore(xx); + } + + if (len>0) + throw x; + len=-1; + } + } + else + { + throw new IOException("Not Implemented"); + } + + return len; + } + + /* (non-Javadoc) + * @see org.eclipse.io.EndPoint#flush(org.eclipse.io.Buffer) + */ + public int flush(Buffer buffer) throws IOException + { + Buffer buf = buffer.buffer(); + int len=0; + if (buf instanceof NIOBuffer) + { + final NIOBuffer nbuf = (NIOBuffer)buf; + final ByteBuffer bbuf=nbuf.getByteBuffer().asReadOnlyBuffer(); + try + { + bbuf.position(buffer.getIndex()); + bbuf.limit(buffer.putIndex()); + len=_channel.write(bbuf); + } + finally + { + if (len>0) + buffer.skip(len); + } + } + else if (buffer.array()!=null) + { + ByteBuffer b = ByteBuffer.wrap(buffer.array(), buffer.getIndex(), buffer.length()); + len=_channel.write(b); + if (len>0) + buffer.skip(len); + } + else + { + throw new IOException("Not Implemented"); + } + return len; + } + + /* (non-Javadoc) + * @see org.eclipse.io.EndPoint#flush(org.eclipse.io.Buffer, org.eclipse.io.Buffer, org.eclipse.io.Buffer) + */ + public int flush(Buffer header, Buffer buffer, Buffer trailer) throws IOException + { + int length=0; + + Buffer buf0 = header==null?null:header.buffer(); + Buffer buf1 = buffer==null?null:buffer.buffer(); + + if (_channel instanceof GatheringByteChannel && + header!=null && header.length()!=0 && buf0 instanceof NIOBuffer && + buffer!=null && buffer.length()!=0 && buf1 instanceof NIOBuffer) + { + length = gatheringFlush(header,((NIOBuffer)buf0).getByteBuffer(),buffer,((NIOBuffer)buf1).getByteBuffer()); + } + else + { + // flush header + if (header!=null && header.length()>0) + length=flush(header); + + // flush buffer + if ((header==null || header.length()==0) && + buffer!=null && buffer.length()>0) + length+=flush(buffer); + + // flush trailer + if ((header==null || header.length()==0) && + (buffer==null || buffer.length()==0) && + trailer!=null && trailer.length()>0) + length+=flush(trailer); + } + + return length; + } + + protected int gatheringFlush(Buffer header, ByteBuffer bbuf0, Buffer buffer, ByteBuffer bbuf1) throws IOException + { + int length; + + synchronized(this) + { + // Adjust position indexs of buf0 and buf1 + bbuf0=bbuf0.asReadOnlyBuffer(); + bbuf0.position(header.getIndex()); + bbuf0.limit(header.putIndex()); + bbuf1=bbuf1.asReadOnlyBuffer(); + bbuf1.position(buffer.getIndex()); + bbuf1.limit(buffer.putIndex()); + + _gather2[0]=bbuf0; + _gather2[1]=bbuf1; + + // do the gathering write. + length=(int)((GatheringByteChannel)_channel).write(_gather2); + + int hl=header.length(); + if (length>hl) + { + header.clear(); + buffer.skip(length-hl); + } + else if (length>0) + { + header.skip(length); + } + } + return length; + } + + /* ------------------------------------------------------------ */ + /** + * @return Returns the channel. + */ + public ByteChannel getChannel() + { + return _channel; + } + + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.EndPoint#getLocalAddr() + */ + public String getLocalAddr() + { + if (_socket==null) + return null; + if (_local==null || _local.getAddress()==null || _local.getAddress().isAnyLocalAddress()) + return ALL_INTERFACES; + return _local.getAddress().getHostAddress(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.EndPoint#getLocalHost() + */ + public String getLocalHost() + { + if (_socket==null) + return null; + if (_local==null || _local.getAddress()==null || _local.getAddress().isAnyLocalAddress()) + return ALL_INTERFACES; + return _local.getAddress().getCanonicalHostName(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.EndPoint#getLocalPort() + */ + public int getLocalPort() + { + if (_socket==null) + return 0; + if (_local==null) + return -1; + return _local.getPort(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.EndPoint#getRemoteAddr() + */ + public String getRemoteAddr() + { + if (_socket==null) + return null; + if (_remote==null) + return null; + return _remote.getAddress().getHostAddress(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.EndPoint#getRemoteHost() + */ + public String getRemoteHost() + { + if (_socket==null) + return null; + if (_remote==null) + return null; + return _remote.getAddress().getCanonicalHostName(); + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.EndPoint#getRemotePort() + */ + public int getRemotePort() + { + if (_socket==null) + return 0; + return _remote==null?-1:_remote.getPort(); + } + + /* ------------------------------------------------------------ */ + public void flush() + throws IOException + { + } + + /* ------------------------------------------------------------ */ + public int getMaxIdleTime() + { + return _maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @see org.eclipse.jetty.io.bio.StreamEndPoint#setMaxIdleTime(int) + */ + public void setMaxIdleTime(int timeMs) throws IOException + { + if (_socket!=null && timeMs!=_maxIdleTime) + _socket.setSoTimeout(timeMs>0?timeMs:0); + _maxIdleTime=timeMs; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/DirectNIOBuffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/DirectNIOBuffer.java new file mode 100644 index 00000000..4bd61b2e --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/DirectNIOBuffer.java @@ -0,0 +1,226 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.channels.FileChannel; + +import org.eclipse.jetty.io.AbstractBuffer; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/* ------------------------------------------------------------------------------- */ +/** + * + * + */ +public class DirectNIOBuffer extends AbstractBuffer implements NIOBuffer +{ + private static final Logger LOG = Log.getLogger(DirectNIOBuffer.class); + + protected final ByteBuffer _buf; + + public DirectNIOBuffer(int size) + { + super(READWRITE,NON_VOLATILE); + _buf = ByteBuffer.allocateDirect(size); + _buf.position(0); + _buf.limit(_buf.capacity()); + } + + public DirectNIOBuffer(ByteBuffer buffer,boolean immutable) + { + super(immutable?IMMUTABLE:READWRITE,NON_VOLATILE); + if (!buffer.isDirect()) + throw new IllegalArgumentException(); + _buf = buffer; + setGetIndex(buffer.position()); + setPutIndex(buffer.limit()); + } + + /** + * @param file + */ + public DirectNIOBuffer(File file) throws IOException + { + super(READONLY,NON_VOLATILE); + FileInputStream fis = null; + FileChannel fc = null; + try + { + fis = new FileInputStream(file); + fc = fis.getChannel(); + _buf = fc.map(FileChannel.MapMode.READ_ONLY, 0, file.length()); + setGetIndex(0); + setPutIndex((int)file.length()); + _access=IMMUTABLE; + } + finally + { + if (fc != null) try {fc.close();} catch (IOException e){LOG.ignore(e);} + IO.close(fis); + } + } + + /* ------------------------------------------------------------ */ + public boolean isDirect() + { + return true; + } + + /* ------------------------------------------------------------ */ + public byte[] array() + { + return null; + } + + /* ------------------------------------------------------------ */ + public int capacity() + { + return _buf.capacity(); + } + + /* ------------------------------------------------------------ */ + public byte peek(int position) + { + return _buf.get(position); + } + + public int peek(int index, byte[] b, int offset, int length) + { + int l = length; + if (index+l > capacity()) + { + l=capacity()-index; + if (l==0) + return -1; + } + + if (l < 0) + return -1; + try + { + _buf.position(index); + _buf.get(b,offset,l); + } + finally + { + _buf.position(0); + } + + return l; + } + + public void poke(int index, byte b) + { + if (isReadOnly()) throw new IllegalStateException(__READONLY); + if (index < 0) throw new IllegalArgumentException("index<0: " + index + "<0"); + if (index > capacity()) + throw new IllegalArgumentException("index>capacity(): " + index + ">" + capacity()); + _buf.put(index,b); + } + + @Override + public int poke(int index, Buffer src) + { + if (isReadOnly()) throw new IllegalStateException(__READONLY); + + byte[] array=src.array(); + if (array!=null) + { + return poke(index,array,src.getIndex(),src.length()); + } + else + { + Buffer src_buf=src.buffer(); + if (src_buf instanceof DirectNIOBuffer) + { + ByteBuffer src_bytebuf = ((DirectNIOBuffer)src_buf)._buf; + if (src_bytebuf==_buf) + src_bytebuf=_buf.duplicate(); + try + { + _buf.position(index); + int space = _buf.remaining(); + + int length=src.length(); + if (length>space) + length=space; + + src_bytebuf.position(src.getIndex()); + src_bytebuf.limit(src.getIndex()+length); + + _buf.put(src_bytebuf); + return length; + } + finally + { + _buf.position(0); + src_bytebuf.limit(src_bytebuf.capacity()); + src_bytebuf.position(0); + } + } + else + return super.poke(index,src); + } + } + + @Override + public int poke(int index, byte[] b, int offset, int length) + { + if (isReadOnly()) throw new IllegalStateException(__READONLY); + + if (index < 0) throw new IllegalArgumentException("index<0: " + index + "<0"); + + if (index + length > capacity()) + { + length=capacity()-index; + if (length<0) + throw new IllegalArgumentException("index>capacity(): " + index + ">" + capacity()); + } + + try + { + _buf.position(index); + + int space=_buf.remaining(); + + if (length>space) + length=space; + if (length>0) + _buf.put(b,offset,length); + return length; + } + finally + { + _buf.position(0); + } + } + + /* ------------------------------------------------------------ */ + public ByteBuffer getByteBuffer() + { + return _buf; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/IndirectNIOBuffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/IndirectNIOBuffer.java new file mode 100644 index 00000000..e9500f06 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/IndirectNIOBuffer.java @@ -0,0 +1,43 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.io.ByteArrayBuffer; + +public class IndirectNIOBuffer extends ByteArrayBuffer implements NIOBuffer +{ + protected final ByteBuffer _buf; + + /* ------------------------------------------------------------ */ + public IndirectNIOBuffer(int size) + { + super(size,READWRITE,NON_VOLATILE); + _buf = ByteBuffer.wrap(_bytes); + _buf.position(0); + _buf.limit(_buf.capacity()); + } + + /* ------------------------------------------------------------ */ + public ByteBuffer getByteBuffer() + { + return _buf; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/NIOBuffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/NIOBuffer.java new file mode 100644 index 00000000..ba4f4b60 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/NIOBuffer.java @@ -0,0 +1,34 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.nio.ByteBuffer; + +import org.eclipse.jetty.io.Buffer; + +/* ------------------------------------------------------------------------------- */ +/** + * + * + */ +public interface NIOBuffer extends Buffer +{ + /* ------------------------------------------------------------ */ + public ByteBuffer getByteBuffer(); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java new file mode 100644 index 00000000..88d7ff7c --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SelectChannelEndPoint.java @@ -0,0 +1,802 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.io.IOException; +import java.io.InterruptedIOException; +import java.nio.channels.ClosedChannelException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.Locale; + +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.ConnectedEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EofException; +import org.eclipse.jetty.io.nio.SelectorManager.SelectSet; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Timeout.Task; + +/* ------------------------------------------------------------ */ +/** + * An Endpoint that can be scheduled by {@link SelectorManager}. + */ +public class SelectChannelEndPoint extends ChannelEndPoint implements AsyncEndPoint, ConnectedEndPoint +{ + public static final Logger LOG=Log.getLogger("org.eclipse.jetty.io.nio"); + + private final boolean WORK_AROUND_JVM_BUG_6346658 = System.getProperty("os.name").toLowerCase(Locale.ENGLISH).contains("win"); + private final SelectorManager.SelectSet _selectSet; + private final SelectorManager _manager; + private SelectionKey _key; + private final Runnable _handler = new Runnable() + { + public void run() { handle(); } + }; + + /** The desired value for {@link SelectionKey#interestOps()} */ + private int _interestOps; + + /** + * The connection instance is the handler for any IO activity on the endpoint. + * There is a different type of connection for HTTP, AJP, WebSocket and + * ProxyConnect. The connection may change for an SCEP as it is upgraded + * from HTTP to proxy connect or websocket. + */ + private volatile AsyncConnection _connection; + + private static final int STATE_NEEDS_DISPATCH=-1; + private static final int STATE_UNDISPATCHED=0; + private static final int STATE_DISPATCHED=1; + private static final int STATE_ASYNC=2; + private int _state; + + private boolean _onIdle; + + /** true if the last write operation succeed and wrote all offered bytes */ + private volatile boolean _writable = true; + + + /** True if a thread has is blocked in {@link #blockReadable(long)} */ + private boolean _readBlocked; + + /** True if a thread has is blocked in {@link #blockWritable(long)} */ + private boolean _writeBlocked; + + /** true if {@link SelectSet#destroyEndPoint(SelectChannelEndPoint)} has not been called */ + private boolean _open; + + private volatile long _idleTimestamp; + private volatile boolean _checkIdle; + + private boolean _interruptable; + + private boolean _ishut; + + /* ------------------------------------------------------------ */ + public SelectChannelEndPoint(SocketChannel channel, SelectSet selectSet, SelectionKey key, int maxIdleTime) + throws IOException + { + super(channel, maxIdleTime); + + _manager = selectSet.getManager(); + _selectSet = selectSet; + _state=STATE_UNDISPATCHED; + _onIdle=false; + _open=true; + _key = key; + + setCheckForIdle(true); + } + + /* ------------------------------------------------------------ */ + public Connection getConnection() + { + return _connection; + } + + /* ------------------------------------------------------------ */ + public void setConnection(Connection connection) + { + Connection old=_connection; + _connection=(AsyncConnection)connection; + if (old!=null && old!=_connection) + _manager.endPointUpgraded(this,old); + } + + /* ------------------------------------------------------------ */ + /** Called by selectSet to schedule handling + * + */ + public void schedule() + { + synchronized (this) + { + // If there is no key, then do nothing + if (_key == null || !_key.isValid()) + { + _readBlocked=false; + _writeBlocked=false; + this.notifyAll(); + return; + } + + // If there are threads dispatched reading and writing + if (_readBlocked || _writeBlocked) + { + // assert _dispatched; + if (_readBlocked && _key.isReadable()) + _readBlocked=false; + if (_writeBlocked && _key.isWritable()) + _writeBlocked=false; + + // wake them up is as good as a dispatched. + this.notifyAll(); + + // we are not interested in further selecting + _key.interestOps(0); + if (_state<STATE_DISPATCHED) + updateKey(); + return; + } + + // Remove writeable op + if ((_key.readyOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE && (_key.interestOps() & SelectionKey.OP_WRITE) == SelectionKey.OP_WRITE) + { + // Remove writeable op + _interestOps = _key.interestOps() & ~SelectionKey.OP_WRITE; + _key.interestOps(_interestOps); + _writable = true; // Once writable is in ops, only removed with dispatch. + } + + // If dispatched, then deregister interest + if (_state>=STATE_DISPATCHED) + _key.interestOps(0); + else + { + // other wise do the dispatch + dispatch(); + if (_state>=STATE_DISPATCHED && !_selectSet.getManager().isDeferringInterestedOps0()) + { + _key.interestOps(0); + } + } + } + } + + /* ------------------------------------------------------------ */ + public void asyncDispatch() + { + synchronized(this) + { + switch(_state) + { + case STATE_NEEDS_DISPATCH: + case STATE_UNDISPATCHED: + dispatch(); + break; + + case STATE_DISPATCHED: + case STATE_ASYNC: + _state=STATE_ASYNC; + break; + } + } + } + + /* ------------------------------------------------------------ */ + public void dispatch() + { + synchronized(this) + { + if (_state<=STATE_UNDISPATCHED) + { + if (_onIdle) + _state = STATE_NEEDS_DISPATCH; + else + { + _state = STATE_DISPATCHED; + boolean dispatched = _manager.dispatch(_handler); + if(!dispatched) + { + _state = STATE_NEEDS_DISPATCH; + LOG.warn("Dispatched Failed! "+this+" to "+_manager); + updateKey(); + } + } + } + } + } + + /* ------------------------------------------------------------ */ + /** + * Called when a dispatched thread is no longer handling the endpoint. + * The selection key operations are updated. + * @return If false is returned, the endpoint has been redispatched and + * thread must keep handling the endpoint. + */ + protected boolean undispatch() + { + synchronized (this) + { + switch(_state) + { + case STATE_ASYNC: + _state=STATE_DISPATCHED; + return false; + + default: + _state=STATE_UNDISPATCHED; + updateKey(); + return true; + } + } + } + + /* ------------------------------------------------------------ */ + public void cancelTimeout(Task task) + { + getSelectSet().cancelTimeout(task); + } + + /* ------------------------------------------------------------ */ + public void scheduleTimeout(Task task, long timeoutMs) + { + getSelectSet().scheduleTimeout(task,timeoutMs); + } + + /* ------------------------------------------------------------ */ + public void setCheckForIdle(boolean check) + { + if (check) + { + _idleTimestamp=System.currentTimeMillis(); + _checkIdle=true; + } + else + _checkIdle=false; + } + + /* ------------------------------------------------------------ */ + public boolean isCheckForIdle() + { + return _checkIdle; + } + + /* ------------------------------------------------------------ */ + protected void notIdle() + { + _idleTimestamp=System.currentTimeMillis(); + } + + /* ------------------------------------------------------------ */ + public void checkIdleTimestamp(long now) + { + if (isCheckForIdle() && _maxIdleTime>0) + { + final long idleForMs=now-_idleTimestamp; + + if (idleForMs>_maxIdleTime) + { + // Don't idle out again until onIdleExpired task completes. + setCheckForIdle(false); + _manager.dispatch(new Runnable() + { + public void run() + { + try + { + onIdleExpired(idleForMs); + } + finally + { + setCheckForIdle(true); + } + } + }); + } + } + } + + /* ------------------------------------------------------------ */ + public void onIdleExpired(long idleForMs) + { + try + { + synchronized (this) + { + _onIdle=true; + } + + _connection.onIdleExpired(idleForMs); + } + finally + { + synchronized (this) + { + _onIdle=false; + if (_state==STATE_NEEDS_DISPATCH) + dispatch(); + } + } + } + + /* ------------------------------------------------------------ */ + @Override + public int fill(Buffer buffer) throws IOException + { + int fill=super.fill(buffer); + if (fill>0) + notIdle(); + return fill; + } + + /* ------------------------------------------------------------ */ + /* + */ + @Override + public int flush(Buffer buffer) throws IOException + { + int l = super.flush(buffer); + + // If there was something to write and it wasn't written, then we are not writable. + if (l==0 && buffer!=null && buffer.hasContent()) + { + synchronized (this) + { + _writable=false; + if (_state<STATE_DISPATCHED) + updateKey(); + } + } + else if (l>0) + { + _writable=true; + notIdle(); + } + + return l; + } + + /* ------------------------------------------------------------ */ + /* + * Allows thread to block waiting for further events. + */ + @SuppressWarnings("serial") + @Override + public boolean blockReadable(long timeoutMs) throws IOException + { + synchronized (this) + { + if (isInputShutdown()) + throw new EofException(); + + long now=_selectSet.getNow(); + long end=now+timeoutMs; + boolean check=isCheckForIdle(); + setCheckForIdle(true); + try + { + _readBlocked=true; + while (!isInputShutdown() && _readBlocked) + { + try + { + updateKey(); + this.wait(timeoutMs>0?(end-now):10000); + } + catch (final InterruptedException e) + { + LOG.warn(e); + if (_interruptable) + throw new InterruptedIOException(){{this.initCause(e);}}; + } + finally + { + now=_selectSet.getNow(); + } + + if (_readBlocked && timeoutMs>0 && now>=end) + return false; + } + } + finally + { + _readBlocked=false; + setCheckForIdle(check); + } + } + return true; + } + + /* ------------------------------------------------------------ */ + /* + * Allows thread to block waiting for further events. + */ + @SuppressWarnings("serial") + @Override + public boolean blockWritable(long timeoutMs) throws IOException + { + synchronized (this) + { + if (isOutputShutdown()) + throw new EofException(); + + long now=_selectSet.getNow(); + long end=now+timeoutMs; + boolean check=isCheckForIdle(); + setCheckForIdle(true); + try + { + _writeBlocked=true; + while (_writeBlocked && !isOutputShutdown()) + { + try + { + updateKey(); + this.wait(timeoutMs>0?(end-now):10000); + } + catch (final InterruptedException e) + { + LOG.warn(e); + if (_interruptable) + throw new InterruptedIOException(){{this.initCause(e);}}; + } + finally + { + now=_selectSet.getNow(); + } + if (_writeBlocked && timeoutMs>0 && now>=end) + return false; + } + } + finally + { + _writeBlocked=false; + setCheckForIdle(check); + } + } + return true; + } + + /* ------------------------------------------------------------ */ + /** + * @see org.eclipse.jetty.io.AsyncEndPoint#scheduleWrite() + */ + public void scheduleWrite() + { + if (_writable) + LOG.debug("Required scheduleWrite {}",this); + + _writable=false; + updateKey(); + } + + /* ------------------------------------------------------------ */ + public boolean isWritable() + { + return _writable; + } + + /* ------------------------------------------------------------ */ + public boolean hasProgressed() + { + return false; + } + + /* ------------------------------------------------------------ */ + /** + * Updates selection key. Adds operations types to the selection key as needed. No operations + * are removed as this is only done during dispatch. This method records the new key and + * schedules a call to doUpdateKey to do the keyChange + */ + private void updateKey() + { + final boolean changed; + synchronized (this) + { + int current_ops=-1; + if (getChannel().isOpen()) + { + boolean read_interest = _readBlocked || (_state<STATE_DISPATCHED && !_connection.isSuspended()); + boolean write_interest= _writeBlocked || (_state<STATE_DISPATCHED && !_writable); + + _interestOps = + ((!_socket.isInputShutdown() && read_interest ) ? SelectionKey.OP_READ : 0) + | ((!_socket.isOutputShutdown()&& write_interest) ? SelectionKey.OP_WRITE : 0); + try + { + current_ops = ((_key!=null && _key.isValid())?_key.interestOps():-1); + } + catch(Exception e) + { + _key=null; + LOG.ignore(e); + } + } + changed=_interestOps!=current_ops; + } + + if(changed) + { + _selectSet.addChange(this); + _selectSet.wakeup(); + } + } + + /* ------------------------------------------------------------ */ + /** + * Synchronize the interestOps with the actual key. Call is scheduled by a call to updateKey + */ + void doUpdateKey() + { + synchronized (this) + { + if (getChannel().isOpen()) + { + if (_interestOps>0) + { + if (_key==null || !_key.isValid()) + { + SelectableChannel sc = (SelectableChannel)getChannel(); + if (sc.isRegistered()) + { + updateKey(); + } + else + { + try + { + _key=((SelectableChannel)getChannel()).register(_selectSet.getSelector(),_interestOps,this); + } + catch (Exception e) + { + LOG.ignore(e); + if (_key!=null && _key.isValid()) + { + _key.cancel(); + } + + if (_open) + { + _selectSet.destroyEndPoint(this); + } + _open=false; + _key = null; + } + } + } + else + { + _key.interestOps(_interestOps); + } + } + else + { + if (_key!=null && _key.isValid()) + _key.interestOps(0); + else + _key=null; + } + } + else + { + if (_key!=null && _key.isValid()) + _key.cancel(); + + if (_open) + { + _open=false; + _selectSet.destroyEndPoint(this); + } + _key = null; + } + } + } + + /* ------------------------------------------------------------ */ + /* + */ + protected void handle() + { + boolean dispatched=true; + try + { + while(dispatched) + { + try + { + while(true) + { + final AsyncConnection next = (AsyncConnection)_connection.handle(); + if (next!=_connection) + { + LOG.debug("{} replaced {}",next,_connection); + Connection old=_connection; + _connection=next; + _manager.endPointUpgraded(this,old); + continue; + } + break; + } + } + catch (ClosedChannelException e) + { + LOG.ignore(e); + } + catch (EofException e) + { + LOG.debug("EOF", e); + try{close();} + catch(IOException e2){LOG.ignore(e2);} + } + catch (IOException e) + { + LOG.warn(e.toString()); + try{close();} + catch(IOException e2){LOG.ignore(e2);} + } + catch (Throwable e) + { + LOG.warn("handle failed", e); + try{close();} + catch(IOException e2){LOG.ignore(e2);} + } + finally + { + if (!_ishut && isInputShutdown() && isOpen()) + { + _ishut=true; + try + { + _connection.onInputShutdown(); + } + catch(Throwable x) + { + LOG.warn("onInputShutdown failed", x); + try{close();} + catch(IOException e2){LOG.ignore(e2);} + } + finally + { + updateKey(); + } + } + dispatched=!undispatch(); + } + } + } + finally + { + if (dispatched) + { + dispatched=!undispatch(); + while (dispatched) + { + LOG.warn("SCEP.run() finally DISPATCHED"); + dispatched=!undispatch(); + } + } + } + } + + /* ------------------------------------------------------------ */ + /* + * @see org.eclipse.io.nio.ChannelEndPoint#close() + */ + @Override + public void close() throws IOException + { + // On unix systems there is a JVM issue that if you cancel before closing, it can + // cause the selector to block waiting for a channel to close and that channel can + // block waiting for the remote end. But on windows, if you don't cancel before a + // close, then the selector can block anyway! + // https://bugs.eclipse.org/bugs/show_bug.cgi?id=357318 + if (WORK_AROUND_JVM_BUG_6346658) + { + try + { + SelectionKey key = _key; + if (key!=null) + key.cancel(); + } + catch (Throwable e) + { + LOG.ignore(e); + } + } + + try + { + super.close(); + } + catch (IOException e) + { + LOG.ignore(e); + } + finally + { + updateKey(); + } + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + // Do NOT use synchronized (this) + // because it's very easy to deadlock when debugging is enabled. + // We do a best effort to print the right toString() and that's it. + SelectionKey key = _key; + String keyString = ""; + if (key != null) + { + if (key.isValid()) + { + if (key.isReadable()) + keyString += "r"; + if (key.isWritable()) + keyString += "w"; + } + else + { + keyString += "!"; + } + } + else + { + keyString += "-"; + } + return String.format(Locale.getDefault(), + "SCEP@%x{l(%s)<->r(%s),s=%d,open=%b,ishut=%b,oshut=%b,rb=%b,wb=%b,w=%b,i=%d%s}-{%s}", + hashCode(), + _socket.getRemoteSocketAddress(), + _socket.getLocalSocketAddress(), + _state, + isOpen(), + isInputShutdown(), + isOutputShutdown(), + _readBlocked, + _writeBlocked, + _writable, + _interestOps, + keyString, + _connection); + } + + /* ------------------------------------------------------------ */ + public SelectSet getSelectSet() + { + return _selectSet; + } + + /* ------------------------------------------------------------ */ + /** + * Don't set the SoTimeout + * @see org.eclipse.jetty.io.nio.ChannelEndPoint#setMaxIdleTime(int) + */ + @Override + public void setMaxIdleTime(int timeMs) throws IOException + { + _maxIdleTime=timeMs; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SelectorManager.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SelectorManager.java new file mode 100644 index 00000000..dd4e6816 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SelectorManager.java @@ -0,0 +1,891 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import java.io.IOException; +import java.nio.channels.CancelledKeyException; +import java.nio.channels.Channel; +import java.nio.channels.ClosedSelectorException; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.ConnectedEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.AggregateLifeCycle; +import org.eclipse.jetty.util.component.Dumpable; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.thread.Timeout; +import org.eclipse.jetty.util.thread.Timeout.Task; + + +/* ------------------------------------------------------------ */ +/** + * The Selector Manager manages and number of SelectSets to allow + * NIO scheduling to scale to large numbers of connections. + * <p> + */ +public abstract class SelectorManager extends AbstractLifeCycle implements Dumpable +{ + public static final Logger LOG=Log.getLogger("org.eclipse.jetty.io.nio"); + + private static final int __MONITOR_PERIOD=Integer.getInteger("org.eclipse.jetty.io.nio.MONITOR_PERIOD",1000).intValue(); + private static final int __MAX_SELECTS=Integer.getInteger("org.eclipse.jetty.io.nio.MAX_SELECTS",100000).intValue(); + private static final int __BUSY_PAUSE=Integer.getInteger("org.eclipse.jetty.io.nio.BUSY_PAUSE",50).intValue(); + private static final int __IDLE_TICK=Integer.getInteger("org.eclipse.jetty.io.nio.IDLE_TICK",400).intValue(); + + private int _maxIdleTime; + private int _lowResourcesMaxIdleTime; + private long _lowResourcesConnections; + private SelectSet[] _selectSet; + private int _selectSets=1; + private volatile int _set=0; + private boolean _deferringInterestedOps0=true; + private int _selectorPriorityDelta=0; + + /* ------------------------------------------------------------ */ + /** + * @return the max idle time + */ + public long getMaxIdleTime() + { + return _maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @return the number of select sets in use + */ + public int getSelectSets() + { + return _selectSets; + } + + /* ------------------------------------------------------------ */ + /** Register a channel + * @param channel + * @param att Attached Object + */ + public void register(SocketChannel channel, Object att) + { + // The ++ increment here is not atomic, but it does not matter. + // so long as the value changes sometimes, then connections will + // be distributed over the available sets. + + int s=_set++; + if (s<0) + s=-s; + s=s%_selectSets; + SelectSet[] sets=_selectSet; + if (sets!=null) + { + SelectSet set=sets[s]; + set.addChange(channel,att); + set.wakeup(); + } + } + + /* ------------------------------------------------------------ */ + /** + * @return delta The value to add to the selector thread priority. + */ + public int getSelectorPriorityDelta() + { + return _selectorPriorityDelta; + } + + /* ------------------------------------------------------------------------------- */ + public abstract boolean dispatch(Runnable task); + + /* ------------------------------------------------------------ */ + /* (non-Javadoc) + * @see org.eclipse.component.AbstractLifeCycle#doStart() + */ + @Override + protected void doStart() throws Exception + { + _selectSet = new SelectSet[_selectSets]; + for (int i=0;i<_selectSet.length;i++) + _selectSet[i]= new SelectSet(i); + + super.doStart(); + + // start a thread to Select + for (int i=0;i<getSelectSets();i++) + { + final int id=i; + boolean selecting=dispatch(new Runnable() + { + public void run() + { + String name=Thread.currentThread().getName(); + int priority=Thread.currentThread().getPriority(); + try + { + SelectSet[] sets=_selectSet; + if (sets==null) + return; + SelectSet set=sets[id]; + + Thread.currentThread().setName(name+" Selector"+id); + if (getSelectorPriorityDelta()!=0) + Thread.currentThread().setPriority(Thread.currentThread().getPriority()+getSelectorPriorityDelta()); + LOG.debug("Starting {} on {}",Thread.currentThread(),this); + while (isRunning()) + { + try + { + set.doSelect(); + } + catch(IOException e) + { + LOG.ignore(e); + } + catch(Exception e) + { + LOG.warn(e); + } + } + } + finally + { + LOG.debug("Stopped {} on {}",Thread.currentThread(),this); + Thread.currentThread().setName(name); + if (getSelectorPriorityDelta()!=0) + Thread.currentThread().setPriority(priority); + } + } + + }); + + if (!selecting) + throw new IllegalStateException("!Selecting"); + } + } + + /* ------------------------------------------------------------------------------- */ + @Override + protected void doStop() throws Exception + { + SelectSet[] sets= _selectSet; + _selectSet=null; + if (sets!=null) + { + for (SelectSet set : sets) + { + if (set!=null) + set.stop(); + } + } + super.doStop(); + } + + /* ------------------------------------------------------------ */ + /** + * @param endpoint + */ + protected abstract void endPointClosed(SelectChannelEndPoint endpoint); + + /* ------------------------------------------------------------ */ + /** + * @param endpoint + */ + protected abstract void endPointOpened(SelectChannelEndPoint endpoint); + + /* ------------------------------------------------------------ */ + protected abstract void endPointUpgraded(ConnectedEndPoint endpoint,Connection oldConnection); + + /* ------------------------------------------------------------------------------- */ + public abstract AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint, Object attachment); + + /* ------------------------------------------------------------ */ + /** + * Create a new end point + * @param channel + * @param selectSet + * @param sKey the selection key + * @return the new endpoint {@link SelectChannelEndPoint} + * @throws IOException + */ + protected abstract SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectorManager.SelectSet selectSet, SelectionKey sKey) throws IOException; + + /* ------------------------------------------------------------------------------- */ + protected void connectionFailed(SocketChannel channel,Throwable ex,Object attachment) + { + LOG.warn(ex+","+channel+","+attachment); + LOG.debug(ex); + } + + /* ------------------------------------------------------------ */ + public void dump(Appendable out, String indent) throws IOException + { + AggregateLifeCycle.dumpObject(out,this); + AggregateLifeCycle.dump(out,indent,Arrays.asList(_selectSet)); + } + + /* ------------------------------------------------------------------------------- */ + /* ------------------------------------------------------------------------------- */ + /* ------------------------------------------------------------------------------- */ + public class SelectSet implements Dumpable + { + private final int _setID; + private final Timeout _timeout; + + private final ConcurrentLinkedQueue<Object> _changes = new ConcurrentLinkedQueue<Object>(); + + private volatile Selector _selector; + + private volatile Thread _selecting; + private int _busySelects; + private long _monitorNext; + private boolean _pausing; + private boolean _paused; + private volatile long _idleTick; + private ConcurrentMap<SelectChannelEndPoint,Object> _endPoints = new ConcurrentHashMap<SelectChannelEndPoint, Object>(); + + /* ------------------------------------------------------------ */ + SelectSet(int acceptorID) throws Exception + { + _setID=acceptorID; + + _idleTick = System.currentTimeMillis(); + _timeout = new Timeout(this); + _timeout.setDuration(0L); + + // create a selector; + _selector = Selector.open(); + _monitorNext=System.currentTimeMillis()+__MONITOR_PERIOD; + } + + /* ------------------------------------------------------------ */ + public void addChange(Object change) + { + _changes.add(change); + } + + /* ------------------------------------------------------------ */ + public void addChange(SelectableChannel channel, Object att) + { + if (att==null) + addChange(channel); + else if (att instanceof EndPoint) + addChange(att); + else + addChange(new ChannelAndAttachment(channel,att)); + } + + /* ------------------------------------------------------------ */ + /** + * Select and dispatch tasks found from changes and the selector. + * + * @throws IOException + */ + public void doSelect() throws IOException + { + try + { + _selecting=Thread.currentThread(); + final Selector selector=_selector; + // Stopped concurrently ? + if (selector == null) + return; + + // Make any key changes required + Object change; + int changes=_changes.size(); + while (changes-->0 && (change=_changes.poll())!=null) + { + Channel ch=null; + SelectionKey key=null; + + try + { + if (change instanceof EndPoint) + { + // Update the operations for a key. + SelectChannelEndPoint endpoint = (SelectChannelEndPoint)change; + ch=endpoint.getChannel(); + endpoint.doUpdateKey(); + } + else if (change instanceof ChannelAndAttachment) + { + // finish accepting/connecting this connection + final ChannelAndAttachment asc = (ChannelAndAttachment)change; + final SelectableChannel channel=asc._channel; + ch=channel; + final Object att = asc._attachment; + + if ((channel instanceof SocketChannel) && ((SocketChannel)channel).isConnected()) + { + key = channel.register(selector,SelectionKey.OP_READ,att); + SelectChannelEndPoint endpoint = createEndPoint((SocketChannel)channel,key); + key.attach(endpoint); + endpoint.schedule(); + } + else if (channel.isOpen()) + { + key = channel.register(selector,SelectionKey.OP_CONNECT,att); + } + } + else if (change instanceof SocketChannel) + { + // Newly registered channel + final SocketChannel channel=(SocketChannel)change; + ch=channel; + key = channel.register(selector,SelectionKey.OP_READ,null); + SelectChannelEndPoint endpoint = createEndPoint(channel,key); + key.attach(endpoint); + endpoint.schedule(); + } + else if (change instanceof ChangeTask) + { + ((Runnable)change).run(); + } + else if (change instanceof Runnable) + { + dispatch((Runnable)change); + } + else + throw new IllegalArgumentException(change.toString()); + } + catch (CancelledKeyException e) + { + LOG.ignore(e); + } + catch (Throwable e) + { + if (isRunning()) + LOG.warn(e); + else + LOG.debug(e); + + try + { + if (ch!=null) + ch.close(); + } + catch(IOException e2) + { + LOG.debug(e2); + } + } + } + + + // Do and instant select to see if any connections can be handled. + int selected=selector.selectNow(); + + long now=System.currentTimeMillis(); + + // if no immediate things to do + if (selected==0 && selector.selectedKeys().isEmpty()) + { + // If we are in pausing mode + if (_pausing) + { + try + { + Thread.sleep(__BUSY_PAUSE); // pause to reduce impact of busy loop + } + catch(InterruptedException e) + { + LOG.ignore(e); + } + now=System.currentTimeMillis(); + } + + // workout how long to wait in select + _timeout.setNow(now); + long to_next_timeout=_timeout.getTimeToNext(); + + long wait = _changes.size()==0?__IDLE_TICK:0L; + if (wait > 0 && to_next_timeout >= 0 && wait > to_next_timeout) + wait = to_next_timeout; + + // If we should wait with a select + if (wait>0) + { + long before=now; + selector.select(wait); + now = System.currentTimeMillis(); + _timeout.setNow(now); + + // If we are monitoring for busy selector + // and this select did not wait more than 1ms + if (__MONITOR_PERIOD>0 && now-before <=1) + { + // count this as a busy select and if there have been too many this monitor cycle + if (++_busySelects>__MAX_SELECTS) + { + // Start injecting pauses + _pausing=true; + + // if this is the first pause + if (!_paused) + { + // Log and dump some status + _paused=true; + LOG.warn("Selector {} is too busy, pausing!",this); + } + } + } + } + } + + // have we been destroyed while sleeping + if (_selector==null || !selector.isOpen()) + return; + + // Look for things to do + for (SelectionKey key: selector.selectedKeys()) + { + SocketChannel channel=null; + + try + { + if (!key.isValid()) + { + key.cancel(); + SelectChannelEndPoint endpoint = (SelectChannelEndPoint)key.attachment(); + if (endpoint != null) + endpoint.doUpdateKey(); + continue; + } + + Object att = key.attachment(); + if (att instanceof SelectChannelEndPoint) + { + if (key.isReadable()||key.isWritable()) + ((SelectChannelEndPoint)att).schedule(); + } + else if (key.isConnectable()) + { + // Complete a connection of a registered channel + channel = (SocketChannel)key.channel(); + boolean connected=false; + try + { + connected=channel.finishConnect(); + } + catch(Exception e) + { + connectionFailed(channel,e,att); + } + finally + { + if (connected) + { + key.interestOps(SelectionKey.OP_READ); + SelectChannelEndPoint endpoint = createEndPoint(channel,key); + key.attach(endpoint); + endpoint.schedule(); + } + else + { + key.cancel(); + channel.close(); + } + } + } + else + { + // Wrap readable registered channel in an endpoint + channel = (SocketChannel)key.channel(); + SelectChannelEndPoint endpoint = createEndPoint(channel,key); + key.attach(endpoint); + if (key.isReadable()) + endpoint.schedule(); + } + key = null; + } + catch (CancelledKeyException e) + { + LOG.ignore(e); + } + catch (Exception e) + { + if (isRunning()) + LOG.warn(e); + else + LOG.ignore(e); + + try + { + if (channel!=null) + channel.close(); + } + catch(IOException e2) + { + LOG.debug(e2); + } + + if (key != null && !(key.channel() instanceof ServerSocketChannel) && key.isValid()) + key.cancel(); + } + } + + // Everything always handled + selector.selectedKeys().clear(); + + now=System.currentTimeMillis(); + _timeout.setNow(now); + Task task = _timeout.expired(); + while (task!=null) + { + if (task instanceof Runnable) + dispatch((Runnable)task); + task = _timeout.expired(); + } + + // Idle tick + if (now-_idleTick>__IDLE_TICK) + { + _idleTick=now; + + final long idle_now=((_lowResourcesConnections>0 && selector.keys().size()>_lowResourcesConnections)) + ?(now+_maxIdleTime-_lowResourcesMaxIdleTime) + :now; + + dispatch(new Runnable() + { + public void run() + { + for (SelectChannelEndPoint endp:_endPoints.keySet()) + { + endp.checkIdleTimestamp(idle_now); + } + } + public String toString() {return "Idle-"+super.toString();} + }); + + } + + // Reset busy select monitor counts + if (__MONITOR_PERIOD>0 && now>_monitorNext) + { + _busySelects=0; + _pausing=false; + _monitorNext=now+__MONITOR_PERIOD; + + } + } + catch (ClosedSelectorException e) + { + if (isRunning()) + LOG.warn(e); + else + LOG.ignore(e); + } + catch (CancelledKeyException e) + { + LOG.ignore(e); + } + finally + { + _selecting=null; + } + } + + /* ------------------------------------------------------------ */ + private void renewSelector() + { + try + { + synchronized (this) + { + Selector selector=_selector; + if (selector==null) + return; + final Selector new_selector = Selector.open(); + for (SelectionKey k: selector.keys()) + { + if (!k.isValid() || k.interestOps()==0) + continue; + + final SelectableChannel channel = k.channel(); + final Object attachment = k.attachment(); + + if (attachment==null) + addChange(channel); + else + addChange(channel,attachment); + } + _selector.close(); + _selector=new_selector; + } + } + catch(IOException e) + { + throw new RuntimeException("recreating selector",e); + } + } + + /* ------------------------------------------------------------ */ + public SelectorManager getManager() + { + return SelectorManager.this; + } + + /* ------------------------------------------------------------ */ + public long getNow() + { + return _timeout.getNow(); + } + + /* ------------------------------------------------------------ */ + /** + * @param task The task to timeout. If it implements Runnable, then + * expired will be called from a dispatched thread. + * + * @param timeoutMs + */ + public void scheduleTimeout(Timeout.Task task, long timeoutMs) + { + if (!(task instanceof Runnable)) + throw new IllegalArgumentException("!Runnable"); + _timeout.schedule(task, timeoutMs); + } + + /* ------------------------------------------------------------ */ + public void cancelTimeout(Timeout.Task task) + { + task.cancel(); + } + + /* ------------------------------------------------------------ */ + public void wakeup() + { + try + { + Selector selector = _selector; + if (selector!=null) + selector.wakeup(); + } + catch(Exception e) + { + addChange(new ChangeTask() + { + public void run() + { + renewSelector(); + } + }); + + renewSelector(); + } + } + + /* ------------------------------------------------------------ */ + private SelectChannelEndPoint createEndPoint(SocketChannel channel, SelectionKey sKey) throws IOException + { + SelectChannelEndPoint endp = newEndPoint(channel,this,sKey); + LOG.debug("created {}",endp); + endPointOpened(endp); + _endPoints.put(endp,this); + return endp; + } + + /* ------------------------------------------------------------ */ + public void destroyEndPoint(SelectChannelEndPoint endp) + { + LOG.debug("destroyEndPoint {}",endp); + _endPoints.remove(endp); + endPointClosed(endp); + } + + /* ------------------------------------------------------------ */ + Selector getSelector() + { + return _selector; + } + + /* ------------------------------------------------------------ */ + void stop() throws Exception + { + // Spin for a while waiting for selector to complete + // to avoid unneccessary closed channel exceptions + try + { + for (int i=0;i<100 && _selecting!=null;i++) + { + wakeup(); + Thread.sleep(10); + } + } + catch(Exception e) + { + LOG.ignore(e); + } + + // close endpoints and selector + synchronized (this) + { + Selector selector=_selector; + for (SelectionKey key:selector.keys()) + { + if (key==null) + continue; + Object att=key.attachment(); + if (att instanceof EndPoint) + { + EndPoint endpoint = (EndPoint)att; + try + { + endpoint.close(); + } + catch(IOException e) + { + LOG.ignore(e); + } + } + } + + + _timeout.cancelAll(); + try + { + selector=_selector; + if (selector != null) + selector.close(); + } + catch (IOException e) + { + LOG.ignore(e); + } + _selector=null; + } + } + + /* ------------------------------------------------------------ */ + public void dump(Appendable out, String indent) throws IOException + { + out.append(String.valueOf(this)).append(" id=").append(String.valueOf(_setID)).append("\n"); + + Thread selecting = _selecting; + + Object where = "not selecting"; + StackTraceElement[] trace =selecting==null?null:selecting.getStackTrace(); + if (trace!=null) + { + for (StackTraceElement t:trace) + if (t.getClassName().startsWith("org.eclipse.jetty.")) + { + where=t; + break; + } + } + + Selector selector=_selector; + if (selector!=null) + { + final ArrayList<Object> dump = new ArrayList<Object>(selector.keys().size()*2); + dump.add(where); + + final CountDownLatch latch = new CountDownLatch(1); + + addChange(new ChangeTask() + { + public void run() + { + dumpKeyState(dump); + latch.countDown(); + } + }); + + try + { + latch.await(5,TimeUnit.SECONDS); + } + catch(InterruptedException e) + { + LOG.ignore(e); + } + + AggregateLifeCycle.dump(out,indent,dump); + } + } + + /* ------------------------------------------------------------ */ + public void dumpKeyState(List<Object> dumpto) + { + Selector selector=_selector; + Set<SelectionKey> keys = selector.keys(); + dumpto.add(selector + " keys=" + keys.size()); + for (SelectionKey key: keys) + { + if (key.isValid()) + dumpto.add(key.attachment()+" iOps="+key.interestOps()+" rOps="+key.readyOps()); + else + dumpto.add(key.attachment()+" iOps=-1 rOps=-1"); + } + } + + /* ------------------------------------------------------------ */ + public String toString() + { + Selector selector=_selector; + return String.format(Locale.getDefault(), "%s keys=%d selected=%d", + super.toString(), + selector != null && selector.isOpen() ? selector.keys().size() : -1, + selector != null && selector.isOpen() ? selector.selectedKeys().size() : -1); + } + } + + /* ------------------------------------------------------------ */ + private static class ChannelAndAttachment + { + final SelectableChannel _channel; + final Object _attachment; + + public ChannelAndAttachment(SelectableChannel channel, Object attachment) + { + super(); + _channel = channel; + _attachment = attachment; + } + } + + /* ------------------------------------------------------------ */ + public boolean isDeferringInterestedOps0() + { + return _deferringInterestedOps0; + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private interface ChangeTask extends Runnable + {} +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SslConnection.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SslConnection.java new file mode 100755 index 00000000..711716ed --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/io/nio/SslConnection.java @@ -0,0 +1,775 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.io.nio; + +import android.annotation.SuppressLint; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.util.Locale; +import java.util.concurrent.atomic.AtomicBoolean; + +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLEngineResult; +import javax.net.ssl.SSLEngineResult.HandshakeStatus; +import javax.net.ssl.SSLException; +import javax.net.ssl.SSLSession; + +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/* ------------------------------------------------------------ */ +/** SSL Connection. + * An AysyncConnection that acts as an interceptor between and EndPoint and another + * Connection, that implements TLS encryption using an {@link SSLEngine}. + * <p> + * The connector uses an {@link AsyncEndPoint} (like {@link SelectChannelEndPoint}) as + * it's source/sink of encrypted data. It then provides {@link #getSslEndPoint()} to + * expose a source/sink of unencrypted data to another connection (eg HttpConnection). + */ +public class SslConnection extends AbstractConnection implements AsyncConnection +{ + private final Logger _logger = Log.getLogger("org.eclipse.jetty.io.nio.ssl"); + + private static final NIOBuffer __ZERO_BUFFER=new IndirectNIOBuffer(0); + + private static final ThreadLocal<SslBuffers> __buffers = new ThreadLocal<SslBuffers>(); + private final SSLEngine _engine; + private final SSLSession _session; + private AsyncConnection _connection; + private final SslEndPoint _sslEndPoint; + private int _allocations; + private SslBuffers _buffers; + private NIOBuffer _inbound; + private NIOBuffer _unwrapBuf; + private NIOBuffer _outbound; + private AsyncEndPoint _aEndp; + private boolean _allowRenegotiate=true; + private boolean _handshook; + private boolean _ishut; + private boolean _oshut; + private final AtomicBoolean _progressed = new AtomicBoolean(); + + /* ------------------------------------------------------------ */ + /* this is a half baked buffer pool + */ + private static class SslBuffers + { + final NIOBuffer _in; + final NIOBuffer _out; + final NIOBuffer _unwrap; + + SslBuffers(int packetSize, int appSize) + { + _in=new IndirectNIOBuffer(packetSize); + _out=new IndirectNIOBuffer(packetSize); + _unwrap=new IndirectNIOBuffer(appSize); + } + } + + /* ------------------------------------------------------------ */ + public SslConnection(SSLEngine engine,EndPoint endp) + { + this(engine,endp,System.currentTimeMillis()); + } + + /* ------------------------------------------------------------ */ + public SslConnection(SSLEngine engine,EndPoint endp, long timeStamp) + { + super(endp,timeStamp); + _engine=engine; + _session=_engine.getSession(); + _aEndp=(AsyncEndPoint)endp; + _sslEndPoint = newSslEndPoint(); + } + + /* ------------------------------------------------------------ */ + protected SslEndPoint newSslEndPoint() + { + return new SslEndPoint(); + } + + /* ------------------------------------------------------------ */ + private void allocateBuffers() + { + synchronized (this) + { + if (_allocations++==0) + { + if (_buffers==null) + { + _buffers=__buffers.get(); + if (_buffers==null) + _buffers=new SslBuffers(_session.getPacketBufferSize()*2,_session.getApplicationBufferSize()*2); + _inbound=_buffers._in; + _outbound=_buffers._out; + _unwrapBuf=_buffers._unwrap; + __buffers.set(null); + } + } + } + } + + /* ------------------------------------------------------------ */ + private void releaseBuffers() + { + synchronized (this) + { + if (--_allocations==0) + { + if (_buffers!=null && + _inbound.length()==0 && + _outbound.length()==0 && + _unwrapBuf.length()==0) + { + _inbound=null; + _outbound=null; + _unwrapBuf=null; + __buffers.set(_buffers); + _buffers=null; + } + } + } + } + + /* ------------------------------------------------------------ */ + public Connection handle() throws IOException + { + try + { + allocateBuffers(); + + boolean progress=true; + + while (progress) + { + progress=false; + + // If we are handshook let the delegate connection + if (_engine.getHandshakeStatus()!=HandshakeStatus.NOT_HANDSHAKING) + progress=process(null,null); + + // handle the delegate connection + AsyncConnection next = (AsyncConnection)_connection.handle(); + if (next!=_connection && next!=null) + { + _connection=next; + progress=true; + } + + _logger.debug("{} handle {} progress={}", _session, this, progress); + } + } + finally + { + releaseBuffers(); + + if (!_ishut && _sslEndPoint.isInputShutdown() && _sslEndPoint.isOpen()) + { + _ishut=true; + try + { + _connection.onInputShutdown(); + } + catch(Throwable x) + { + _logger.warn("onInputShutdown failed", x); + try{_sslEndPoint.close();} + catch(IOException e2){ + _logger.ignore(e2);} + } + } + } + + return this; + } + + /* ------------------------------------------------------------ */ + public boolean isSuspended() + { + return false; + } + + /* ------------------------------------------------------------ */ + public void onClose() + { + Connection connection = _sslEndPoint.getConnection(); + if (connection != null && connection != this) + connection.onClose(); + } + + /* ------------------------------------------------------------ */ + @Override + public void onIdleExpired(long idleForMs) + { + try + { + _logger.debug("onIdleExpired {}ms on {}",idleForMs,this); + if (_endp.isOutputShutdown()) + _sslEndPoint.close(); + else + _sslEndPoint.shutdownOutput(); + } + catch (IOException e) + { + _logger.warn(e); + super.onIdleExpired(idleForMs); + } + } + + /* ------------------------------------------------------------ */ + public void onInputShutdown() throws IOException + { + + } + + /* ------------------------------------------------------------ */ + private synchronized boolean process(Buffer toFill, Buffer toFlush) throws IOException + { + boolean some_progress=false; + try + { + // We need buffers to progress + allocateBuffers(); + + // if we don't have a buffer to put received data into + if (toFill==null) + { + // use the unwrapbuffer to hold received data. + _unwrapBuf.compact(); + toFill=_unwrapBuf; + } + // Else if the fill buffer is too small for the SSL session + else if (toFill.capacity()<_session.getApplicationBufferSize()) + { + // fill to the temporary unwrapBuffer + boolean progress=process(null,toFlush); + + // if we received any data, + if (_unwrapBuf!=null && _unwrapBuf.hasContent()) + { + // transfer from temp buffer to fill buffer + _unwrapBuf.skip(toFill.put(_unwrapBuf)); + return true; + } + else + // return progress from recursive call + return progress; + } + // Else if there is some temporary data + else if (_unwrapBuf!=null && _unwrapBuf.hasContent()) + { + // transfer from temp buffer to fill buffer + _unwrapBuf.skip(toFill.put(_unwrapBuf)); + return true; + } + + // If we are here, we have a buffer ready into which we can put some read data. + + // If we have no data to flush, flush the empty buffer + if (toFlush==null) + toFlush=__ZERO_BUFFER; + + // While we are making progress processing SSL engine + boolean progress=true; + while (progress) + { + progress=false; + + // Do any real IO + int filled=0,flushed=0; + try + { + // Read any available data + if (_inbound.space() > 0 && (filled = _endp.fill(_inbound)) > 0) + progress = true; + + // flush any output data + if (_outbound.hasContent() && (flushed=_endp.flush(_outbound)) > 0) + progress = true; + } + catch (IOException e) + { + _endp.close(); + throw e; + } + finally + { + _logger.debug("{} {} {} filled={}/{} flushed={}/{}",_session,this,_engine.getHandshakeStatus(),filled,_inbound.length(),flushed,_outbound.length()); + } + + // handle the current hand share status + switch(_engine.getHandshakeStatus()) + { + case FINISHED: + throw new IllegalStateException(); + + case NOT_HANDSHAKING: + { + // Try unwrapping some application data + if (toFill.space()>0 && _inbound.hasContent() && unwrap(toFill)) + progress=true; + + // Try wrapping some application data + if (toFlush.hasContent() && _outbound.space()>0 && wrap(toFlush)) + progress=true; + } + break; + + case NEED_TASK: + { + // A task needs to be run, so run it! + Runnable task; + while ((task=_engine.getDelegatedTask())!=null) + { + progress=true; + task.run(); + } + + } + break; + + case NEED_WRAP: + { + // The SSL needs to send some handshake data to the other side + if (_handshook && !_allowRenegotiate) + _endp.close(); + else if (wrap(toFlush)) + progress=true; + } + break; + + case NEED_UNWRAP: + { + // The SSL needs to receive some handshake data from the other side + if (_handshook && !_allowRenegotiate) + _endp.close(); + else if (!_inbound.hasContent()&&filled==-1) + { + // No more input coming + _endp.shutdownInput(); + } + else if (unwrap(toFill)) + progress=true; + } + break; + } + + // pass on ishut/oshut state + if (_endp.isOpen() && _endp.isInputShutdown() && !_inbound.hasContent()) + closeInbound(); + + if (_endp.isOpen() && _engine.isOutboundDone() && !_outbound.hasContent()) + _endp.shutdownOutput(); + + // remember if any progress has been made + some_progress|=progress; + } + + // If we are reading into the temp buffer and it has some content, then we should be dispatched. + if (toFill==_unwrapBuf && _unwrapBuf.hasContent() && !_connection.isSuspended()) + _aEndp.dispatch(); + } + finally + { + releaseBuffers(); + if (some_progress) + _progressed.set(true); + } + return some_progress; + } + + private void closeInbound() + { + try + { + _engine.closeInbound(); + } + catch (SSLException x) + { + _logger.debug(x); + } + } + + @SuppressLint("TrulyRandom") + private synchronized boolean wrap(final Buffer buffer) throws IOException + { + ByteBuffer bbuf=extractByteBuffer(buffer); + final SSLEngineResult result; + int encrypted_produced = 0; + int decrypted_consumed = 0; + synchronized(bbuf) + { + _outbound.compact(); + ByteBuffer out_buffer=_outbound.getByteBuffer(); + synchronized(out_buffer) + { + try + { + bbuf.position(buffer.getIndex()); + bbuf.limit(buffer.putIndex()); + int decrypted_position = bbuf.position(); + + out_buffer.position(_outbound.putIndex()); + out_buffer.limit(out_buffer.capacity()); + int encrypted_position = out_buffer.position(); + + result=_engine.wrap(bbuf,out_buffer); + if (_logger.isDebugEnabled()) + _logger.debug("{} wrap {} {} consumed={} produced={}", + _session, + result.getStatus(), + result.getHandshakeStatus(), + result.bytesConsumed(), + result.bytesProduced()); + + decrypted_consumed = bbuf.position() - decrypted_position; + buffer.skip(decrypted_consumed); + + encrypted_produced = out_buffer.position() - encrypted_position; + _outbound.setPutIndex(_outbound.putIndex() + encrypted_produced); + } + catch(SSLException e) + { + _logger.debug(String.valueOf(_endp), e); + _endp.close(); + throw e; + } + catch (Exception x) + { + throw new IOException(x); + } + finally + { + out_buffer.position(0); + out_buffer.limit(out_buffer.capacity()); + bbuf.position(0); + bbuf.limit(bbuf.capacity()); + } + } + } + + switch(result.getStatus()) + { + case BUFFER_UNDERFLOW: + throw new IllegalStateException(); + + case BUFFER_OVERFLOW: + break; + + case OK: + if (result.getHandshakeStatus()==HandshakeStatus.FINISHED) + _handshook=true; + break; + + case CLOSED: + _logger.debug("wrap CLOSE {} {}",this,result); + if (result.getHandshakeStatus()==HandshakeStatus.FINISHED) + _endp.close(); + break; + + default: + _logger.debug("{} wrap default {}",_session,result); + throw new IOException(result.toString()); + } + + return decrypted_consumed > 0 || encrypted_produced > 0; + } + + private synchronized boolean unwrap(final Buffer buffer) throws IOException + { + if (!_inbound.hasContent()) + return false; + + ByteBuffer bbuf=extractByteBuffer(buffer); + final SSLEngineResult result; + int encrypted_consumed = 0; + int decrypted_produced = 0; + synchronized(bbuf) + { + ByteBuffer in_buffer=_inbound.getByteBuffer(); + synchronized(in_buffer) + { + try + { + bbuf.position(buffer.putIndex()); + bbuf.limit(buffer.capacity()); + int decrypted_position = bbuf.position(); + + in_buffer.position(_inbound.getIndex()); + in_buffer.limit(_inbound.putIndex()); + int encrypted_position = in_buffer.position(); + + result=_engine.unwrap(in_buffer,bbuf); + if (_logger.isDebugEnabled()) + _logger.debug("{} unwrap {} {} consumed={} produced={}", + _session, + result.getStatus(), + result.getHandshakeStatus(), + result.bytesConsumed(), + result.bytesProduced()); + + encrypted_consumed = in_buffer.position() - encrypted_position; + _inbound.skip(encrypted_consumed); + _inbound.compact(); + + decrypted_produced = bbuf.position() - decrypted_position; + buffer.setPutIndex(buffer.putIndex() + decrypted_produced); + } + catch(SSLException e) + { + _logger.debug(String.valueOf(_endp), e); + _endp.close(); + throw e; + } + catch (Exception x) + { + throw new IOException(x); + } + finally + { + in_buffer.position(0); + in_buffer.limit(in_buffer.capacity()); + bbuf.position(0); + bbuf.limit(bbuf.capacity()); + } + } + } + + switch(result.getStatus()) + { + case BUFFER_UNDERFLOW: + if (_endp.isInputShutdown()) + _inbound.clear(); + break; + + case BUFFER_OVERFLOW: + if (_logger.isDebugEnabled()) _logger.debug("{} unwrap {} {}->{}",_session,result.getStatus(),_inbound.toDetailString(),buffer.toDetailString()); + break; + + case OK: + if (result.getHandshakeStatus()==HandshakeStatus.FINISHED) + _handshook=true; + break; + + case CLOSED: + _logger.debug("unwrap CLOSE {} {}",this,result); + if (result.getHandshakeStatus()==HandshakeStatus.FINISHED) + _endp.close(); + break; + + default: + _logger.debug("{} wrap default {}",_session,result); + throw new IOException(result.toString()); + } + + return encrypted_consumed > 0 || decrypted_produced > 0; + } + + /* ------------------------------------------------------------ */ + private ByteBuffer extractByteBuffer(Buffer buffer) + { + if (buffer.buffer() instanceof NIOBuffer) + return ((NIOBuffer)buffer.buffer()).getByteBuffer(); + return ByteBuffer.wrap(buffer.array()); + } + + /* ------------------------------------------------------------ */ + public AsyncEndPoint getSslEndPoint() + { + return _sslEndPoint; + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return String.format("%s %s", super.toString(), _sslEndPoint); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public class SslEndPoint implements AsyncEndPoint + { + public void shutdownOutput() throws IOException + { + synchronized (SslConnection.this) + { + try + { + _logger.debug("{} ssl endp.oshut {}",_session,this); + _oshut=true; + _engine.closeOutbound(); + } + catch (Exception x) + { + throw new IOException(x); + } + } + flush(); + } + + public boolean isOutputShutdown() + { + synchronized (SslConnection.this) + { + return _oshut||!isOpen()||_engine.isOutboundDone(); + } + } + + public void shutdownInput() throws IOException + { + _logger.debug("{} ssl endp.ishut!",_session); + // We do not do a closeInput here, as SSL does not support half close. + // isInputShutdown works it out itself from buffer state and underlying endpoint state. + } + + public boolean isInputShutdown() + { + synchronized (SslConnection.this) + { + return _endp.isInputShutdown() && + !(_unwrapBuf!=null&&_unwrapBuf.hasContent()) && + !(_inbound!=null&&_inbound.hasContent()); + } + } + + public void close() throws IOException + { + _logger.debug("{} ssl endp.close",_session); + _endp.close(); + } + + public int fill(Buffer buffer) throws IOException + { + int size=buffer.length(); + process(buffer, null); + + int filled=buffer.length()-size; + + if (filled==0 && isInputShutdown()) + return -1; + return filled; + } + + public int flush(Buffer buffer) throws IOException + { + int size = buffer.length(); + process(null, buffer); + return size-buffer.length(); + } + + public boolean blockWritable(long millisecs) throws IOException + { + return _endp.blockWritable(millisecs); + } + + public boolean isOpen() + { + return _endp.isOpen(); + } + + public void flush() throws IOException + { + process(null, null); + } + + public void dispatch() + { + _aEndp.dispatch(); + } + + public void scheduleWrite() + { + _aEndp.scheduleWrite(); + } + + public boolean hasProgressed() + { + return _progressed.getAndSet(false); + } + + public String getLocalAddr() + { + return _aEndp.getLocalAddr(); + } + + public int getLocalPort() + { + return _aEndp.getLocalPort(); + } + + public String getRemoteAddr() + { + return _aEndp.getRemoteAddr(); + } + + public int getRemotePort() + { + return _aEndp.getRemotePort(); + } + + public boolean isBlocking() + { + return false; + } + + public int getMaxIdleTime() + { + return _aEndp.getMaxIdleTime(); + } + + public void setMaxIdleTime(int timeMs) throws IOException + { + _aEndp.setMaxIdleTime(timeMs); + } + + public Connection getConnection() + { + return _connection; + } + + public void setConnection(Connection connection) + { + _connection=(AsyncConnection)connection; + } + + public String toString() + { + // Do NOT use synchronized (SslConnection.this) + // because it's very easy to deadlock when debugging is enabled. + // We do a best effort to print the right toString() and that's it. + Buffer inbound = _inbound; + Buffer outbound = _outbound; + Buffer unwrap = _unwrapBuf; + int i = inbound == null? -1 : inbound.length(); + int o = outbound == null ? -1 : outbound.length(); + int u = unwrap == null ? -1 : unwrap.length(); + return String.format(Locale.getDefault(), "SSL %s i/o/u=%d/%d/%d ishut=%b oshut=%b {%s}", + _engine.getHandshakeStatus(), + i, o, u, + _ishut, _oshut, + _connection); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/BlockingArrayQueue.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/BlockingArrayQueue.java new file mode 100644 index 00000000..110192b1 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/BlockingArrayQueue.java @@ -0,0 +1,482 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.util.AbstractList; +import java.util.Collection; +import java.util.NoSuchElementException; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.Condition; +import java.util.concurrent.locks.ReentrantLock; + + +/* ------------------------------------------------------------ */ +/** Queue backed by a circular array. + * + * This queue is uses a variant of the two lock queue algorithm to + * provide an efficient queue or list backed by a growable circular + * array. This queue also has a partial implementation of + * {@link java.util.concurrent.BlockingQueue}, specifically the {@link #take()} and + * {@link #poll(long, TimeUnit)} methods. + * Unlike {@link java.util.concurrent.ArrayBlockingQueue}, this class is + * able to grow and provides a blocking put call. + * <p> + * The queue has both a capacity (the size of the array currently allocated) + * and a limit (the maximum size that may be allocated), which defaults to + * {@link Integer#MAX_VALUE}. + * + * @param <E> The element type + */ +public class BlockingArrayQueue<E> extends AbstractList<E> implements BlockingQueue<E> +{ + public final int DEFAULT_CAPACITY=128; + public final int DEFAULT_GROWTH=64; + private final int _limit; + private final AtomicInteger _size=new AtomicInteger(); + private final int _growCapacity; + + private volatile int _capacity; + private Object[] _elements; + + private final ReentrantLock _headLock = new ReentrantLock(); + private final Condition _notEmpty = _headLock.newCondition(); + private int _head; + + private final ReentrantLock _tailLock = new ReentrantLock(); + private int _tail; + + /* ------------------------------------------------------------ */ + /** Create a growing partially blocking Queue. + * @param capacity Initial capacity + * @param growBy Incremental capacity. + */ + public BlockingArrayQueue(int capacity,int growBy) + { + _elements=new Object[capacity]; + _capacity=_elements.length; + _growCapacity=growBy; + _limit=Integer.MAX_VALUE; + } + + /* ------------------------------------------------------------ */ + public int getCapacity() + { + return _capacity; + } + + /* ------------------------------------------------------------ */ + @Override + public boolean add(E e) + { + return offer(e); + } + + /* ------------------------------------------------------------ */ + public E element() + { + E e = peek(); + if (e==null) + throw new NoSuchElementException(); + return e; + } + + /* ------------------------------------------------------------ */ + @SuppressWarnings("unchecked") + public E peek() + { + if (_size.get() == 0) + return null; + + E e = null; + _headLock.lock(); // Size cannot shrink + try + { + if (_size.get() > 0) + e = (E)_elements[_head]; + } + finally + { + _headLock.unlock(); + } + + return e; + } + + /* ------------------------------------------------------------ */ + public boolean offer(E e) + { + if (e == null) + throw new NullPointerException(); + + boolean not_empty=false; + _tailLock.lock(); // size cannot grow... only shrink + try + { + if (_size.get() >= _limit) + return false; + + // should we expand array? + if (_size.get()==_capacity) + { + _headLock.lock(); // Need to grow array + try + { + if (!grow()) + return false; + } + finally + { + _headLock.unlock(); + } + } + + // add the element + _elements[_tail]=e; + _tail=(_tail+1)%_capacity; + + not_empty=0==_size.getAndIncrement(); + + } + finally + { + _tailLock.unlock(); + } + + if (not_empty) + { + _headLock.lock(); + try + { + _notEmpty.signal(); + } + finally + { + _headLock.unlock(); + } + } + + return true; + } + + /* ------------------------------------------------------------ */ + @SuppressWarnings("unchecked") + public E poll() + { + if (_size.get() == 0) + return null; + + E e = null; + _headLock.lock(); // Size cannot shrink + try + { + if (_size.get() > 0) + { + final int head=_head; + e = (E)_elements[head]; + _elements[head]=null; + _head=(head+1)%_capacity; + + if (_size.decrementAndGet()>0) + _notEmpty.signal(); + } + } + finally + { + _headLock.unlock(); + } + + return e; + } + + /* ------------------------------------------------------------ */ + /** + * Retrieves and removes the head of this queue, waiting + * if no elements are present on this queue. + * @return the head of this queue + * @throws InterruptedException if interrupted while waiting. + */ + @SuppressWarnings("unchecked") + public E take() throws InterruptedException + { + E e = null; + _headLock.lockInterruptibly(); // Size cannot shrink + try + { + try + { + while (_size.get() == 0) + { + _notEmpty.await(); + } + } + catch (InterruptedException ie) + { + _notEmpty.signal(); + throw ie; + } + + final int head=_head; + e = (E)_elements[head]; + _elements[head]=null; + _head=(head+1)%_capacity; + + if (_size.decrementAndGet()>0) + _notEmpty.signal(); + } + finally + { + _headLock.unlock(); + } + + return e; + } + + /* ------------------------------------------------------------ */ + /** + * Retrieves and removes the head of this queue, waiting + * if necessary up to the specified wait time if no elements are + * present on this queue. + * @param time how long to wait before giving up, in units of + * <tt>unit</tt> + * @param unit a <tt>TimeUnit</tt> determining how to interpret the + * <tt>timeout</tt> parameter + * @return the head of this queue, or <tt>null</tt> if the + * specified waiting time elapses before an element is present. + * @throws InterruptedException if interrupted while waiting. + */ + @SuppressWarnings("unchecked") + public E poll(long time, TimeUnit unit) throws InterruptedException + { + + E e = null; + + long nanos = unit.toNanos(time); + + _headLock.lockInterruptibly(); // Size cannot shrink + try + { + try + { + while (_size.get() == 0) + { + if (nanos<=0) + return null; + nanos = _notEmpty.awaitNanos(nanos); + } + } + catch (InterruptedException ie) + { + _notEmpty.signal(); + throw ie; + } + + e = (E)_elements[_head]; + _elements[_head]=null; + _head=(_head+1)%_capacity; + + if (_size.decrementAndGet()>0) + _notEmpty.signal(); + } + finally + { + _headLock.unlock(); + } + + return e; + } + + /* ------------------------------------------------------------ */ + public E remove() + { + E e=poll(); + if (e==null) + throw new NoSuchElementException(); + return e; + } + + /* ------------------------------------------------------------ */ + @Override + public void clear() + { + _tailLock.lock(); + try + { + _headLock.lock(); + try + { + _head=0; + _tail=0; + _size.set(0); + } + finally + { + _headLock.unlock(); + } + } + finally + { + _tailLock.unlock(); + } + } + + /* ------------------------------------------------------------ */ + @Override + public boolean isEmpty() + { + return _size.get()==0; + } + + /* ------------------------------------------------------------ */ + @Override + public int size() + { + return _size.get(); + } + + /* ------------------------------------------------------------ */ + @SuppressWarnings("unchecked") + @Override + public E get(int index) + { + _tailLock.lock(); + try + { + _headLock.lock(); + try + { + if (index<0 || index>=_size.get()) + throw new IndexOutOfBoundsException("!("+0+"<"+index+"<="+_size+")"); + int i = _head+index; + if (i>=_capacity) + i-=_capacity; + return (E)_elements[i]; + } + finally + { + _headLock.unlock(); + } + } + finally + { + _tailLock.unlock(); + } + } + + /* ------------------------------------------------------------ */ + private boolean grow() + { + if (_growCapacity<=0) + return false; + + _tailLock.lock(); + try + { + _headLock.lock(); + try + { + final int head=_head; + final int tail=_tail; + final int new_tail; + + Object[] elements=new Object[_capacity+_growCapacity]; + + if (head<tail) + { + new_tail=tail-head; + System.arraycopy(_elements,head,elements,0,new_tail); + } + else if (head>tail || _size.get()>0) + { + new_tail=_capacity+tail-head; + int cut=_capacity-head; + System.arraycopy(_elements,head,elements,0,cut); + System.arraycopy(_elements,0,elements,cut,tail); + } + else + { + new_tail=0; + } + + _elements=elements; + _capacity=_elements.length; + _head=0; + _tail=new_tail; + return true; + } + finally + { + _headLock.unlock(); + } + } + finally + { + _tailLock.unlock(); + } + + } + + /* ------------------------------------------------------------ */ + public int drainTo(Collection<? super E> c) + { + throw new UnsupportedOperationException(); + } + + /* ------------------------------------------------------------ */ + public int drainTo(Collection<? super E> c, int maxElements) + { + throw new UnsupportedOperationException(); + } + + /* ------------------------------------------------------------ */ + public boolean offer(E o, long timeout, TimeUnit unit) throws InterruptedException + { + throw new UnsupportedOperationException(); + } + + /* ------------------------------------------------------------ */ + public void put(E o) throws InterruptedException + { + if (!add(o)) + throw new IllegalStateException("full"); + } + + /* ------------------------------------------------------------ */ + public int remainingCapacity() + { + _tailLock.lock(); + try + { + _headLock.lock(); + try + { + return getCapacity()-size(); + } + finally + { + _headLock.unlock(); + } + } + finally + { + _tailLock.unlock(); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ConcurrentHashSet.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ConcurrentHashSet.java new file mode 100644 index 00000000..9f88ebc9 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ConcurrentHashSet.java @@ -0,0 +1,126 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.util.AbstractSet; +import java.util.Collection; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +public class ConcurrentHashSet<E> extends AbstractSet<E> implements Set<E> +{ + private final Map<E, Boolean> _map = new ConcurrentHashMap<E, Boolean>(); + private transient Set<E> _keys = _map.keySet(); + + public ConcurrentHashSet() + { + } + + @Override + public boolean add(E e) + { + return _map.put(e,Boolean.TRUE) == null; + } + + @Override + public void clear() + { + _map.clear(); + } + + @Override + public boolean contains(Object o) + { + return _map.containsKey(o); + } + + @Override + public boolean containsAll(Collection<?> c) + { + return _keys.containsAll(c); + } + + @Override + public boolean equals(Object o) + { + return o == this || _keys.equals(o); + } + + @Override + public int hashCode() + { + return _keys.hashCode(); + } + + @Override + public boolean isEmpty() + { + return _map.isEmpty(); + } + + @Override + public Iterator<E> iterator() + { + return _keys.iterator(); + } + + @Override + public boolean remove(Object o) + { + return _map.remove(o) != null; + } + + @Override + public boolean removeAll(Collection<?> c) + { + return _keys.removeAll(c); + } + + @Override + public boolean retainAll(Collection<?> c) + { + return _keys.retainAll(c); + } + + @Override + public int size() + { + return _map.size(); + } + + @Override + public Object[] toArray() + { + return _keys.toArray(); + } + + @Override + public <T> T[] toArray(T[] a) + { + return _keys.toArray(a); + } + + @Override + public String toString() + { + return _keys.toString(); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/IO.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/IO.java new file mode 100644 index 00000000..ea1b0887 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/IO.java @@ -0,0 +1,122 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/* ======================================================================== */ +/** IO Utilities. + * Provides stream handling utilities in + * singleton Threadpool implementation accessed by static members. + */ +public class IO +{ + private static final Logger LOG = Log.getLogger(IO.class); + + /* ------------------------------------------------------------------- */ + public static int bufferSize = 64*1024; + + /* ------------------------------------------------------------------- */ + /** Copy Stream in to Stream out until EOF or exception. + */ + public static void copy(InputStream in, OutputStream out) + throws IOException + { + copy(in,out,-1); + } + + /* ------------------------------------------------------------------- */ + /** Copy Stream in to Stream for byteCount bytes or until EOF or exception. + */ + public static void copy(InputStream in, + OutputStream out, + long byteCount) + throws IOException + { + byte buffer[] = new byte[bufferSize]; + int len=bufferSize; + + if (byteCount>=0) + { + while (byteCount>0) + { + int max = byteCount<bufferSize?(int)byteCount:bufferSize; + len=in.read(buffer,0,max); + + if (len==-1) + break; + + byteCount -= len; + out.write(buffer,0,len); + } + } + else + { + while (true) + { + len=in.read(buffer,0,bufferSize); + if (len<0 ) + break; + out.write(buffer,0,len); + } + } + } + + /* ------------------------------------------------------------ */ + /** + * closes any {@link Closeable} + * + * @param c the closeable to close + */ + public static void close(Closeable c) + { + try + { + if (c != null) + c.close(); + } + catch (IOException e) + { + LOG.ignore(e); + } + } + + /** + * closes an input stream, and logs exceptions + * + * @param is the input stream to close + */ + public static void close(InputStream is) + { + try + { + if (is != null) + is.close(); + } + catch (IOException e) + { + LOG.ignore(e); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/QuotedStringTokenizer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/QuotedStringTokenizer.java new file mode 100644 index 00000000..2c6e2460 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/QuotedStringTokenizer.java @@ -0,0 +1,312 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.io.IOException; +import java.util.Arrays; +import java.util.NoSuchElementException; +import java.util.StringTokenizer; + +/* ------------------------------------------------------------ */ +/** StringTokenizer with Quoting support. + * + * This class is a copy of the java.util.StringTokenizer API and + * the behaviour is the same, except that single and double quoted + * string values are recognised. + * Delimiters within quotes are not considered delimiters. + * Quotes can be escaped with '\'. + * + * @see java.util.StringTokenizer + * + */ +public class QuotedStringTokenizer + extends StringTokenizer +{ + private final static String __delim="\t\n\r"; + private String _string; + private String _delim = __delim; + private boolean _returnQuotes=false; + private boolean _returnDelimiters=false; + private StringBuffer _token; + private boolean _hasToken=false; + private int _i=0; + private boolean _double=true; + private boolean _single=true; + + /* ------------------------------------------------------------ */ + public QuotedStringTokenizer(String str, + String delim, + boolean returnDelimiters, + boolean returnQuotes) + { + super(""); + _string=str; + if (delim!=null) + _delim=delim; + _returnDelimiters=returnDelimiters; + _returnQuotes=returnQuotes; + + if (_delim.indexOf('\'')>=0 || + _delim.indexOf('"')>=0) + throw new Error("Can't use quotes as delimiters: "+_delim); + + _token=new StringBuffer(_string.length()>1024?512:_string.length()/2); + } + + /* ------------------------------------------------------------ */ + @Override + public boolean hasMoreTokens() + { + // Already found a token + if (_hasToken) + return true; + + int state=0; + boolean escape=false; + while (_i<_string.length()) + { + char c=_string.charAt(_i++); + + switch (state) + { + case 0: // Start + if(_delim.indexOf(c)>=0) + { + if (_returnDelimiters) + { + _token.append(c); + return _hasToken=true; + } + } + else if (c=='\'' && _single) + { + if (_returnQuotes) + _token.append(c); + state=2; + } + else if (c=='\"' && _double) + { + if (_returnQuotes) + _token.append(c); + state=3; + } + else + { + _token.append(c); + _hasToken=true; + state=1; + } + break; + + case 1: // Token + _hasToken=true; + if(_delim.indexOf(c)>=0) + { + if (_returnDelimiters) + _i--; + return _hasToken; + } + else if (c=='\'' && _single) + { + if (_returnQuotes) + _token.append(c); + state=2; + } + else if (c=='\"' && _double) + { + if (_returnQuotes) + _token.append(c); + state=3; + } + else + { + _token.append(c); + } + break; + + case 2: // Single Quote + _hasToken=true; + if (escape) + { + escape=false; + _token.append(c); + } + else if (c=='\'') + { + if (_returnQuotes) + _token.append(c); + state=1; + } + else if (c=='\\') + { + if (_returnQuotes) + _token.append(c); + escape=true; + } + else + { + _token.append(c); + } + break; + + case 3: // Double Quote + _hasToken=true; + if (escape) + { + escape=false; + _token.append(c); + } + else if (c=='\"') + { + if (_returnQuotes) + _token.append(c); + state=1; + } + else if (c=='\\') + { + if (_returnQuotes) + _token.append(c); + escape=true; + } + else + { + _token.append(c); + } + break; + } + } + + return _hasToken; + } + + /* ------------------------------------------------------------ */ + @Override + public String nextToken() + throws NoSuchElementException + { + if (!hasMoreTokens() || _token==null) + throw new NoSuchElementException(); + String t=_token.toString(); + _token.setLength(0); + _hasToken=false; + return t; + } + + /* ------------------------------------------------------------ */ + @Override + public boolean hasMoreElements() + { + return hasMoreTokens(); + } + + /* ------------------------------------------------------------ */ + @Override + public Object nextElement() + throws NoSuchElementException + { + return nextToken(); + } + + /* ------------------------------------------------------------ */ + /** Quote a string. + * The string is quoted only if quoting is required due to + * embedded delimiters, quote characters or the + * empty string. + * @param s The string to quote. + * @param delim the delimiter to use to quote the string + * @return quoted string + */ + public static String quoteIfNeeded(String s, String delim) + { + if (s==null) + return null; + if (s.length()==0) + return "\"\""; + + + for (int i=0;i<s.length();i++) + { + char c = s.charAt(i); + if (c=='\\' || c=='"' || c=='\'' || Character.isWhitespace(c) || delim.indexOf(c)>=0) + { + StringBuffer b=new StringBuffer(s.length()+8); + quote(b,s); + return b.toString(); + } + } + + return s; + } + + private static final char[] escapes = new char[32]; + static + { + Arrays.fill(escapes, (char)0xFFFF); + escapes['\b'] = 'b'; + escapes['\t'] = 't'; + escapes['\n'] = 'n'; + escapes['\f'] = 'f'; + escapes['\r'] = 'r'; + } + + /* ------------------------------------------------------------ */ + /** Quote a string into an Appendable. + * The characters ", \, \n, \r, \t, \f and \b are escaped + * @param buffer The Appendable + * @param input The String to quote. + */ + public static void quote(Appendable buffer, String input) + { + try + { + buffer.append('"'); + for (int i = 0; i < input.length(); ++i) + { + char c = input.charAt(i); + if (c >= 32) + { + if (c == '"' || c == '\\') + buffer.append('\\'); + buffer.append(c); + } + else + { + char escape = escapes[c]; + if (escape == 0xFFFF) + { + // Unicode escape + buffer.append('\\').append('u').append('0').append('0'); + if (c < 0x10) + buffer.append('0'); + buffer.append(Integer.toString(c, 16)); + } + else + { + buffer.append('\\').append(escape); + } + } + } + buffer.append('"'); + } + catch (IOException x) + { + throw new RuntimeException(x); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/StringMap.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/StringMap.java new file mode 100644 index 00000000..addba083 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/StringMap.java @@ -0,0 +1,603 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.io.Externalizable; +import java.util.AbstractMap; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +/* ------------------------------------------------------------ */ +/** Map implementation Optimized for Strings keys.. + * This String Map has been optimized for mapping small sets of + * Strings where the most frequently accessed Strings have been put to + * the map first. + * + * It also has the benefit that it can look up entries by substring or + * sections of char and byte arrays. This can prevent many String + * objects from being created just to look up in the map. + * + * This map is NOT synchronized. + */ +public class StringMap extends AbstractMap<Object, Object> implements Externalizable +{ + public static final boolean CASE_INSENSTIVE=true; + protected static final int __HASH_WIDTH=17; + + /* ------------------------------------------------------------ */ + protected int _width=__HASH_WIDTH; + protected Node _root=new Node(); + protected boolean _ignoreCase=false; + protected NullEntry _nullEntry=null; + protected Object _nullValue=null; + protected HashSet<Entry<Object, Object>> _entrySet=new HashSet<Entry<Object, Object>>(3); + protected Set<Entry<Object, Object>> _umEntrySet=Collections.unmodifiableSet(_entrySet); + + /* ------------------------------------------------------------ */ + /** Constructor. + */ + public StringMap() + {} + + /* ------------------------------------------------------------ */ + /** Constructor. + * @param ignoreCase + */ + public StringMap(boolean ignoreCase) + { + this(); + _ignoreCase=ignoreCase; + } + + /* ------------------------------------------------------------ */ + /** Set the ignoreCase attribute. + * @param ic If true, the map is case insensitive for keys. + */ + public void setIgnoreCase(boolean ic) + { + if (_root._children!=null) + throw new IllegalStateException("Must be set before first put"); + _ignoreCase=ic; + } + + /* ------------------------------------------------------------ */ + @Override + public Object put(Object key, Object value) + { + if (key==null) + return put(null,value); + return put(key.toString(),value); + } + + /* ------------------------------------------------------------ */ + public Object put(String key, Object value) + { + if (key==null) + { + Object oldValue=_nullValue; + _nullValue=value; + if (_nullEntry==null) + { + _nullEntry=new NullEntry(); + _entrySet.add(_nullEntry); + } + return oldValue; + } + + Node node = _root; + int ni=-1; + Node prev = null; + Node parent = null; + + // look for best match + charLoop: + for (int i=0;i<key.length();i++) + { + char c=key.charAt(i); + + // Advance node + if (ni==-1) + { + parent=node; + prev=null; + ni=0; + node=(node._children==null)?null:node._children[c%_width]; + } + + // Loop through a node chain at the same level + while (node!=null) + { + // If it is a matching node, goto next char + if (node._char[ni]==c || _ignoreCase&&node._ochar[ni]==c) + { + prev=null; + ni++; + if (ni==node._char.length) + ni=-1; + continue charLoop; + } + + // no char match + // if the first char, + if (ni==0) + { + // look along the chain for a char match + prev=node; + node=node._next; + } + else + { + // Split the current node! + node.split(this,ni); + i--; + ni=-1; + continue charLoop; + } + } + + // We have run out of nodes, so as this is a put, make one + node = new Node(_ignoreCase,key,i); + + if (prev!=null) // add to end of chain + prev._next=node; + else if (parent!=null) // add new child + { + if (parent._children==null) + parent._children=new Node[_width]; + parent._children[c%_width]=node; + int oi=node._ochar[0]%_width; + if (node._ochar!=null && node._char[0]%_width!=oi) + { + if (parent._children[oi]==null) + parent._children[oi]=node; + else + { + Node n=parent._children[oi]; + while(n._next!=null) + n=n._next; + n._next=node; + } + } + } + else // this is the root. + _root=node; + break; + } + + // Do we have a node + if (node!=null) + { + // Split it if we are in the middle + if(ni>0) + node.split(this,ni); + + Object old = node._value; + node._key=key; + node._value=value; + _entrySet.add(node); + return old; + } + return null; + } + + /* ------------------------------------------------------------ */ + @Override + public Object get(Object key) + { + if (key==null) + return _nullValue; + if (key instanceof String) + return get((String)key); + return get(key.toString()); + } + + /* ------------------------------------------------------------ */ + public Object get(String key) + { + if (key==null) + return _nullValue; + + Map.Entry<Object, Object> entry = getEntry(key,0,key.length()); + if (entry==null) + return null; + return entry.getValue(); + } + + /* ------------------------------------------------------------ */ + /** Get a map entry by substring key. + * @param key String containing the key + * @param offset Offset of the key within the String. + * @param length The length of the key + * @return The Map.Entry for the key or null if the key is not in + * the map. + */ + public Map.Entry<Object, Object> getEntry(String key,int offset, int length) + { + if (key==null) + return _nullEntry; + + Node node = _root; + int ni=-1; + + // look for best match + charLoop: + for (int i=0;i<length;i++) + { + char c=key.charAt(offset+i); + + // Advance node + if (ni==-1) + { + ni=0; + node=(node._children==null)?null:node._children[c%_width]; + } + + // Look through the node chain + while (node!=null) + { + // If it is a matching node, goto next char + if (node._char[ni]==c || _ignoreCase&&node._ochar[ni]==c) + { + ni++; + if (ni==node._char.length) + ni=-1; + continue charLoop; + } + + // No char match, so if mid node then no match at all. + if (ni>0) return null; + + // try next in chain + node=node._next; + } + return null; + } + + if (ni>0) return null; + if (node!=null && node._key==null) + return null; + return node; + } + + /* ------------------------------------------------------------ */ + /** Get a map entry by byte array key, using as much of the passed key as needed for a match. + * A simple 8859-1 byte to char mapping is assumed. + * @param key char array containing the key + * @param offset Offset of the key within the array. + * @param maxLength The length of the key + * @return The Map.Entry for the key or null if the key is not in + * the map. + */ + public Map.Entry<Object, Object> getBestEntry(byte[] key,int offset, int maxLength) + { + if (key==null) + return _nullEntry; + + Node node = _root; + int ni=-1; + + // look for best match + charLoop: + for (int i=0;i<maxLength;i++) + { + char c=(char)key[offset+i]; + + // Advance node + if (ni==-1) + { + ni=0; + + Node child = (node._children==null)?null:node._children[c%_width]; + + if (child==null && i>0) + return node; // This is the best match + node=child; + } + + // While we have a node to try + while (node!=null) + { + // If it is a matching node, goto next char + if (node._char[ni]==c || _ignoreCase&&node._ochar[ni]==c) + { + ni++; + if (ni==node._char.length) + ni=-1; + continue charLoop; + } + + // No char match, so if mid node then no match at all. + if (ni>0) return null; + + // try next in chain + node=node._next; + } + return null; + } + + if (ni>0) return null; + if (node!=null && node._key==null) + return null; + return node; + } + + + /* ------------------------------------------------------------ */ + @Override + public Object remove(Object key) + { + if (key==null) + return remove(null); + return remove(key.toString()); + } + + /* ------------------------------------------------------------ */ + public Object remove(String key) + { + if (key==null) + { + Object oldValue=_nullValue; + if (_nullEntry!=null) + { + _entrySet.remove(_nullEntry); + _nullEntry=null; + _nullValue=null; + } + return oldValue; + } + + Node node = _root; + int ni=-1; + + // look for best match + charLoop: + for (int i=0;i<key.length();i++) + { + char c=key.charAt(i); + + // Advance node + if (ni==-1) + { + ni=0; + node=(node._children==null)?null:node._children[c%_width]; + } + + // While we have a node to try + while (node!=null) + { + // If it is a matching node, goto next char + if (node._char[ni]==c || _ignoreCase&&node._ochar[ni]==c) + { + ni++; + if (ni==node._char.length) + ni=-1; + continue charLoop; + } + + // No char match, so if mid node then no match at all. + if (ni>0) return null; + + // try next in chain + node=node._next; + } + return null; + } + + if (ni>0) return null; + if (node!=null && node._key==null) + return null; + + Object old = node._value; + _entrySet.remove(node); + node._value=null; + node._key=null; + + return old; + } + + /* ------------------------------------------------------------ */ + @Override + public Set<Map.Entry<Object, Object>> entrySet() + { + return _umEntrySet; + } + + /* ------------------------------------------------------------ */ + @Override + public int size() + { + return _entrySet.size(); + } + + /* ------------------------------------------------------------ */ + @Override + public boolean isEmpty() + { + return _entrySet.isEmpty(); + } + + /* ------------------------------------------------------------ */ + @Override + public boolean containsKey(Object key) + { + if (key==null) + return _nullEntry!=null; + return + getEntry(key.toString(),0,key==null?0:key.toString().length())!=null; + } + + /* ------------------------------------------------------------ */ + @Override + public void clear() + { + _root=new Node(); + _nullEntry=null; + _nullValue=null; + _entrySet.clear(); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private static class Node implements Map.Entry<Object, Object> + { + char[] _char; + char[] _ochar; + Node _next; + Node[] _children; + String _key; + Object _value; + + Node(){} + + Node(boolean ignoreCase,String s, int offset) + { + int l=s.length()-offset; + _char=new char[l]; + _ochar=new char[l]; + for (int i=0;i<l;i++) + { + char c=s.charAt(offset+i); + _char[i]=c; + if (ignoreCase) + { + char o=c; + if (Character.isUpperCase(c)) + o=Character.toLowerCase(c); + else if (Character.isLowerCase(c)) + o=Character.toUpperCase(c); + _ochar[i]=o; + } + } + } + + Node split(StringMap map,int offset) + { + Node split = new Node(); + int sl=_char.length-offset; + + char[] tmp=this._char; + this._char=new char[offset]; + split._char = new char[sl]; + System.arraycopy(tmp,0,this._char,0,offset); + System.arraycopy(tmp,offset,split._char,0,sl); + + if (this._ochar!=null) + { + tmp=this._ochar; + this._ochar=new char[offset]; + split._ochar = new char[sl]; + System.arraycopy(tmp,0,this._ochar,0,offset); + System.arraycopy(tmp,offset,split._ochar,0,sl); + } + + split._key=this._key; + split._value=this._value; + this._key=null; + this._value=null; + if (map._entrySet.remove(this)) + map._entrySet.add(split); + + split._children=this._children; + this._children=new Node[map._width]; + this._children[split._char[0]%map._width]=split; + if (split._ochar!=null && this._children[split._ochar[0]%map._width]!=split) + this._children[split._ochar[0]%map._width]=split; + + return split; + } + + public Object getKey(){return _key;} + public Object getValue(){return _value;} + public Object setValue(Object o){Object old=_value;_value=o;return old;} + @Override + public String toString() + { + StringBuilder buf=new StringBuilder(); + toString(buf); + return buf.toString(); + } + + private void toString(StringBuilder buf) + { + buf.append("{["); + if (_char==null) + buf.append('-'); + else + for (int i=0;i<_char.length;i++) + buf.append(_char[i]); + buf.append(':'); + buf.append(_key); + buf.append('='); + buf.append(_value); + buf.append(']'); + if (_children!=null) + { + for (int i=0;i<_children.length;i++) + { + buf.append('|'); + if (_children[i]!=null) + _children[i].toString(buf); + else + buf.append("-"); + } + } + buf.append('}'); + if (_next!=null) + { + buf.append(",\n"); + _next.toString(buf); + } + } + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + private class NullEntry implements Map.Entry<Object, Object> + { + public Object getKey(){return null;} + public Object getValue(){return _nullValue;} + public Object setValue(Object o) + {Object old=_nullValue;_nullValue=o;return old;} + @Override + public String toString(){return "[:null="+_nullValue+"]";} + } + + /* ------------------------------------------------------------ */ + public void writeExternal(java.io.ObjectOutput out) + throws java.io.IOException + { + HashMap<Object, Object> map = new HashMap<Object, Object>(this); + out.writeBoolean(_ignoreCase); + out.writeObject(map); + } + + /* ------------------------------------------------------------ */ + public void readExternal(java.io.ObjectInput in) + throws java.io.IOException, ClassNotFoundException + { + boolean ic=in.readBoolean(); + HashMap<?, ?> map = (HashMap<?, ?>)in.readObject(); + setIgnoreCase(ic); + this.putAll(map); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/TypeUtil.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/TypeUtil.java new file mode 100644 index 00000000..48c373fd --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/TypeUtil.java @@ -0,0 +1,48 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util; + +import java.io.IOException; + +/* ------------------------------------------------------------ */ +/** + * TYPE Utilities. + * Provides various static utiltiy methods for manipulating types and their + * string representations. + * + * @since Jetty 4.1 + */ +public class TypeUtil +{ + /* ------------------------------------------------------------ */ + public static void toHex(byte b,Appendable buf) + { + try + { + int d=0xf&((0xF0&b)>>4); + buf.append((char)((d>9?('A'-10):'0')+d)); + d=0xf&b; + buf.append((char)((d>9?('A'-10):'0')+d)); + } + catch(IOException e) + { + throw new RuntimeException(e); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/AbstractLifeCycle.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/AbstractLifeCycle.java new file mode 100644 index 00000000..92c4996c --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/AbstractLifeCycle.java @@ -0,0 +1,193 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.util.concurrent.CopyOnWriteArrayList; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Basic implementation of the life cycle interface for components. + * + * + */ +public abstract class AbstractLifeCycle implements LifeCycle +{ + private static final Logger LOG = Log.getLogger(AbstractLifeCycle.class); + public static final String STOPPED="STOPPED"; + public static final String FAILED="FAILED"; + public static final String STARTING="STARTING"; + public static final String STARTED="STARTED"; + public static final String STOPPING="STOPPING"; + public static final String RUNNING="RUNNING"; + + private final Object _lock = new Object(); + private final int __FAILED = -1, __STOPPED = 0, __STARTING = 1, __STARTED = 2, __STOPPING = 3; + private volatile int _state = __STOPPED; + + protected final CopyOnWriteArrayList<LifeCycle.Listener> _listeners=new CopyOnWriteArrayList<LifeCycle.Listener>(); + + protected void doStart() throws Exception + { + } + + protected void doStop() throws Exception + { + } + + public final void start() throws Exception + { + synchronized (_lock) + { + try + { + if (_state == __STARTED || _state == __STARTING) + return; + setStarting(); + doStart(); + setStarted(); + } + catch (Exception e) + { + setFailed(e); + throw e; + } + catch (Error e) + { + setFailed(e); + throw e; + } + } + } + + public final void stop() throws Exception + { + synchronized (_lock) + { + try + { + if (_state == __STOPPING || _state == __STOPPED) + return; + setStopping(); + doStop(); + setStopped(); + } + catch (Exception e) + { + setFailed(e); + throw e; + } + catch (Error e) + { + setFailed(e); + throw e; + } + } + } + + public boolean isRunning() + { + final int state = _state; + + return state == __STARTED || state == __STARTING; + } + + public boolean isStarted() + { + return _state == __STARTED; + } + + public boolean isStarting() + { + return _state == __STARTING; + } + + public boolean isStopping() + { + return _state == __STOPPING; + } + + public boolean isStopped() + { + return _state == __STOPPED; + } + + public String getState() + { + switch(_state) + { + case __FAILED: return FAILED; + case __STARTING: return STARTING; + case __STARTED: return STARTED; + case __STOPPING: return STOPPING; + case __STOPPED: return STOPPED; + } + return null; + } + + public static String getState(LifeCycle lc) + { + if (lc.isStarting()) return STARTING; + if (lc.isStarted()) return STARTED; + if (lc.isStopping()) return STOPPING; + if (lc.isStopped()) return STOPPED; + return FAILED; + } + + private void setStarted() + { + _state = __STARTED; + LOG.debug(STARTED+" {}",this); + for (Listener listener : _listeners) + listener.lifeCycleStarted(this); + } + + private void setStarting() + { + LOG.debug("starting {}",this); + _state = __STARTING; + for (Listener listener : _listeners) + listener.lifeCycleStarting(this); + } + + private void setStopping() + { + LOG.debug("stopping {}",this); + _state = __STOPPING; + for (Listener listener : _listeners) + listener.lifeCycleStopping(this); + } + + private void setStopped() + { + _state = __STOPPED; + LOG.debug("{} {}",STOPPED,this); + for (Listener listener : _listeners) + listener.lifeCycleStopped(this); + } + + private void setFailed(Throwable th) + { + _state = __FAILED; + LOG.warn(FAILED+" " + this+": "+th,th); + for (Listener listener : _listeners) + listener.lifeCycleFailure(this,th); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/AggregateLifeCycle.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/AggregateLifeCycle.java new file mode 100644 index 00000000..670fbfd1 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/AggregateLifeCycle.java @@ -0,0 +1,250 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; + +/** + * An AggregateLifeCycle is an {@link LifeCycle} implementation for a collection of contained beans. + * <p> + * Beans can be added the AggregateLifeCycle either as managed beans or as unmanaged beans. A managed bean is started, stopped and destroyed with the aggregate. + * An unmanaged bean is associated with the aggregate for the purposes of {@link #dump()}, but it's lifecycle must be managed externally. + * <p> + * When a bean is added, if it is a {@link LifeCycle} and it is already started, then it is assumed to be an unmanaged bean. + * Otherwise the methods {@link #addBean(Object, boolean)}, {@link #manage(Object)} and {@link #unmanage(Object)} can be used to + * explicitly control the life cycle relationship. + * <p> + * If adding a bean that is shared between multiple {@link AggregateLifeCycle} instances, then it should be started before being added, so it is unmanaged, or + * the API must be used to explicitly set it as unmanaged. + * <p> + */ +public class AggregateLifeCycle extends AbstractLifeCycle implements Dumpable +{ + private final List<Bean> _beans=new CopyOnWriteArrayList<Bean>(); + private boolean _started=false; + + private class Bean + { + Bean(Object b) + { + _bean=b; + } + final Object _bean; + volatile boolean _managed=true; + + public String toString() + { + return "{"+_bean+","+_managed+"}"; + } + } + + /* ------------------------------------------------------------ */ + /** + * Start the managed lifecycle beans in the order they were added. + * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart() + */ + @Override + protected void doStart() throws Exception + { + for (Bean b:_beans) + { + if (b._managed && b._bean instanceof LifeCycle) + { + LifeCycle l=(LifeCycle)b._bean; + if (!l.isRunning()) + l.start(); + } + } + // indicate that we are started, so that addBean will start other beans added. + _started=true; + super.doStart(); + } + + /* ------------------------------------------------------------ */ + /** + * Stop the joined lifecycle beans in the reverse order they were added. + * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart() + */ + @Override + protected void doStop() throws Exception + { + _started=false; + super.doStop(); + List<Bean> reverse = new ArrayList<Bean>(_beans); + Collections.reverse(reverse); + for (Bean b:reverse) + { + if (b._managed && b._bean instanceof LifeCycle) + { + LifeCycle l=(LifeCycle)b._bean; + if (l.isRunning()) + l.stop(); + } + } + } + + /* ------------------------------------------------------------ */ + /** Is the bean contained in the aggregate. + * @param bean + * @return True if the aggregate contains the bean + */ + public boolean contains(Object bean) + { + for (Bean b:_beans) + if (b._bean==bean) + return true; + return false; + } + + /* ------------------------------------------------------------ */ + /** + * Add an associated bean. + * If the bean is a {@link LifeCycle}, then it will be managed if it is not + * already started and umanaged if it is already started. The {@link #addBean(Object, boolean)} + * method should be used if this is not correct, or the {@link #manage(Object)} and {@link #unmanage(Object)} + * methods may be used after an add to change the status. + * @param o the bean object to add + * @return true if the bean was added or false if it has already been added. + */ + public boolean addBean(Object o) + { + // beans are joined unless they are started lifecycles + return addBean(o,!((o instanceof LifeCycle)&&((LifeCycle)o).isStarted())); + } + + /* ------------------------------------------------------------ */ + /** Add an associated lifecycle. + * @param o The lifecycle to add + * @param managed True if the LifeCycle is to be joined, otherwise it will be disjoint. + * @return true if bean was added, false if already present. + */ + public boolean addBean(Object o, boolean managed) + { + if (contains(o)) + return false; + + Bean b = new Bean(o); + b._managed=managed; + _beans.add(b); + + if (o instanceof LifeCycle) + { + LifeCycle l=(LifeCycle)o; + + // Start the bean if we are started + if (managed && _started) + { + try + { + l.start(); + } + catch(Exception e) + { + throw new RuntimeException (e); + } + } + } + return true; + } + + /* ------------------------------------------------------------ */ + protected void dumpThis(Appendable out) throws IOException + { + out.append(String.valueOf(this)).append(" - ").append(getState()).append("\n"); + } + + /* ------------------------------------------------------------ */ + public static void dumpObject(Appendable out,Object o) throws IOException + { + try + { + if (o instanceof LifeCycle) + out.append(String.valueOf(o)).append(" - ").append((AbstractLifeCycle.getState((LifeCycle)o))).append("\n"); + else + out.append(String.valueOf(o)).append("\n"); + } + catch(Throwable th) + { + out.append(" => ").append(th.toString()).append('\n'); + } + } + + /* ------------------------------------------------------------ */ + public void dump(Appendable out,String indent) throws IOException + { + dumpThis(out); + int size=_beans.size(); + if (size==0) + return; + int i=0; + for (Bean b : _beans) + { + i++; + + out.append(indent).append(" +- "); + if (b._managed) + { + if (b._bean instanceof Dumpable) + ((Dumpable)b._bean).dump(out,indent+(i==size?" ":" | ")); + else + dumpObject(out,b._bean); + } + else + dumpObject(out,b._bean); + } + + if (i!=size) + out.append(indent).append(" |\n"); + } + + /* ------------------------------------------------------------ */ + public static void dump(Appendable out,String indent,Collection<?>... collections) throws IOException + { + if (collections.length==0) + return; + int size=0; + for (Collection<?> c : collections) + size+=c.size(); + if (size==0) + return; + + int i=0; + for (Collection<?> c : collections) + { + for (Object o : c) + { + i++; + out.append(indent).append(" +- "); + + if (o instanceof Dumpable) + ((Dumpable)o).dump(out,indent+(i==size?" ":" | ")); + else + dumpObject(out,o); + } + + if (i!=size) + out.append(indent).append(" |\n"); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/Dumpable.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/Dumpable.java new file mode 100644 index 00000000..58d2404c --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/Dumpable.java @@ -0,0 +1,26 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.io.IOException; + +public interface Dumpable +{ + void dump(Appendable out,String indent) throws IOException; +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/LifeCycle.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/LifeCycle.java new file mode 100644 index 00000000..85fe6503 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/component/LifeCycle.java @@ -0,0 +1,107 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.component; + +import java.util.EventListener; + +/* ------------------------------------------------------------ */ +/** + * The lifecycle interface for generic components. + * <br /> + * Classes implementing this interface have a defined life cycle + * defined by the methods of this interface. + * + * + */ +public interface LifeCycle +{ + /* ------------------------------------------------------------ */ + /** + * Starts the component. + * @throws Exception If the component fails to start + * @see #isStarted() + * @see #stop() + * @see #isFailed() + */ + public void start() + throws Exception; + + /* ------------------------------------------------------------ */ + /** + * Stops the component. + * The component may wait for current activities to complete + * normally, but it can be interrupted. + * @exception Exception If the component fails to stop + * @see #isStopped() + * @see #start() + * @see #isFailed() + */ + public void stop() + throws Exception; + + /* ------------------------------------------------------------ */ + /** + * @return true if the component is starting or has been started. + */ + public boolean isRunning(); + + /* ------------------------------------------------------------ */ + /** + * @return true if the component has been started. + * @see #start() + * @see #isStarting() + */ + public boolean isStarted(); + + /* ------------------------------------------------------------ */ + /** + * @return true if the component is starting. + * @see #isStarted() + */ + public boolean isStarting(); + + /* ------------------------------------------------------------ */ + /** + * @return true if the component is stopping. + * @see #isStopped() + */ + public boolean isStopping(); + + /* ------------------------------------------------------------ */ + /** + * @return true if the component has been stopped. + * @see #stop() + * @see #isStopping() + */ + public boolean isStopped(); + + + /* ------------------------------------------------------------ */ + /** Listener. + * A listener for Lifecycle events. + */ + public interface Listener extends EventListener + { + public void lifeCycleStarting(LifeCycle event); + public void lifeCycleStarted(LifeCycle event); + public void lifeCycleFailure(LifeCycle event,Throwable cause); + public void lifeCycleStopping(LifeCycle event); + public void lifeCycleStopped(LifeCycle event); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/AndroidLogger.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/AndroidLogger.java new file mode 100644 index 00000000..a04d21fe --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/AndroidLogger.java @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.jetty.util.log; + +/** + * Logger for Android + * + * Copyright (c) 2015 KNOWLEDGECODE + */ +class AndroidLogger implements Logger { + + private String _tag; + + AndroidLogger() { + } + + private AndroidLogger(String tag) { + _tag = tag.substring(tag.length() > 23 ? tag.length() - 23 : 0); + } + + private static String format(String msg, Object... args) { + return String.format(msg.replaceAll("\\{\\}", "%s"), args); + } + + @Override + public String getName() { + return AndroidLogger.class.getName(); + } + + @Override + public void warn(String msg, Object... args) { + android.util.Log.w(_tag, format(msg, args)); + } + + @Override + public void warn(Throwable thrown) { + android.util.Log.w(_tag, thrown); + } + + @Override + public void warn(String msg, Throwable thrown) { + android.util.Log.w(_tag, msg, thrown); + } + + @Override + public void info(String msg, Object... args) { + android.util.Log.i(_tag, format(msg, args)); + } + + @Override + public void info(Throwable thrown) { + android.util.Log.i(_tag, "", thrown); + } + + @Override + public void info(String msg, Throwable thrown) { + android.util.Log.i(_tag, msg, thrown); + } + + @Override + public boolean isDebugEnabled() { + return android.util.Log.isLoggable(_tag, android.util.Log.DEBUG); + } + + @Override + public void setDebugEnabled(boolean enabled) { + throw new UnsupportedOperationException(); + } + + @Override + public void debug(String msg, Object... args) { + android.util.Log.d(_tag, format(msg, args)); + } + + @Override + public void debug(Throwable thrown) { + android.util.Log.d(_tag, "", thrown); + } + + @Override + public void debug(String msg, Throwable thrown) { + android.util.Log.d(_tag, msg, thrown); + } + + @Override + public Logger getLogger(String tag) { + return new AndroidLogger(tag); + } + + @Override + public void ignore(Throwable ignored) { + // ignore + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/Log.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/Log.java new file mode 100644 index 00000000..0549bab3 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/Log.java @@ -0,0 +1,151 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.log; + +/** + * Logging. + * + * Modified by KNOWLEDGECODE + */ +public class Log +{ + public static final String EXCEPTION = "EXCEPTION "; + public static final String IGNORED = "IGNORED "; + + private static Logger LOG = new Logger() + { + @Override + public String getName() + { + return null; + } + + @Override + public void warn(String msg, Object... args) + { + } + + @Override + public void warn(Throwable thrown) + { + } + + @Override + public void warn(String msg, Throwable thrown) + { + } + + @Override + public void info(String msg, Object... args) + { + } + + @Override + public void info(Throwable thrown) + { + } + + @Override + public void info(String msg, Throwable thrown) + { + } + + @Override + public boolean isDebugEnabled() + { + return false; + } + + @Override + public void setDebugEnabled(boolean enabled) + { + } + + @Override + public void debug(String msg, Object... args) + { + } + + @Override + public void debug(Throwable thrown) + { + } + + @Override + public void debug(String msg, Throwable thrown) + { + } + + @Override + public Logger getLogger(String name) + { + return this; + } + + @Override + public void ignore(Throwable ignored) + { + } + }; + + static + { + ClassLoader loader = Thread.currentThread().getContextClassLoader(); + try + { + @SuppressWarnings("unchecked") + Class<Logger> clazz = (Class<Logger>) loader.loadClass("org.eclipse.jetty.util.log.AndroidLogger"); + LOG = clazz.newInstance(); + } + catch (ClassNotFoundException e) + { + } + catch (IllegalAccessException e) + { + } + catch (IllegalArgumentException e) + { + } + catch (InstantiationException e) + { + } + } + + /** + * Obtain a named Logger based on the fully qualified class name. + * + * @param clazz + * the class to base the Logger name off of + * @return the Logger with the given name + */ + public static Logger getLogger(Class<?> clazz) + { + return getLogger(clazz.getName()); + } + + /** + * Obtain a named Logger or the default Logger if null is passed. + * @param name the Logger name + * @return the Logger with the given name + */ + public static Logger getLogger(String name) + { + return LOG.getLogger(name); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/Logger.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/Logger.java new file mode 100644 index 00000000..40f70ff2 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/log/Logger.java @@ -0,0 +1,113 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.log; + +/** + * A simple logging facade that is intended simply to capture the style of logging as used by Jetty. + */ +public interface Logger +{ + /** + * @return the name of this logger + */ + public String getName(); + + /** + * Formats and logs at warn level. + * @param msg the formatting string + * @param args the optional arguments + */ + public void warn(String msg, Object... args); + + /** + * Logs the given Throwable information at warn level + * @param thrown the Throwable to log + */ + public void warn(Throwable thrown); + + /** + * Logs the given message at warn level, with Throwable information. + * @param msg the message to log + * @param thrown the Throwable to log + */ + public void warn(String msg, Throwable thrown); + + /** + * Formats and logs at info level. + * @param msg the formatting string + * @param args the optional arguments + */ + public void info(String msg, Object... args); + + /** + * Logs the given Throwable information at info level + * @param thrown the Throwable to log + */ + public void info(Throwable thrown); + + /** + * Logs the given message at info level, with Throwable information. + * @param msg the message to log + * @param thrown the Throwable to log + */ + public void info(String msg, Throwable thrown); + + /** + * @return whether the debug level is enabled + */ + public boolean isDebugEnabled(); + + /** + * Mutator used to turn debug on programmatically. + * @param enabled whether to enable the debug level + */ + public void setDebugEnabled(boolean enabled); + + /** + * Formats and logs at debug level. + * @param msg the formatting string + * @param args the optional arguments + */ + public void debug(String msg, Object... args); + + /** + * Logs the given Throwable information at debug level + * @param thrown the Throwable to log + */ + public void debug(Throwable thrown); + + /** + * Logs the given message at debug level, with Throwable information. + * @param msg the message to log + * @param thrown the Throwable to log + */ + public void debug(String msg, Throwable thrown); + + /** + * @param name the name of the logger + * @return a logger with the given name + */ + public Logger getLogger(String name); + + /** + * Ignore an exception. + * <p>This should be used rather than an empty catch block. + */ + public void ignore(Throwable ignored); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/resource/Resource.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/resource/Resource.java new file mode 100644 index 00000000..90a3ba2f --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/resource/Resource.java @@ -0,0 +1,119 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.resource; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.net.URL; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/* ------------------------------------------------------------ */ +/**g + * Abstract resource class. + */ +public abstract class Resource +{ + private static final Logger LOG = Log.getLogger(Resource.class); + public static boolean __defaultUseCaches = true; + volatile Object _associate; + + /* ------------------------------------------------------------ */ + /** Construct a resource from a url. + * @param url A URL. + * @return A Resource object. + * @throws IOException Problem accessing URL + */ + public static Resource newResource(URL url) + throws IOException + { + return newResource(url, __defaultUseCaches); + } + + /* ------------------------------------------------------------ */ + /** + * Construct a resource from a url. + * @param url the url for which to make the resource + * @param useCaches true enables URLConnection caching if applicable to the type of resource + * @return + */ + static Resource newResource(URL url, boolean useCaches) + { + if (url==null) + return null; + + return new URLResource(url,null,useCaches); + } + + /* ------------------------------------------------------------ */ + /** Construct a resource from a string. + * @param resource A URL or filename. + * @return A Resource object. + */ + public static Resource newResource(String resource) + throws MalformedURLException, IOException + { + return newResource(resource, __defaultUseCaches); + } + + /* ------------------------------------------------------------ */ + /** Construct a resource from a string. + * @param resource A URL or filename. + * @param useCaches controls URLConnection caching + * @return A Resource object. + */ + public static Resource newResource (String resource, boolean useCaches) + throws MalformedURLException, IOException + { + URL url=null; + try + { + // Try to format as a URL? + url = new URL(resource); + } + catch(MalformedURLException e) + { + LOG.warn("Bad Resource: "+resource); + throw e; + } + + return newResource(url); + } + + /* ------------------------------------------------------------ */ + @Override + protected void finalize() + { + release(); + } + + /* ------------------------------------------------------------ */ + /** Release any temporary resources held by the resource. + */ + public abstract void release(); + + /* ------------------------------------------------------------ */ + /** + * Returns an input stream to the resource + */ + public abstract InputStream getInputStream() + throws java.io.IOException; +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/resource/URLResource.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/resource/URLResource.java new file mode 100644 index 00000000..5de288ef --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/resource/URLResource.java @@ -0,0 +1,130 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.resource; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/* ------------------------------------------------------------ */ +/** Abstract resource class. + */ +public class URLResource extends Resource +{ + private static final Logger LOG = Log.getLogger(URLResource.class); + protected URL _url; + protected String _urlString; + + protected URLConnection _connection; + protected InputStream _in=null; + transient boolean _useCaches = Resource.__defaultUseCaches; + + /* ------------------------------------------------------------ */ + protected URLResource(URL url, URLConnection connection) + { + _url = url; + _urlString=_url.toString(); + _connection=connection; + } + + /* ------------------------------------------------------------ */ + protected URLResource (URL url, URLConnection connection, boolean useCaches) + { + this (url, connection); + _useCaches = useCaches; + } + + /* ------------------------------------------------------------ */ + protected synchronized boolean checkConnection() + { + if (_connection==null) + { + try{ + _connection=_url.openConnection(); + _connection.setUseCaches(_useCaches); + } + catch(IOException e) + { + LOG.ignore(e); + } + } + return _connection!=null; + } + + /* ------------------------------------------------------------ */ + /** Release any resources held by the resource. + */ + @Override + public synchronized void release() + { + if (_in!=null) + { + try{_in.close();}catch(IOException e){LOG.ignore(e);} + _in=null; + } + + if (_connection!=null) + _connection=null; + } + + /* ------------------------------------------------------------ */ + /** + * Returns an input stream to the resource + */ + @Override + public synchronized InputStream getInputStream() + throws java.io.IOException + { + if (!checkConnection()) + throw new IOException( "Invalid resource"); + + try + { + if( _in != null) + { + InputStream in = _in; + _in=null; + return in; + } + return _connection.getInputStream(); + } + finally + { + _connection=null; + } + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return _urlString; + } + + /* ------------------------------------------------------------ */ + @Override + public boolean equals( Object o) + { + return o instanceof URLResource && _urlString.equals(((URLResource)o)._urlString); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/CertificateUtils.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/CertificateUtils.java new file mode 100644 index 00000000..83828f95 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/CertificateUtils.java @@ -0,0 +1,93 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.security; + +import java.io.InputStream; +import java.security.KeyStore; +import java.security.cert.CRL; +import java.security.cert.CertificateFactory; +import java.util.Collection; + +import org.eclipse.jetty.util.resource.Resource; + +public class CertificateUtils +{ + /* ------------------------------------------------------------ */ + public static KeyStore getKeyStore(InputStream storeStream, String storePath, String storeType, String storeProvider, String storePassword) throws Exception + { + KeyStore keystore = null; + + if (storeStream != null || storePath != null) + { + InputStream inStream = storeStream; + try + { + if (inStream == null) + { + inStream = Resource.newResource(storePath).getInputStream(); + } + + if (storeProvider != null) + { + keystore = KeyStore.getInstance(storeType, storeProvider); + } + else + { + keystore = KeyStore.getInstance(storeType); + } + + keystore.load(inStream, storePassword == null ? null : storePassword.toCharArray()); + } + finally + { + if (inStream != null) + { + inStream.close(); + } + } + } + + return keystore; + } + + /* ------------------------------------------------------------ */ + public static Collection<? extends CRL> loadCRL(String crlPath) throws Exception + { + Collection<? extends CRL> crlList = null; + + if (crlPath != null) + { + InputStream in = null; + try + { + in = Resource.newResource(crlPath).getInputStream(); + crlList = CertificateFactory.getInstance("X.509").generateCRLs(in); + } + finally + { + if (in != null) + { + in.close(); + } + } + } + + return crlList; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/CertificateValidator.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/CertificateValidator.java new file mode 100644 index 00000000..b018b83f --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/CertificateValidator.java @@ -0,0 +1,228 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.security; + +import java.security.GeneralSecurityException; +import java.security.InvalidParameterException; +import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.Security; +import java.security.cert.CRL; +import java.security.cert.CertPathBuilder; +import java.security.cert.CertPathBuilderResult; +import java.security.cert.CertPathValidator; +import java.security.cert.CertStore; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.X509CertSelector; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Collection; +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/** + * Convenience class to handle validation of certificates, aliases and keystores + * + * Allows specifying Certificate Revocation List (CRL), as well as enabling + * CRL Distribution Points Protocol (CRLDP) certificate extension support, + * and also enabling On-Line Certificate Status Protocol (OCSP) support. + * + * IMPORTANT: at least one of the above mechanisms *MUST* be configured and + * operational, otherwise certificate validation *WILL FAIL* unconditionally. + */ +public class CertificateValidator +{ + private static final Logger LOG = Log.getLogger(CertificateValidator.class); + private static AtomicLong __aliasCount = new AtomicLong(); + + private KeyStore _trustStore; + private Collection<? extends CRL> _crls; + + /** Maximum certification path length (n - number of intermediate certs, -1 for unlimited) */ + private int _maxCertPathLength = -1; + /** CRL Distribution Points (CRLDP) support */ + private boolean _enableCRLDP = false; + /** On-Line Certificate Status Protocol (OCSP) support */ + private boolean _enableOCSP = false; + + /** + * creates an instance of the certificate validator + * + * @param trustStore + * @param crls + */ + public CertificateValidator(KeyStore trustStore, Collection<? extends CRL> crls) + { + if (trustStore == null) + { + throw new InvalidParameterException("TrustStore must be specified for CertificateValidator."); + } + + _trustStore = trustStore; + _crls = crls; + } + + /** + * validates a specific certificate inside of the keystore being passed in + * + * @param keyStore + * @param cert + * @throws CertificateException + */ + public void validate(KeyStore keyStore, Certificate cert) throws CertificateException + { + Certificate[] certChain = null; + + if (cert != null && cert instanceof X509Certificate) + { + ((X509Certificate)cert).checkValidity(); + + String certAlias = null; + try + { + if (keyStore == null) + { + throw new InvalidParameterException("Keystore cannot be null"); + } + + certAlias = keyStore.getCertificateAlias((X509Certificate)cert); + if (certAlias == null) + { + certAlias = "JETTY" + String.format("%016X",__aliasCount.incrementAndGet()); + keyStore.setCertificateEntry(certAlias, cert); + } + + certChain = keyStore.getCertificateChain(certAlias); + if (certChain == null || certChain.length == 0) + { + throw new IllegalStateException("Unable to retrieve certificate chain"); + } + } + catch (KeyStoreException kse) + { + LOG.debug(kse); + throw new CertificateException("Unable to validate certificate" + + (certAlias == null ? "":" for alias [" +certAlias + "]") + ": " + kse.getMessage(), kse); + } + + validate(certChain); + } + } + + public void validate(Certificate[] certChain) throws CertificateException + { + try + { + ArrayList<X509Certificate> certList = new ArrayList<X509Certificate>(); + for (Certificate item : certChain) + { + if (item == null) + continue; + + if (!(item instanceof X509Certificate)) + { + throw new IllegalStateException("Invalid certificate type in chain"); + } + + certList.add((X509Certificate)item); + } + + if (certList.isEmpty()) + { + throw new IllegalStateException("Invalid certificate chain"); + + } + + X509CertSelector certSelect = new X509CertSelector(); + certSelect.setCertificate(certList.get(0)); + + // Configure certification path builder parameters + PKIXBuilderParameters pbParams = new PKIXBuilderParameters(_trustStore, certSelect); + pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(certList))); + + // Set maximum certification path length + pbParams.setMaxPathLength(_maxCertPathLength); + + // Enable revocation checking + pbParams.setRevocationEnabled(true); + + // Set static Certificate Revocation List + if (_crls != null && !_crls.isEmpty()) + { + pbParams.addCertStore(CertStore.getInstance("Collection", new CollectionCertStoreParameters(_crls))); + } + + // Enable On-Line Certificate Status Protocol (OCSP) support + if (_enableOCSP) + { + Security.setProperty("ocsp.enable","true"); + } + // Enable Certificate Revocation List Distribution Points (CRLDP) support + if (_enableCRLDP) + { + System.setProperty("com.sun.security.enableCRLDP","true"); + } + + // Build certification path + CertPathBuilderResult buildResult = CertPathBuilder.getInstance("PKIX").build(pbParams); + + // Validate certification path + CertPathValidator.getInstance("PKIX").validate(buildResult.getCertPath(),pbParams); + } + catch (GeneralSecurityException gse) + { + LOG.debug(gse); + throw new CertificateException("Unable to validate certificate: " + gse.getMessage(), gse); + } + } + + /* ------------------------------------------------------------ */ + /** + * @param maxCertPathLength + * maximum number of intermediate certificates in + * the certification path (-1 for unlimited) + */ + public void setMaxCertPathLength(int maxCertPathLength) + { + _maxCertPathLength = maxCertPathLength; + } + + /* ------------------------------------------------------------ */ + /** Enables CRL Distribution Points Support + * @param enableCRLDP true - turn on, false - turns off + */ + public void setEnableCRLDP(boolean enableCRLDP) + { + _enableCRLDP = enableCRLDP; + } + + /* ------------------------------------------------------------ */ + /** Enables On-Line Certificate Status Protocol support + * @param enableOCSP true - turn on, false - turn off + */ + public void setEnableOCSP(boolean enableOCSP) + { + _enableOCSP = enableOCSP; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/Password.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/Password.java new file mode 100644 index 00000000..255eee60 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/security/Password.java @@ -0,0 +1,84 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.security; + +/* ------------------------------------------------------------ */ +/** + * Password utility class. + * + * This utility class gets a password or pass phrase either by: + * + * <PRE> + * + Password is set as a system property. + * + The password is prompted for and read from standard input + * + A program is run to get the password. + * </pre> + * + * Passwords that begin with OBF: are de obfuscated. Passwords can be obfuscated + * by run org.eclipse.util.Password as a main class. Obfuscated password are + * required if a system needs to recover the full password (eg. so that it may + * be passed to another system). They are not secure, but prevent casual + * observation. + * <p> + * Passwords that begin with CRYPT: are oneway encrypted with UnixCrypt. The + * real password cannot be retrieved, but comparisons can be made to other + * passwords. A Crypt can be generated by running org.eclipse.util.UnixCrypt as + * a main class, passing password and then the username. Checksum passwords are + * a secure(ish) way to store passwords that only need to be checked rather than + * recovered. Note that it is not strong security - specially if simple + * passwords are used. + * + * + */ +public class Password +{ + public static final String __OBFUSCATE = "OBF:"; + + private String _pw; + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return _pw; + } + + /* ------------------------------------------------------------ */ + @Override + public boolean equals(Object o) + { + if (this == o) + return true; + + if (null == o) + return false; + + if (o instanceof Password) + { + Password p = (Password) o; + //noinspection StringEquality + return p._pw == _pw || (null != _pw && _pw.equals(p._pw)); + } + + if (o instanceof String) + return o.equals(_pw); + + return false; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ssl/AliasedX509ExtendedKeyManager.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ssl/AliasedX509ExtendedKeyManager.java new file mode 100644 index 00000000..4c0da301 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ssl/AliasedX509ExtendedKeyManager.java @@ -0,0 +1,107 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.ssl; + +import java.net.Socket; +import java.security.Principal; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; + +import javax.net.ssl.X509ExtendedKeyManager; +import javax.net.ssl.X509KeyManager; + + +/* ------------------------------------------------------------ */ +/** + * KeyManager to select a key with desired alias + * while delegating processing to specified KeyManager + * Can be used both with server and client sockets + */ +public class AliasedX509ExtendedKeyManager extends X509ExtendedKeyManager +{ + private String _keyAlias; + private X509KeyManager _keyManager; + + /* ------------------------------------------------------------ */ + /** + * Construct KeyManager instance + * @param keyAlias Alias of the key to be selected + * @param keyManager Instance of KeyManager to be wrapped + * @throws Exception + */ + public AliasedX509ExtendedKeyManager(String keyAlias, X509KeyManager keyManager) throws Exception + { + _keyAlias = keyAlias; + _keyManager = keyManager; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.net.ssl.X509KeyManager#chooseClientAlias(java.lang.String[], java.security.Principal[], java.net.Socket) + */ + public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) + { + return _keyAlias == null ? _keyManager.chooseClientAlias(keyType, issuers, socket) : _keyAlias; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.net.ssl.X509KeyManager#chooseServerAlias(java.lang.String, java.security.Principal[], java.net.Socket) + */ + public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) + { + return _keyAlias == null ? _keyManager.chooseServerAlias(keyType, issuers, socket) : _keyAlias; + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.net.ssl.X509KeyManager#getClientAliases(java.lang.String, java.security.Principal[]) + */ + public String[] getClientAliases(String keyType, Principal[] issuers) + { + return _keyManager.getClientAliases(keyType, issuers); + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.net.ssl.X509KeyManager#getServerAliases(java.lang.String, java.security.Principal[]) + */ + public String[] getServerAliases(String keyType, Principal[] issuers) + { + return _keyManager.getServerAliases(keyType, issuers); + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.net.ssl.X509KeyManager#getCertificateChain(java.lang.String) + */ + public X509Certificate[] getCertificateChain(String alias) + { + return _keyManager.getCertificateChain(alias); + } + + /* ------------------------------------------------------------ */ + /** + * @see javax.net.ssl.X509KeyManager#getPrivateKey(java.lang.String) + */ + public PrivateKey getPrivateKey(String alias) + { + return _keyManager.getPrivateKey(alias); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ssl/SslContextFactory.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ssl/SslContextFactory.java new file mode 100644 index 00000000..4ac6ed95 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/ssl/SslContextFactory.java @@ -0,0 +1,620 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.ssl; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.InputStream; +import java.security.KeyStore; +import java.security.SecureRandom; +import java.security.Security; +import java.security.cert.CRL; +import java.security.cert.CertStore; +import java.security.cert.Certificate; +import java.security.cert.CollectionCertStoreParameters; +import java.security.cert.PKIXBuilderParameters; +import java.security.cert.X509CertSelector; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; +import javax.net.ssl.CertPathTrustManagerParameters; +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509KeyManager; +import javax.net.ssl.X509TrustManager; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.security.CertificateUtils; +import org.eclipse.jetty.util.security.CertificateValidator; +import org.eclipse.jetty.util.security.Password; + +import android.annotation.SuppressLint; +import android.os.Build; + + +/* ------------------------------------------------------------ */ +/** + * SslContextFactory is used to configure SSL connectors + * as well as HttpClient. It holds all SSL parameters and + * creates SSL context based on these parameters to be + * used by the SSL connectors. + * + * modified by KNOWLEDGECODE + */ +public class SslContextFactory extends AbstractLifeCycle +{ + public final static TrustManager[] TRUST_ALL_CERTS = new X509TrustManager[]{new X509TrustManager() + { + public java.security.cert.X509Certificate[] getAcceptedIssuers() + { + return new java.security.cert.X509Certificate[]{}; + } + + public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) + { + } + + public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) + { + /** + * workaround for SSL bugs in 4.0.3 and lower + * @see https://github.com/koush/AndroidAsync/blob/master/AndroidAsync/src/com/koushikdutta/async/AsyncSSLSocketWrapper.java + */ + if (Build.VERSION.SDK_INT <= 15) + { + for (java.security.cert.X509Certificate cert : certs) + { + if (cert != null && cert.getCriticalExtensionOIDs() != null) + { + cert.getCriticalExtensionOIDs().remove("2.5.29.15"); + } + } + } + } + }}; + + private static final Logger LOG = Log.getLogger(SslContextFactory.class); + + public static final String DEFAULT_KEYMANAGERFACTORY_ALGORITHM = + (Security.getProperty("ssl.KeyManagerFactory.algorithm") == null ? + "SunX509" : Security.getProperty("ssl.KeyManagerFactory.algorithm")); + public static final String DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM = + (Security.getProperty("ssl.TrustManagerFactory.algorithm") == null ? + "SunX509" : Security.getProperty("ssl.TrustManagerFactory.algorithm")); + + /** Default value for the keystore location path. */ + public static final String DEFAULT_KEYSTORE_PATH = + System.getProperty("user.home") + File.separator + ".keystore"; + + /** String name of key password property. */ + public static final String KEYPASSWORD_PROPERTY = "org.eclipse.jetty.ssl.keypassword"; + + /** String name of keystore password property. */ + public static final String PASSWORD_PROPERTY = "org.eclipse.jetty.ssl.password"; + + /** Excluded protocols. */ + private final Set<String> _excludeProtocols = new LinkedHashSet<String>(); + /** Included protocols. */ + private Set<String> _includeProtocols = new LinkedHashSet<String>(); + + /** Excluded cipher suites. */ + private final Set<String> _excludeCipherSuites = new LinkedHashSet<String>(); + /** Included cipher suites. */ + private Set<String> _includeCipherSuites = new LinkedHashSet<String>(); + + /** Keystore path. */ + private String _keyStorePath; + /** Keystore provider name */ + private String _keyStoreProvider; + /** Keystore type */ + private String _keyStoreType = "JKS"; + /** Keystore input stream */ + private InputStream _keyStoreInputStream; + + /** SSL certificate alias */ + private String _certAlias; + + /** Truststore path */ + private String _trustStorePath; + /** Truststore provider name */ + private String _trustStoreProvider; + /** Truststore type */ + private String _trustStoreType = "JKS"; + /** Truststore input stream */ + private InputStream _trustStoreInputStream; + + /** Set to true if client certificate authentication is required */ + private boolean _needClientAuth = false; + /** Set to true if client certificate authentication is desired */ + private boolean _wantClientAuth = false; + + /** Keystore password */ + private transient Password _keyStorePassword; + /** Key manager password */ + private transient Password _keyManagerPassword; + /** Truststore password */ + private transient Password _trustStorePassword; + + /** SSL provider name */ + private String _sslProvider; + /** SSL protocol name */ + private String _sslProtocol = "TLS"; + + /** SecureRandom algorithm */ + private String _secureRandomAlgorithm; + /** KeyManager factory algorithm */ + private String _keyManagerFactoryAlgorithm = DEFAULT_KEYMANAGERFACTORY_ALGORITHM; + /** TrustManager factory algorithm */ + private String _trustManagerFactoryAlgorithm = DEFAULT_TRUSTMANAGERFACTORY_ALGORITHM; + + /** Set to true if SSL certificate validation is required */ + private boolean _validateCerts; + /** Set to true if SSL certificate of the peer validation is required */ + private boolean _validatePeerCerts; + /** Maximum certification path length (n - number of intermediate certs, -1 for unlimited) */ + private int _maxCertPathLength = -1; + /** Path to file that contains Certificate Revocation List */ + private String _crlPath; + /** Set to true to enable CRL Distribution Points (CRLDP) support */ + private boolean _enableCRLDP = false; + /** Set to true to enable On-Line Certificate Status Protocol (OCSP) support */ + private boolean _enableOCSP = false; + /** Location of OCSP Responder */ + private String _ocspResponderURL; + + /** SSL keystore */ + private KeyStore _keyStore; + /** SSL truststore */ + private KeyStore _trustStore; + /** Set to true to enable SSL Session caching */ + private boolean _sessionCachingEnabled = true; + + /** SSL context */ + private SSLContext _context; + + private boolean _trustAll; + + /* ------------------------------------------------------------ */ + /** + * Construct an instance of SslContextFactory + * Default constructor for use in XmlConfiguration files + */ + public SslContextFactory() + { + _trustAll=true; + } + + /* ------------------------------------------------------------ */ + /** + * Create the SSLContext object and start the lifecycle + * @see org.eclipse.jetty.util.component.AbstractLifeCycle#doStart() + */ + @SuppressLint("TrulyRandom") + @Override + protected void doStart() throws Exception + { + if (_context == null) + { + if (_keyStore==null && _keyStoreInputStream == null && _keyStorePath == null && + _trustStore==null && _trustStoreInputStream == null && _trustStorePath == null ) + { + TrustManager[] trust_managers=null; + + if (_trustAll) + { + LOG.debug("No keystore or trust store configured. ACCEPTING UNTRUSTED CERTIFICATES!!!!!"); + // Create a trust manager that does not validate certificate chains + trust_managers = TRUST_ALL_CERTS; + } + + SecureRandom secureRandom = (_secureRandomAlgorithm == null)?null:SecureRandom.getInstance(_secureRandomAlgorithm); + _context = (_sslProvider == null)?SSLContext.getInstance(_sslProtocol):SSLContext.getInstance(_sslProtocol,_sslProvider); + _context.init(null, trust_managers, secureRandom); + } + else + { + // verify that keystore and truststore + // parameters are set up correctly + checkKeyStore(); + + KeyStore keyStore = loadKeyStore(); + KeyStore trustStore = loadTrustStore(); + + Collection<? extends CRL> crls = loadCRL(_crlPath); + + if (_validateCerts && keyStore != null) + { + if (_certAlias == null) + { + List<String> aliases = Collections.list(keyStore.aliases()); + _certAlias = aliases.size() == 1 ? aliases.get(0) : null; + } + + Certificate cert = _certAlias == null?null:keyStore.getCertificate(_certAlias); + if (cert == null) + { + throw new Exception("No certificate found in the keystore" + (_certAlias==null ? "":" for alias " + _certAlias)); + } + + CertificateValidator validator = new CertificateValidator(trustStore, crls); + validator.setMaxCertPathLength(_maxCertPathLength); + validator.setEnableCRLDP(_enableCRLDP); + validator.setEnableOCSP(_enableOCSP); + validator.validate(keyStore, cert); + } + + KeyManager[] keyManagers = getKeyManagers(keyStore); + TrustManager[] trustManagers = getTrustManagers(trustStore,crls); + + SecureRandom secureRandom = (_secureRandomAlgorithm == null)?null:SecureRandom.getInstance(_secureRandomAlgorithm); + _context = (_sslProvider == null)?SSLContext.getInstance(_sslProtocol):SSLContext.getInstance(_sslProtocol,_sslProvider); + _context.init(keyManagers,trustManagers,secureRandom); + + SSLEngine engine=newSslEngine(); + + LOG.info("Enabled Protocols {} of {}",Arrays.asList(engine.getEnabledProtocols()),Arrays.asList(engine.getSupportedProtocols())); + if (LOG.isDebugEnabled()) + LOG.debug("Enabled Ciphers {} of {}",Arrays.asList(engine.getEnabledCipherSuites()),Arrays.asList(engine.getSupportedCipherSuites())); + } + } + } + + /* ------------------------------------------------------------ */ + /** + * @return True if SSL needs client authentication. + * @see SSLEngine#getNeedClientAuth() + */ + public boolean getNeedClientAuth() + { + return _needClientAuth; + } + + /* ------------------------------------------------------------ */ + /** + * @return True if SSL wants client authentication. + * @see SSLEngine#getWantClientAuth() + */ + public boolean getWantClientAuth() + { + return _wantClientAuth; + } + + /* ------------------------------------------------------------ */ + /** + * Override this method to provide alternate way to load a keystore. + * + * @return the key store instance + * @throws Exception if the keystore cannot be loaded + */ + protected KeyStore loadKeyStore() throws Exception + { + return _keyStore != null ? _keyStore : getKeyStore(_keyStoreInputStream, + _keyStorePath, _keyStoreType, _keyStoreProvider, + _keyStorePassword==null? null: _keyStorePassword.toString()); + } + + /* ------------------------------------------------------------ */ + /** + * Override this method to provide alternate way to load a truststore. + * + * @return the key store instance + * @throws Exception if the truststore cannot be loaded + */ + protected KeyStore loadTrustStore() throws Exception + { + return _trustStore != null ? _trustStore : getKeyStore(_trustStoreInputStream, + _trustStorePath, _trustStoreType, _trustStoreProvider, + _trustStorePassword==null? null: _trustStorePassword.toString()); + } + + /* ------------------------------------------------------------ */ + /** + * Loads keystore using an input stream or a file path in the same + * order of precedence. + * + * Required for integrations to be able to override the mechanism + * used to load a keystore in order to provide their own implementation. + * + * @param storeStream keystore input stream + * @param storePath path of keystore file + * @param storeType keystore type + * @param storeProvider keystore provider + * @param storePassword keystore password + * @return created keystore + * @throws Exception if the keystore cannot be obtained + */ + protected KeyStore getKeyStore(InputStream storeStream, String storePath, String storeType, String storeProvider, String storePassword) throws Exception + { + return CertificateUtils.getKeyStore(storeStream, storePath, storeType, storeProvider, storePassword); + } + + /* ------------------------------------------------------------ */ + /** + * Loads certificate revocation list (CRL) from a file. + * + * Required for integrations to be able to override the mechanism used to + * load CRL in order to provide their own implementation. + * + * @param crlPath path of certificate revocation list file + * @return Collection of CRL's + * @throws Exception if the certificate revocation list cannot be loaded + */ + protected Collection<? extends CRL> loadCRL(String crlPath) throws Exception + { + return CertificateUtils.loadCRL(crlPath); + } + + /* ------------------------------------------------------------ */ + protected KeyManager[] getKeyManagers(KeyStore keyStore) throws Exception + { + KeyManager[] managers = null; + + if (keyStore != null) + { + KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(_keyManagerFactoryAlgorithm); + keyManagerFactory.init(keyStore,_keyManagerPassword == null?(_keyStorePassword == null?null:_keyStorePassword.toString().toCharArray()):_keyManagerPassword.toString().toCharArray()); + managers = keyManagerFactory.getKeyManagers(); + + if (_certAlias != null) + { + for (int idx = 0; idx < managers.length; idx++) + { + if (managers[idx] instanceof X509KeyManager) + { + managers[idx] = new AliasedX509ExtendedKeyManager(_certAlias,(X509KeyManager)managers[idx]); + } + } + } + } + + return managers; + } + + /* ------------------------------------------------------------ */ + protected TrustManager[] getTrustManagers(KeyStore trustStore, Collection<? extends CRL> crls) throws Exception + { + TrustManager[] managers = null; + if (trustStore != null) + { + // Revocation checking is only supported for PKIX algorithm + if (_validatePeerCerts && _trustManagerFactoryAlgorithm.equalsIgnoreCase("PKIX")) + { + PKIXBuilderParameters pbParams = new PKIXBuilderParameters(trustStore,new X509CertSelector()); + + // Set maximum certification path length + pbParams.setMaxPathLength(_maxCertPathLength); + + // Make sure revocation checking is enabled + pbParams.setRevocationEnabled(true); + + if (crls != null && !crls.isEmpty()) + { + pbParams.addCertStore(CertStore.getInstance("Collection",new CollectionCertStoreParameters(crls))); + } + + if (_enableCRLDP) + { + // Enable Certificate Revocation List Distribution Points (CRLDP) support + System.setProperty("com.sun.security.enableCRLDP","true"); + } + + if (_enableOCSP) + { + // Enable On-Line Certificate Status Protocol (OCSP) support + Security.setProperty("ocsp.enable","true"); + + if (_ocspResponderURL != null) + { + // Override location of OCSP Responder + Security.setProperty("ocsp.responderURL", _ocspResponderURL); + } + } + + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_trustManagerFactoryAlgorithm); + trustManagerFactory.init(new CertPathTrustManagerParameters(pbParams)); + + managers = trustManagerFactory.getTrustManagers(); + } + else + { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(_trustManagerFactoryAlgorithm); + trustManagerFactory.init(trustStore); + + managers = trustManagerFactory.getTrustManagers(); + } + } + + return managers; + } + + /* ------------------------------------------------------------ */ + /** + * Check KeyStore Configuration. Ensures that if keystore has been + * configured but there's no truststore, that keystore is + * used as truststore. + * @throws IllegalStateException if SslContextFactory configuration can't be used. + */ + public void checkKeyStore() + { + if (_context != null) + return; //nothing to check if using preconfigured context + + + if (_keyStore == null && _keyStoreInputStream == null && _keyStorePath == null) + throw new IllegalStateException("SSL doesn't have a valid keystore"); + + // if the keystore has been configured but there is no + // truststore configured, use the keystore as the truststore + if (_trustStore == null && _trustStoreInputStream == null && _trustStorePath == null) + { + _trustStore = _keyStore; + _trustStorePath = _keyStorePath; + _trustStoreInputStream = _keyStoreInputStream; + _trustStoreType = _keyStoreType; + _trustStoreProvider = _keyStoreProvider; + _trustStorePassword = _keyStorePassword; + _trustManagerFactoryAlgorithm = _keyManagerFactoryAlgorithm; + } + + // It's the same stream we cannot read it twice, so read it once in memory + if (_keyStoreInputStream != null && _keyStoreInputStream == _trustStoreInputStream) + { + try + { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + IO.copy(_keyStoreInputStream, baos); + _keyStoreInputStream.close(); + + _keyStoreInputStream = new ByteArrayInputStream(baos.toByteArray()); + _trustStoreInputStream = new ByteArrayInputStream(baos.toByteArray()); + } + catch (Exception ex) + { + throw new IllegalStateException(ex); + } + } + } + + /* ------------------------------------------------------------ */ + /** + * Select protocols to be used by the connector + * based on configured inclusion and exclusion lists + * as well as enabled and supported protocols. + * @param enabledProtocols Array of enabled protocols + * @param supportedProtocols Array of supported protocols + * @return Array of protocols to enable + */ + public String[] selectProtocols(String[] enabledProtocols, String[] supportedProtocols) + { + Set<String> selected_protocols = new LinkedHashSet<String>(); + + // Set the starting protocols - either from the included or enabled list + if (!_includeProtocols.isEmpty()) + { + // Use only the supported included protocols + for (String protocol : _includeProtocols) + if(Arrays.asList(supportedProtocols).contains(protocol)) + selected_protocols.add(protocol); + } + else + selected_protocols.addAll(Arrays.asList(enabledProtocols)); + + + // Remove any excluded protocols + if (_excludeProtocols != null) + selected_protocols.removeAll(_excludeProtocols); + + return selected_protocols.toArray(new String[selected_protocols.size()]); + } + + /* ------------------------------------------------------------ */ + /** + * Select cipher suites to be used by the connector + * based on configured inclusion and exclusion lists + * as well as enabled and supported cipher suite lists. + * @param enabledCipherSuites Array of enabled cipher suites + * @param supportedCipherSuites Array of supported cipher suites + * @return Array of cipher suites to enable + */ + public String[] selectCipherSuites(String[] enabledCipherSuites, String[] supportedCipherSuites) + { + Set<String> selected_ciphers = new LinkedHashSet<String>(); + + // Set the starting ciphers - either from the included or enabled list + if (!_includeCipherSuites.isEmpty()) + { + // Use only the supported included ciphers + for (String cipherSuite : _includeCipherSuites) + if(Arrays.asList(supportedCipherSuites).contains(cipherSuite)) + selected_ciphers.add(cipherSuite); + } + else + selected_ciphers.addAll(Arrays.asList(enabledCipherSuites)); + + + // Remove any excluded ciphers + if (_excludeCipherSuites != null) + selected_ciphers.removeAll(_excludeCipherSuites); + return selected_ciphers.toArray(new String[selected_ciphers.size()]); + } + + /* ------------------------------------------------------------ */ + /** + * @return true if SSL Session caching is enabled + */ + public boolean isSessionCachingEnabled() + { + return _sessionCachingEnabled; + } + + /* ------------------------------------------------------------ */ + public SSLEngine newSslEngine(String host,int port) + { + SSLEngine sslEngine=isSessionCachingEnabled() + ?_context.createSSLEngine(host, port) + :_context.createSSLEngine(); + + customize(sslEngine); + return sslEngine; + } + + /* ------------------------------------------------------------ */ + public SSLEngine newSslEngine() + { + SSLEngine sslEngine=_context.createSSLEngine(); + customize(sslEngine); + return sslEngine; + } + + /* ------------------------------------------------------------ */ + public void customize(SSLEngine sslEngine) + { + if (getWantClientAuth()) + sslEngine.setWantClientAuth(getWantClientAuth()); + if (getNeedClientAuth()) + sslEngine.setNeedClientAuth(getNeedClientAuth()); + + sslEngine.setEnabledCipherSuites(selectCipherSuites( + sslEngine.getEnabledCipherSuites(), + sslEngine.getSupportedCipherSuites())); + + sslEngine.setEnabledProtocols(selectProtocols(sslEngine.getEnabledProtocols(),sslEngine.getSupportedProtocols())); + } + + /* ------------------------------------------------------------ */ + public String toString() + { + return String.format("%s@%x(%s,%s)", + getClass().getSimpleName(), + hashCode(), + _keyStorePath, + _trustStorePath); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/QueuedThreadPool.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/QueuedThreadPool.java new file mode 100644 index 00000000..28cf3bd4 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/QueuedThreadPool.java @@ -0,0 +1,385 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + + +package org.eclipse.jetty.util.thread; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.Executor; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; + +import org.eclipse.jetty.util.BlockingArrayQueue; +import org.eclipse.jetty.util.ConcurrentHashSet; +import org.eclipse.jetty.util.component.AbstractLifeCycle; +import org.eclipse.jetty.util.component.AggregateLifeCycle; +import org.eclipse.jetty.util.component.Dumpable; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +public class QueuedThreadPool extends AbstractLifeCycle implements ThreadPool, Executor, Dumpable +{ + private static final Logger LOG = Log.getLogger(QueuedThreadPool.class); + + private final AtomicInteger _threadsStarted = new AtomicInteger(); + private final AtomicInteger _threadsIdle = new AtomicInteger(); + private final AtomicLong _lastShrink = new AtomicLong(); + private final ConcurrentHashSet<Thread> _threads=new ConcurrentHashSet<Thread>(); + private final Object _joinLock = new Object(); + private BlockingQueue<Runnable> _jobs; + private String _name; + private int _maxIdleTimeMs=60000; + private int _maxThreads=254; + private int _minThreads=8; + private int _maxQueued=-1; + private int _priority=Thread.NORM_PRIORITY; + private boolean _daemon=false; + private int _maxStopTime=100; + private boolean _detailedDump=false; + + /* ------------------------------------------------------------------- */ + /** Construct + */ + public QueuedThreadPool() + { + _name="qtp"+super.hashCode(); + } + + /* ------------------------------------------------------------ */ + @Override + protected void doStart() throws Exception + { + super.doStart(); + _threadsStarted.set(0); + + if (_jobs==null) + { + _jobs=_maxQueued>0 ?new ArrayBlockingQueue<Runnable>(_maxQueued) + :new BlockingArrayQueue<Runnable>(_minThreads,_minThreads); + } + + int threads=_threadsStarted.get(); + while (isRunning() && threads<_minThreads) + { + startThread(threads); + threads=_threadsStarted.get(); + } + } + + /* ------------------------------------------------------------ */ + @Override + protected void doStop() throws Exception + { + super.doStop(); + long start=System.currentTimeMillis(); + + // let jobs complete naturally for a while + while (_threadsStarted.get()>0 && (System.currentTimeMillis()-start) < (_maxStopTime/2)) + Thread.sleep(1); + + // kill queued jobs and flush out idle jobs + _jobs.clear(); + Runnable noop = new Runnable(){public void run(){}}; + for (int i=_threadsIdle.get();i-->0;) + _jobs.offer(noop); + Thread.yield(); + + // interrupt remaining threads + if (_threadsStarted.get()>0) + for (Thread thread : _threads) + thread.interrupt(); + + // wait for remaining threads to die + while (_threadsStarted.get()>0 && (System.currentTimeMillis()-start) < _maxStopTime) + { + Thread.sleep(1); + } + Thread.yield(); + int size=_threads.size(); + if (size>0) + { + LOG.warn(size+" threads could not be stopped"); + + if (size==1 || LOG.isDebugEnabled()) + { + for (Thread unstopped : _threads) + { + LOG.info("Couldn't stop "+unstopped); + for (StackTraceElement element : unstopped.getStackTrace()) + { + LOG.info(" at "+element); + } + } + } + } + + synchronized (_joinLock) + { + _joinLock.notifyAll(); + } + } + + /* ------------------------------------------------------------ */ + /** Set the maximum number of threads. + * Delegated to the named or anonymous Pool. + * @see #setMaxThreads + * @return maximum number of threads. + */ + public int getMaxThreads() + { + return _maxThreads; + } + + /* ------------------------------------------------------------ */ + /** Get the minimum number of threads. + * Delegated to the named or anonymous Pool. + * @see #setMinThreads + * @return minimum number of threads. + */ + public int getMinThreads() + { + return _minThreads; + } + + /* ------------------------------------------------------------ */ + public boolean dispatch(Runnable job) + { + if (isRunning()) + { + final int jobQ = _jobs.size(); + final int idle = getIdleThreads(); + if(_jobs.offer(job)) + { + // If we had no idle threads or the jobQ is greater than the idle threads + if (idle==0 || jobQ>idle) + { + int threads=_threadsStarted.get(); + if (threads<_maxThreads) + startThread(threads); + } + return true; + } + } + LOG.debug("Dispatched {} to stopped {}",job,this); + return false; + } + + /* ------------------------------------------------------------ */ + public void execute(Runnable job) + { + if (!dispatch(job)) + throw new RejectedExecutionException(); + } + + /* ------------------------------------------------------------ */ + /** + * @return The total number of threads currently in the pool + */ + public int getThreads() + { + return _threadsStarted.get(); + } + + /* ------------------------------------------------------------ */ + /** + * @return The number of idle threads in the pool + */ + public int getIdleThreads() + { + return _threadsIdle.get(); + } + + /* ------------------------------------------------------------ */ + private boolean startThread(int threads) + { + final int next=threads+1; + if (!_threadsStarted.compareAndSet(threads,next)) + return false; + + boolean started=false; + try + { + Thread thread=newThread(_runnable); + thread.setDaemon(_daemon); + thread.setPriority(_priority); + thread.setName(_name+"-"+thread.getId()); + _threads.add(thread); + + thread.start(); + started=true; + } + finally + { + if (!started) + _threadsStarted.decrementAndGet(); + } + return started; + } + + /* ------------------------------------------------------------ */ + protected Thread newThread(Runnable runnable) + { + return new Thread(runnable); + } + + /* ------------------------------------------------------------ */ + public void dump(Appendable out, String indent) throws IOException + { + List<Object> dump = new ArrayList<Object>(getMaxThreads()); + for (final Thread thread: _threads) + { + final StackTraceElement[] trace=thread.getStackTrace(); + boolean inIdleJobPoll=false; + // trace can be null on early java 6 jvms + if (trace != null) + { + for (StackTraceElement t : trace) + { + if ("idleJobPoll".equals(t.getMethodName())) + { + inIdleJobPoll = true; + break; + } + } + } + final boolean idle=inIdleJobPoll; + + if (_detailedDump) + { + dump.add(new Dumpable() + { + public void dump(Appendable out, String indent) throws IOException + { + out.append(String.valueOf(thread.getId())).append(' ').append(thread.getName()).append(' ').append(thread.getState().toString()).append(idle?" IDLE":"").append('\n'); + if (!idle) + AggregateLifeCycle.dump(out,indent,Arrays.asList(trace)); + } + }); + } + else + { + dump.add(thread.getId()+" "+thread.getName()+" "+thread.getState()+" @ "+(trace.length>0?trace[0]:"???")+(idle?" IDLE":"")); + } + } + + AggregateLifeCycle.dumpObject(out,this); + AggregateLifeCycle.dump(out,indent,dump); + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return _name+"{"+getMinThreads()+"<="+getIdleThreads()+"<="+getThreads()+"/"+getMaxThreads()+","+(_jobs==null?-1:_jobs.size())+"}"; + } + + /* ------------------------------------------------------------ */ + private Runnable idleJobPoll() throws InterruptedException + { + return _jobs.poll(_maxIdleTimeMs,TimeUnit.MILLISECONDS); + } + + /* ------------------------------------------------------------ */ + private Runnable _runnable = new Runnable() + { + public void run() + { + boolean shrink=false; + try + { + Runnable job=_jobs.poll(); + while (isRunning()) + { + // Job loop + while (job!=null && isRunning()) + { + runJob(job); + job=_jobs.poll(); + } + + // Idle loop + try + { + _threadsIdle.incrementAndGet(); + + while (isRunning() && job==null) + { + if (_maxIdleTimeMs<=0) + job=_jobs.take(); + else + { + // maybe we should shrink? + final int size=_threadsStarted.get(); + if (size>_minThreads) + { + long last=_lastShrink.get(); + long now=System.currentTimeMillis(); + if (last==0 || (now-last)>_maxIdleTimeMs) + { + shrink=_lastShrink.compareAndSet(last,now) && + _threadsStarted.compareAndSet(size,size-1); + if (shrink) + return; + } + } + job=idleJobPoll(); + } + } + } + finally + { + _threadsIdle.decrementAndGet(); + } + } + } + catch(InterruptedException e) + { + LOG.ignore(e); + } + catch(Exception e) + { + LOG.warn(e); + } + finally + { + if (!shrink) + _threadsStarted.decrementAndGet(); + _threads.remove(Thread.currentThread()); + } + } + }; + + /* ------------------------------------------------------------ */ + /** + * <p>Runs the given job in the {@link Thread#currentThread() current thread}.</p> + * <p>Subclasses may override to perform pre/post actions before/after the job is run.</p> + * + * @param job the job to run + */ + protected void runJob(Runnable job) + { + job.run(); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/ThreadPool.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/ThreadPool.java new file mode 100644 index 00000000..4bff2682 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/ThreadPool.java @@ -0,0 +1,42 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.thread; + +/* ------------------------------------------------------------ */ +/** ThreadPool. + * + * + */ +public interface ThreadPool +{ + /* ------------------------------------------------------------ */ + public abstract boolean dispatch(Runnable job); + + /* ------------------------------------------------------------ */ + /** + * @return The total number of threads currently in the pool + */ + public int getThreads(); + + /* ------------------------------------------------------------ */ + /** + * @return The number of idle threads in the pool + */ + public int getIdleThreads(); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/Timeout.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/Timeout.java new file mode 100644 index 00000000..d8086531 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/util/thread/Timeout.java @@ -0,0 +1,282 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.util.thread; + +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + + +/* ------------------------------------------------------------ */ +/** Timeout queue. + * This class implements a timeout queue for timers that are at least as likely to be cancelled as they are to expire. + * Unlike the util timeout class, the duration of the timeouts is shared by all scheduled tasks and if the duration + * is changed, this affects all scheduled tasks. + * <p> + * The nested class Task should be extended by users of this class to obtain call back notification of + * expires. + */ +public class Timeout +{ + private static final Logger LOG = Log.getLogger(Timeout.class); + private Object _lock; + private long _duration; + private volatile long _now=System.currentTimeMillis(); + private Task _head=new Task(); + + /* ------------------------------------------------------------ */ + public Timeout(Object lock) + { + _lock=lock; + _head._timeout=this; + } + + /* ------------------------------------------------------------ */ + /** + * @param duration The duration to set. + */ + public void setDuration(long duration) + { + _duration = duration; + } + + /* ------------------------------------------------------------ */ + public long getNow() + { + return _now; + } + + /* ------------------------------------------------------------ */ + public void setNow(long now) + { + _now=now; + } + + /* ------------------------------------------------------------ */ + /** Get an expired tasks. + * This is called instead of {@link #tick()} to obtain the next + * expired Task, but without calling it's {@link Task#expire()} or + * {@link Task#expired()} methods. + * + * @return the next expired task or null. + */ + public Task expired() + { + synchronized (_lock) + { + long _expiry = _now-_duration; + + if (_head._next!=_head) + { + Task task = _head._next; + if (task._timestamp>_expiry) + return null; + + task.unlink(); + task._expired=true; + return task; + } + return null; + } + } + + /* ------------------------------------------------------------ */ + public void tick() + { + final long expiry = _now-_duration; + + Task task=null; + while (true) + { + try + { + synchronized (_lock) + { + task= _head._next; + if (task==_head || task._timestamp>expiry) + break; + task.unlink(); + task._expired=true; + task.expire(); + } + + task.expired(); + } + catch(Throwable th) + { + LOG.warn(Log.EXCEPTION,th); + } + } + } + + /* ------------------------------------------------------------ */ + public void schedule(Task task) + { + schedule(task,0L); + } + + /* ------------------------------------------------------------ */ + /** + * @param task + * @param delay A delay in addition to the default duration of the timeout + */ + public void schedule(Task task,long delay) + { + synchronized (_lock) + { + if (task._timestamp!=0) + { + task.unlink(); + task._timestamp=0; + } + task._timeout=this; + task._expired=false; + task._delay=delay; + task._timestamp = _now+delay; + + Task last=_head._prev; + while (last!=_head) + { + if (last._timestamp <= task._timestamp) + break; + last=last._prev; + } + last.link(task); + } + } + + /* ------------------------------------------------------------ */ + public void cancelAll() + { + synchronized (_lock) + { + _head._next=_head._prev=_head; + } + } + + /* ------------------------------------------------------------ */ + public long getTimeToNext() + { + synchronized (_lock) + { + if (_head._next==_head) + return -1; + long to_next = _duration+_head._next._timestamp-_now; + return to_next<0?0:to_next; + } + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + StringBuffer buf = new StringBuffer(); + buf.append(super.toString()); + + Task task = _head._next; + while (task!=_head) + { + buf.append("-->"); + buf.append(task); + task=task._next; + } + + return buf.toString(); + } + + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /** Task. + * The base class for scheduled timeouts. This class should be + * extended to implement the expire() method, which is called if the + * timeout expires. + * + * + * + */ + public static class Task + { + Task _next; + Task _prev; + Timeout _timeout; + long _delay; + long _timestamp=0; + boolean _expired=false; + + /* ------------------------------------------------------------ */ + protected Task() + { + _next=_prev=this; + } + + /* ------------------------------------------------------------ */ + private void unlink() + { + _next._prev=_prev; + _prev._next=_next; + _next=_prev=this; + _expired=false; + } + + /* ------------------------------------------------------------ */ + private void link(Task task) + { + Task next_next = _next; + _next._prev=task; + _next=task; + _next._next=next_next; + _next._prev=this; + } + + /* ------------------------------------------------------------ */ + /** Cancel the task. + * Remove the task from the timeout. + */ + public void cancel() + { + Timeout timeout = _timeout; + if (timeout!=null) + { + synchronized (timeout._lock) + { + unlink(); + _timestamp=0; + } + } + } + + /* ------------------------------------------------------------ */ + /** Expire task. + * This method is called when the timeout expires. It is called + * in the scope of the synchronize block (on this) that sets + * the {@link #isExpired()} state to true. + * @see #expired() For an unsynchronized callback. + */ + protected void expire(){} + + /* ------------------------------------------------------------ */ + /** Expire task. + * This method is called when the timeout expires. It is called + * outside of any synchronization scope and may be delayed. + * + */ + public void expired(){} + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/Extension.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/Extension.java new file mode 100644 index 00000000..936aa5fb --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/Extension.java @@ -0,0 +1,31 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.util.Map; + +public interface Extension extends WebSocketParser.FrameHandler, WebSocketGenerator +{ + public String getName(); + public String getParameterizedName(); + + public boolean init(Map<String,String> parameters); + public void bind(WebSocket.FrameConnection connection, WebSocketParser.FrameHandler inbound, WebSocketGenerator outbound); + +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/MaskGen.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/MaskGen.java new file mode 100644 index 00000000..9178d906 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/MaskGen.java @@ -0,0 +1,24 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +public interface MaskGen +{ + void genMask(byte[] mask); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/PerMessageDeflateExtension.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/PerMessageDeflateExtension.java new file mode 100644 index 00000000..fdd11d93 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/PerMessageDeflateExtension.java @@ -0,0 +1,291 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.zip.DataFormatException; +import java.util.zip.Deflater; +import java.util.zip.Inflater; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.WebSocket.FrameConnection; +import org.eclipse.jetty.websocket.WebSocketParser.FrameHandler; + +import android.annotation.TargetApi; +import android.os.Build; +import android.text.TextUtils; + +/** + * PerMessageDeflateExtension + * + * Copyright (c) 2015 KNOWLEDGECODE + */ +public class PerMessageDeflateExtension implements Extension { + + private static final Logger __log = Log.getLogger(PerMessageDeflateExtension.class.getName()); + private static final byte[] TAIL_BYTES = new byte[] { 0x00, 0x00, (byte) 0xff, (byte) 0xff }; + private static final int INITIAL_CAPACITY = 8192; + private static final String EXTENSION = "permessage-deflate"; + private static final String CLIENT_NO_CONTEXT_TAKEOVER = "client_no_context_takeover"; + private static final String SERVER_NO_CONTEXT_TAKEOVER = "server_no_context_takeover"; + private static final String SERVER_MAX_WINDOW_BITS = "server_max_window_bits"; + + private static class Zlib { + protected Deflater _deflater; + protected Inflater _inflater; + protected WebSocketBuffer _buffer; + protected byte[] _buf; + protected int _capcacity; + protected boolean _deflaterReset; + protected boolean _inflaterReset; + + public Zlib(boolean deflaterReset, boolean inflaterReset) { + _deflater = new Deflater(Deflater.DEFAULT_COMPRESSION, true); + _inflater = new Inflater(true); + _buffer = new WebSocketBuffer(INITIAL_CAPACITY); + _buf = new byte[INITIAL_CAPACITY]; + _capcacity = INITIAL_CAPACITY; + _deflaterReset = deflaterReset; + _inflaterReset = inflaterReset; + } + + public byte[] compress(byte[] b, int offset, int length) { + int len; + int index = 0; + + _deflater.reset(); + _deflater.setInput(b, offset, length); + _deflater.finish(); + while ((len = _deflater.deflate(_buf, index, _capcacity - index)) > 0) { + if ((index += len) == _capcacity) { + _buf = Arrays.copyOf(_buf, _capcacity <<= 1); + } + } + return Arrays.copyOf(_buf, index); + } + + public Buffer decompress(byte[] b, int offset, int length) throws DataFormatException { + int len; + int index = 0; + + if (_inflaterReset) { + _inflater.reset(); + } + _inflater.setInput(b, offset, length); + while ((len = _inflater.inflate(_buf, index, _capcacity - index)) > 0) { + if ((index += len) == _capcacity) { + _buf = Arrays.copyOf(_buf, _capcacity <<= 1); + } + } + _buffer.clear(); + return _buffer.append(_buf, 0, index); + } + + public boolean isCompressible() { + return _deflaterReset; + } + + public void end() { + _deflater.end(); + _inflater.end(); + } + } + + @TargetApi(Build.VERSION_CODES.KITKAT) + private static class NewZlib extends Zlib { + public NewZlib(boolean deflaterReset, boolean inflaterReset) { + super(deflaterReset, inflaterReset); + } + + @Override + public byte[] compress(byte[] b, int offset, int length) { + int len; + int index = 0; + + if (_deflaterReset) { + _deflater.reset(); + } + _deflater.setInput(b, offset, length); + while ((len = _deflater.deflate(_buf, index, _capcacity - index, Deflater.SYNC_FLUSH)) > 0) { + if ((index += len) == _capcacity) { + _buf = Arrays.copyOf(_buf, _capcacity <<= 1); + } + } + return Arrays.copyOf(_buf, index - TAIL_BYTES.length); + } + + @Override + public boolean isCompressible() { + return true; + } + } + + private FrameConnection _connection; + private FrameHandler _inbound; + private WebSocketGenerator _outbound; + private Zlib _zlib; + private String _parameters; + private boolean _compressed; + + public PerMessageDeflateExtension() { + if (Build.VERSION.SDK_INT < 19) { + _parameters = TextUtils.join("; ", new String[]{ EXTENSION, CLIENT_NO_CONTEXT_TAKEOVER }); + } else { + _parameters = EXTENSION; + } + _compressed = false; + } + + @Override + public void onFrame(byte flags, byte opcode, Buffer buffer) { + switch (opcode) { + case WebSocketConnectionRFC6455.OP_TEXT: + case WebSocketConnectionRFC6455.OP_BINARY: + _compressed = ((flags & 0x07) == 0x04); + case WebSocketConnectionRFC6455.OP_CONTINUATION: + if (_compressed) { + try { + if ((flags &= 0x08) > 0) { + buffer.put(TAIL_BYTES); + } + _inbound.onFrame(flags, opcode, _zlib.decompress(buffer.array(), buffer.getIndex(), buffer.length())); + } catch (DataFormatException e) { + __log.warn(e); + _connection.close(WebSocketConnectionRFC6455.CLOSE_BAD_DATA, "Bad data"); + } + return; + } + } + _inbound.onFrame(flags, opcode, buffer); + } + + @Override + public void close(int code, String message) { + _zlib.end(); + } + + @Override + public int flush() throws IOException { + return 0; + } + + @Override + public boolean isBufferEmpty() { + return _outbound.isBufferEmpty(); + } + + @Override + public void addFrame(byte flags, byte opcode, byte[] content, int offset, int length) throws IOException { + if (opcode == WebSocketConnectionRFC6455.OP_TEXT || opcode == WebSocketConnectionRFC6455.OP_BINARY) { + if (_zlib.isCompressible()) { + byte[] compressed = _zlib.compress(content, offset, length); + _outbound.addFrame((byte) (flags | 0x04), opcode, compressed, 0, compressed.length); + return; + } + } + _outbound.addFrame(flags, opcode, content, offset, length); + } + + @Override + public String getName() { + return EXTENSION; + } + + @Override + public String getParameterizedName() { + return _parameters; + } + + @Override + public boolean init(Map<String, String> parameters) { + boolean extension = false; + boolean client_no_context_takeover = false; + boolean server_no_context_takeover = false; + int server_max_window_bits = 0; + + _parameters = ""; + for (String key : parameters.keySet()) { + String value = parameters.get(key); + + if (EXTENSION.equals(key) && TextUtils.isEmpty(value) && !extension) { + extension = true; + } else if (CLIENT_NO_CONTEXT_TAKEOVER.equals(key) && TextUtils.isEmpty(value) && !client_no_context_takeover) { + client_no_context_takeover = true; + } else if (SERVER_NO_CONTEXT_TAKEOVER.equals(key) && TextUtils.isEmpty(value) && !server_no_context_takeover) { + server_no_context_takeover = true; + } else if (SERVER_MAX_WINDOW_BITS.equals(key) && server_max_window_bits == 0) { + if (TextUtils.isEmpty(value)) { + __log.warn("Unexpected parameter: {}", key); + return false; + } + if (!value.matches("[1-9]\\d?")) { + __log.warn("Unexpected parameter: {}={}", key, value); + return false; + } + int v = Integer.parseInt(value); + if (v < 8 || v > 15) { + __log.warn("Unexpected parameter: {}={}", key, value); + return false; + } + server_max_window_bits = v; + } else if (!TextUtils.isEmpty(value)) { + __log.warn("Unexpected parameter: {}={}", key, value); + return false; + } else { + __log.warn("Unexpected parameter: {}", key); + return false; + } + } + if (extension) { + List<String> p = new ArrayList<String>(); + + p.add(EXTENSION); + if (client_no_context_takeover) { + p.add(CLIENT_NO_CONTEXT_TAKEOVER); + } + if (server_no_context_takeover) { + p.add(SERVER_NO_CONTEXT_TAKEOVER); + } + if (server_max_window_bits > 0) { + p.add(String.format("%s=%d", SERVER_MAX_WINDOW_BITS, server_max_window_bits)); + } + _parameters = TextUtils.join("; ", p); + + if (Build.VERSION.SDK_INT < 19) { + _zlib = new Zlib(client_no_context_takeover, server_no_context_takeover); + } else { + _zlib = new NewZlib(client_no_context_takeover, server_no_context_takeover); + } + } + return extension; + } + + @Override + public void bind(FrameConnection connection, FrameHandler inbound, WebSocketGenerator outbound) { + _connection = connection; + _inbound = inbound; + _outbound = outbound; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/RandomMaskGen.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/RandomMaskGen.java new file mode 100644 index 00000000..a180c60f --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/RandomMaskGen.java @@ -0,0 +1,45 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.util.Random; + + +public class RandomMaskGen implements MaskGen +{ + private final Random _random; + + public RandomMaskGen() + { + this(new Random()); + } + + public RandomMaskGen(Random random) + { + _random=random; + } + + public void genMask(byte[] mask) + { + // The assumption is that this code is always called + // with an external lock held to prevent concurrent access + // Otherwise we need to synchronize on the _random. + _random.nextBytes(mask); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocket.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocket.java new file mode 100644 index 00000000..4cf39df7 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocket.java @@ -0,0 +1,273 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; + +/** + * WebSocket Interface. + * <p> + * This interface provides the signature for a server-side end point of a websocket connection. + * The Interface has several nested interfaces, for each type of message that may be received. + */ +public interface WebSocket +{ + /** + * Called when a new websocket connection is accepted. + * @param connection The Connection object to use to send messages. + */ + void onOpen(Connection connection); + + /** + * Called when an established websocket connection closes + * @param closeCode + * @param message + */ + void onClose(int closeCode, String message); + + /** + * A nested WebSocket interface for receiving text messages + */ + interface OnTextMessage extends WebSocket + { + /** + * Called with a complete text message when all fragments have been received. + * The maximum size of text message that may be aggregated from multiple frames is set with {@link Connection#setMaxTextMessageSize(int)}. + * @param data The message + */ + void onMessage(String data); + } + + /** + * A nested WebSocket interface for receiving binary messages + */ + interface OnBinaryMessage extends WebSocket + { + /** + * Called with a complete binary message when all fragments have been received. + * The maximum size of binary message that may be aggregated from multiple frames is set with {@link Connection#setMaxBinaryMessageSize(int)}. + * @param data + * @param offset + * @param length + */ + void onMessage(byte[] data, int offset, int length); + } + + /** + * A nested WebSocket interface for receiving control messages + */ + interface OnControl extends WebSocket + { + /** + * Called when a control message has been received. + * @param controlCode + * @param data + * @param offset + * @param length + * @return true if this call has completely handled the control message and no further processing is needed. + */ + boolean onControl(byte controlCode,byte[] data, int offset, int length); + } + + /** + * A nested WebSocket interface for receiving any websocket frame + */ + interface OnFrame extends WebSocket + { + /** + * Called when any websocket frame is received. + * @param flags + * @param opcode + * @param data + * @param offset + * @param length + * @return true if this call has completely handled the frame and no further processing is needed (including aggregation and/or message delivery) + */ + boolean onFrame(byte flags,byte opcode,byte[] data, int offset, int length); + + void onHandshake(FrameConnection connection); + } + + /** + * A Connection interface is passed to a WebSocket instance via the {@link WebSocket#onOpen(Connection)} to + * give the application access to the specifics of the current connection. This includes methods + * for sending frames and messages as well as methods for interpreting the flags and opcodes of the connection. + * + * modified by KNOWLEDGECODE + */ + public interface Connection + { + String getProtocol(); + String getExtensions(); + void sendMessage(String data) throws IOException; + void sendMessage(byte[] data, int offset, int length) throws IOException; + + /** + * Close the connection with normal close code. + */ + void close(); + + /** Close the connection with specific closeCode and message. + * @param closeCode The close code to send, or -1 for no close code + * @param message The message to send or null for no message + */ + void close(int closeCode,String message); + + boolean isOpen(); + + /** + * @param ms The time in ms that the connection can be idle before closing + */ + void setMaxIdleTime(int ms); + + /** + * @param size size<0 No aggregation of frames to messages, >=0 max size of text frame aggregation buffer in characters + */ + void setMaxTextMessageSize(int size); + + /** + * @param size size<0 no aggregation of binary frames, >=0 size of binary frame aggregation buffer + */ + void setMaxBinaryMessageSize(int size); + + /** + * @return The time in ms that the connection can be idle before closing + */ + int getMaxIdleTime(); + + /** + * Size in characters of the maximum text message to be received + * @return size <0 No aggregation of frames to messages, >=0 max size of text frame aggregation buffer in characters + */ + int getMaxTextMessageSize(); + + /** + * Size in bytes of the maximum binary message to be received + * @return size <0 no aggregation of binary frames, >=0 size of binary frame aggregation buffer + */ + int getMaxBinaryMessageSize(); + } + + /** + * Frame Level Connection + * <p>The Connection interface at the level of sending/receiving frames rather than messages. + * Also contains methods to decode/generate flags and opcodes without using constants, so that + * code can be written to work with multiple drafts of the protocol. + * + */ + public interface FrameConnection extends Connection + { + /** + * @return The opcode of a binary message + */ + byte binaryOpcode(); + + /** + * @return The opcode of a text message + */ + byte textOpcode(); + + /** + * @return The opcode of a continuation frame + */ + byte continuationOpcode(); + + /** + * @return Mask for the FIN bit. + */ + byte finMask(); + + /** Set if frames larger than the frame buffer are handled with local fragmentations + * @param allowFragmentation + */ + void setAllowFrameFragmentation(boolean allowFragmentation); + + /** + * @param flags The flags bytes of a frame + * @return True of the flags indicate a final frame. + */ + boolean isMessageComplete(byte flags); + + /** + * @param opcode + * @return True if the opcode is for a control frame + */ + boolean isControl(byte opcode); + + /** + * @param opcode + * @return True if the opcode is for a text frame + */ + boolean isText(byte opcode); + + /** + * @param opcode + * @return True if the opcode is for a binary frame + */ + boolean isBinary(byte opcode); + + /** + * @param opcode + * @return True if the opcode is for a continuation frame + */ + boolean isContinuation(byte opcode); + + /** + * @param opcode + * @return True if the opcode is a close control + */ + boolean isClose(byte opcode); + + /** + * @param opcode + * @return True if the opcode is a ping control + */ + boolean isPing(byte opcode); + + /** + * @param opcode + * @return True if the opcode is a pong control + */ + boolean isPong(byte opcode); + + /** + * @return True if frames larger than the frame buffer are fragmented. + */ + boolean isAllowFrameFragmentation(); + + /** Send a control frame + * @param control + * @param data + * @param offset + * @param length + * @throws IOException + */ + void sendControl(byte control,byte[] data, int offset, int length) throws IOException; + + /** Send an arbitrary frame + * @param flags + * @param opcode + * @param data + * @param offset + * @param length + * @throws IOException + */ + void sendFrame(byte flags,byte opcode,byte[] data, int offset, int length) throws IOException; + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketBuffer.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketBuffer.java new file mode 100644 index 00000000..ad89312b --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketBuffer.java @@ -0,0 +1,244 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.eclipse.jetty.websocket; + +import java.nio.charset.Charset; +import java.util.Arrays; + +import org.eclipse.jetty.io.Buffer; + +/** + * WebSocketBuffer + * + * Copyright (c) 2015 KNOWLEDGECODE + */ +class WebSocketBuffer implements Buffer { + + private byte[] _buffer; + private int _index; + private int _capacity; + + public WebSocketBuffer(final int capacity) { + _buffer = new byte[capacity]; + _index = 0; + _capacity = capacity; + } + + public WebSocketBuffer(final byte[] buffer, final int offset, final int length) { + _buffer = Arrays.copyOfRange(buffer, offset, length); + _index = length; + _capacity = length; + } + + public WebSocketBuffer append(final byte[] array, final int offset, final int length) { + if ((_index += length) > _capacity) { + _buffer = Arrays.copyOf(_buffer, (_capacity = Math.max(_capacity << 1, _index))); + } + System.arraycopy(array, offset, _buffer, _index - length, length); + return this; + } + + @Override + public byte[] array() { + return _buffer; + } + + @Override + public byte[] asArray() { + return Arrays.copyOf(_buffer, _index); + } + + @Override + public Buffer buffer() { + return null; + } + + @Override + public Buffer asMutableBuffer() { + return null; + } + + @Override + public int capacity() { + return _capacity; + } + + @Override + public int space() { + return _capacity - _index; + } + + @Override + public void clear() { + _index = 0; + } + + @Override + public void compact() { + } + + @Override + public byte get() { + return 0; + } + + @Override + public int get(byte[] b, int offset, int length) { + return 0; + } + + @Override + public Buffer get(int length) { + return null; + } + + @Override + public int getIndex() { + return 0; + } + + @Override + public boolean hasContent() { + return false; + } + + @Override + public boolean equalsIgnoreCase(Buffer buffer) { + return false; + } + + @Override + public boolean isImmutable() { + return false; + } + + @Override + public boolean isReadOnly() { + return false; + } + + @Override + public boolean isVolatile() { + return false; + } + + @Override + public int length() { + return _index; + } + + @Override + public void mark() { + } + + @Override + public int markIndex() { + return 0; + } + + @Override + public byte peek() { + return 0; + } + + @Override + public byte peek(int index) { + return 0; + } + + @Override + public int peek(int index, byte[] b, int offset, int length) { + return 0; + } + + @Override + public int poke(int index, Buffer src) { + return 0; + } + + @Override + public void poke(int index, byte b) { + } + + @Override + public int poke(int index, byte[] b, int offset, int length) { + return 0; + } + + @Override + public int put(Buffer src) { + return 0; + } + + @Override + public void put(byte b) { + } + + @Override + public int put(byte[] b, int offset, int length) { + return 0; + } + + @Override + public int put(byte[] b) { + return 0; + } + + @Override + public int putIndex() { + return 0; + } + + @Override + public void setGetIndex(int newStart) { + } + + @Override + public void setMarkIndex(int newMark) { + } + + @Override + public void setPutIndex(int newLimit) { + } + + @Override + public int skip(int n) { + return n; + } + + @Override + public Buffer sliceFromMark() { + return null; + } + + @Override + public Buffer sliceFromMark(int length) { + return null; + } + + @Override + public String toDetailString() { + return null; + } + + @Override + public String toString(Charset charset) { + return new String(_buffer, 0, _index, charset); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketBuffers.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketBuffers.java new file mode 100644 index 00000000..97dcb135 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketBuffers.java @@ -0,0 +1,57 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.Buffers.Type; +import org.eclipse.jetty.io.ThreadLocalBuffers; + +/* ------------------------------------------------------------ */ +/** The WebSocket Buffer Pool. + * + * The normal buffers are byte array buffers so that user processes + * can access directly. However the generator uses direct buffers + * for the final output stage as they are filled in bulk and are more + * efficient to flush. + */ +public class WebSocketBuffers +{ + final private Buffers _buffers; + + public WebSocketBuffers(final int bufferSize) + { + _buffers = new ThreadLocalBuffers(Type.DIRECT, bufferSize, Type.INDIRECT, bufferSize, Type.INDIRECT); + } + + public Buffer getBuffer() + { + return _buffers.getBuffer(); + } + + public Buffer getDirectBuffer() + { + return _buffers.getHeader(); + } + + public void returnBuffer(Buffer buffer) + { + _buffers.returnBuffer(buffer); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketClient.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketClient.java new file mode 100644 index 00000000..dc6fcf21 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketClient.java @@ -0,0 +1,611 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ProtocolException; +import java.net.SocketAddress; +import java.net.URI; +import java.nio.channels.ByteChannel; +import java.nio.channels.SocketChannel; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +import org.eclipse.jetty.util.IO; +import org.eclipse.jetty.util.log.Logger; + + +/* ------------------------------------------------------------ */ +/** + * <p>{@link WebSocketClient} allows to create multiple connections to multiple destinations + * that can speak the websocket protocol.</p> + * <p>When creating websocket connections, {@link WebSocketClient} accepts a {@link WebSocket} + * object (to receive events from the server), and returns a {@link WebSocket.Connection} to + * send data to the server.</p> + * <p>Example usage is as follows:</p> + * <pre> + * WebSocketClientFactory factory = new WebSocketClientFactory(); + * factory.start(); + * + * WebSocketClient client = factory.newWebSocketClient(); + * // Configure the client + * + * WebSocket.Connection connection = client.open(new URI("ws://127.0.0.1:8080/"), new WebSocket.OnTextMessage() + * { + * public void onOpen(Connection connection) + * { + * // open notification + * } + * + * public void onClose(int closeCode, String message) + * { + * // close notification + * } + * + * public void onMessage(String data) + * { + * // handle incoming message + * } + * }).get(5, TimeUnit.SECONDS); + * + * connection.sendMessage("Hello World"); + * </pre> + * + * modified by KNOWLEDGECODE + */ +public class WebSocketClient +{ + private final static Logger __log = org.eclipse.jetty.util.log.Log.getLogger(WebSocketClient.class.getName()); + + private final WebSocketClientFactory _factory; + private final Map<String,String> _cookies=new ConcurrentHashMap<String, String>(); + private final List<Extension> _extensions=new CopyOnWriteArrayList<Extension>(); + private String _origin; + private String _protocol; + private String _agent; + private int _maxIdleTime=-1; + private int _maxTextMessageSize=-1; + private int _maxBinaryMessageSize=-1; + private MaskGen _maskGen; + private SocketAddress _bindAddress; + + /* ------------------------------------------------------------ */ + /** + * <p>Creates a WebSocketClient with shared WebSocketClientFactory.</p> + * + * @param factory the shared {@link WebSocketClientFactory} + */ + public WebSocketClient(WebSocketClientFactory factory) + { + _factory=factory; + _maskGen=_factory.getMaskGen(); + } + + /* ------------------------------------------------------------ */ + /** + * @return The WebSocketClientFactory this client was created with. + */ + public WebSocketClientFactory getFactory() + { + return _factory; + } + + /* ------------------------------------------------------------ */ + /** + * @return The maxIdleTime in ms for connections opened by this client, + * or -1 if the default from {@link WebSocketClientFactory#getSelectorManager()} is used. + * @see #setMaxIdleTime(int) + */ + public int getMaxIdleTime() + { + return _maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @param maxIdleTime The max idle time in ms for connections opened by this client + * @see #getMaxIdleTime() + */ + public void setMaxIdleTime(int maxIdleTime) + { + _maxIdleTime=maxIdleTime; + } + + /* ------------------------------------------------------------ */ + /** + * @return The subprotocol string for connections opened by this client. + * @see #setProtocol(String) + */ + public String getProtocol() + { + return _protocol; + } + + /* ------------------------------------------------------------ */ + /** + * @param protocol The subprotocol string for connections opened by this client. + * @see #getProtocol() + */ + public void setProtocol(String protocol) + { + _protocol = protocol; + } + + /* ------------------------------------------------------------ */ + /** + * @return The origin URI of the client + * @see #setOrigin(String) + */ + public String getOrigin() + { + return _origin; + } + + /* ------------------------------------------------------------ */ + /** + * @param origin The origin URI of the client (eg "http://example.com") + * @see #getOrigin() + */ + public void setOrigin(String origin) + { + _origin = origin; + } + + /* ------------------------------------------------------------ */ + /** + * <p>Returns the map of the cookies that are sent during the initial HTTP handshake + * that upgrades to the websocket protocol.</p> + * @return The read-write cookie map + */ + public Map<String,String> getCookies() + { + return _cookies; + } + + /* ------------------------------------------------------------ */ + /** + * @return The list of websocket protocol extensions + */ + public List<Extension> getExtensions() + { + return _extensions; + } + + /* ------------------------------------------------------------ */ + /** + * @return the mask generator to use, or null if not mask generator should be used + * @see #setMaskGen(MaskGen) + */ + public MaskGen getMaskGen() + { + return _maskGen; + } + + /* ------------------------------------------------------------ */ + /** + * @param maskGen the mask generator to use, or null if not mask generator should be used + * @see #getMaskGen() + */ + public void setMaskGen(MaskGen maskGen) + { + _maskGen = maskGen; + } + + /* ------------------------------------------------------------ */ + /** + * @return The initial maximum text message size (in characters) for a connection + */ + public int getMaxTextMessageSize() + { + return _maxTextMessageSize; + } + + /* ------------------------------------------------------------ */ + /** + * Set the initial maximum text message size for a connection. This can be changed by + * the application calling {@link WebSocket.Connection#setMaxTextMessageSize(int)}. + * @param maxTextMessageSize The default maximum text message size (in characters) for a connection + */ + public void setMaxTextMessageSize(int maxTextMessageSize) + { + _maxTextMessageSize = maxTextMessageSize; + } + + /* ------------------------------------------------------------ */ + /** + * @return The initial maximum binary message size (in bytes) for a connection + */ + public int getMaxBinaryMessageSize() + { + return _maxBinaryMessageSize; + } + + /* ------------------------------------------------------------ */ + /** + * Set the initial maximum binary message size for a connection. This can be changed by + * the application calling {@link WebSocket.Connection#setMaxBinaryMessageSize(int)}. + * @param maxBinaryMessageSize The default maximum binary message size (in bytes) for a connection + */ + public void setMaxBinaryMessageSize(int maxBinaryMessageSize) + { + _maxBinaryMessageSize = maxBinaryMessageSize; + } + + /* ------------------------------------------------------------ */ + /** + * @return user-agent + */ + public String getAgent() + { + return _agent; + } + + /* ------------------------------------------------------------ */ + /** + * @param user-agent + */ + public void setAgent(String _agent) + { + this._agent = _agent; + } + + /* ------------------------------------------------------------ */ + /** + * <p>Opens a websocket connection to the URI and blocks until the connection is accepted or there is an error.</p> + * + * @param uri The URI to connect to. + * @param websocket The {@link WebSocket} instance to handle incoming events. + * @param maxConnectTime The interval to wait for a successful connection + * @param units the units of the maxConnectTime + * @return A {@link WebSocket.Connection} + * @throws IOException if the connection fails + * @throws InterruptedException if the thread is interrupted + * @throws TimeoutException if the timeout elapses before the connection is completed + * @see #open(URI, WebSocket) + */ + public WebSocket.Connection open(URI uri, WebSocket websocket,long maxConnectTime,TimeUnit units) throws IOException, InterruptedException, TimeoutException + { + try + { + return open(uri,websocket).get(maxConnectTime,units); + } + catch (ExecutionException e) + { + Throwable cause = e.getCause(); + if (cause instanceof IOException) + throw (IOException)cause; + if (cause instanceof Error) + throw (Error)cause; + if (cause instanceof RuntimeException) + throw (RuntimeException)cause; + throw new RuntimeException(cause); + } + } + + /* ------------------------------------------------------------ */ + /** + * <p>Asynchronously opens a websocket connection and returns a {@link Future} to obtain the connection.</p> + * <p>The caller must call {@link Future#get(long, TimeUnit)} if they wish to impose a connect timeout on the open.</p> + * + * @param uri The URI to connect to. + * @param websocket The {@link WebSocket} instance to handle incoming events. + * @return A {@link Future} to the {@link WebSocket.Connection} + * @throws IOException if the connection fails + * @see #open(URI, WebSocket, long, TimeUnit) + */ + public Future<WebSocket.Connection> open(URI uri, WebSocket websocket) throws IOException + { + if (!_factory.isStarted()) + throw new IllegalStateException("Factory !started"); + + InetSocketAddress address = toSocketAddress(uri); + + SocketChannel channel = null; + try + { + channel = SocketChannel.open(); + if (_bindAddress != null) + channel.socket().bind(_bindAddress); + channel.socket().setTcpNoDelay(true); + + WebSocketFuture holder = new WebSocketFuture(websocket,uri,this,channel); + + channel.configureBlocking(false); + channel.connect(address); + _factory.getSelectorManager().register(channel,holder); + + return holder; + } + catch (RuntimeException e) + { + // close the channel (prevent connection leak) + IO.close(channel); + + // rethrow + throw e; + } + catch(IOException e) + { + // close the channel (prevent connection leak) + IO.close(channel); + + // rethrow + throw e; + } + } + + public static InetSocketAddress toSocketAddress(URI uri) + { + String scheme = uri.getScheme(); + if (!("ws".equalsIgnoreCase(scheme) || "wss".equalsIgnoreCase(scheme))) + throw new IllegalArgumentException("Bad WebSocket scheme: " + scheme); + int port = uri.getPort(); + if (port == 0) + throw new IllegalArgumentException("Bad WebSocket port: " + port); + if (port < 0) + port = "ws".equals(scheme) ? 80 : 443; + + return new InetSocketAddress(uri.getHost(), port); + } + + /* ------------------------------------------------------------ */ + /** The Future Websocket Connection. + */ + static class WebSocketFuture implements Future<WebSocket.Connection> + { + final WebSocket _websocket; + final URI _uri; + final WebSocketClient _client; + final CountDownLatch _done = new CountDownLatch(1); + ByteChannel _channel; + WebSocketConnection _connection; + Throwable _exception; + + private WebSocketFuture(WebSocket websocket, URI uri, WebSocketClient client, ByteChannel channel) + { + _websocket=websocket; + _uri=uri; + _client=client; + _channel=channel; + } + + public void onConnection(WebSocketConnection connection) + { + try + { + _client.getFactory().addConnection(connection); + + connection.getConnection().setMaxTextMessageSize(_client.getMaxTextMessageSize()); + connection.getConnection().setMaxBinaryMessageSize(_client.getMaxBinaryMessageSize()); + + WebSocketConnection con; + synchronized (this) + { + if (_channel!=null) + _connection=connection; + con=_connection; + } + + if (con!=null) + { + if (_websocket instanceof WebSocket.OnFrame) + ((WebSocket.OnFrame)_websocket).onHandshake((WebSocket.FrameConnection)con.getConnection()); + + _websocket.onOpen(con.getConnection()); + } + } + finally + { + _done.countDown(); + } + } + + public void handshakeFailed(Throwable ex) + { + try + { + ByteChannel channel=null; + synchronized (this) + { + if (_channel!=null) + { + channel=_channel; + _channel=null; + _exception=ex; + } + } + + if (channel!=null) + { + if (ex instanceof ProtocolException) + closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_PROTOCOL,ex.getMessage()); + else + closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,ex.getMessage()); + } + } + finally + { + _done.countDown(); + } + } + + public Map<String,String> getCookies() + { + return _client.getCookies(); + } + + public String getProtocol() + { + return _client.getProtocol(); + } + + public WebSocket getWebSocket() + { + return _websocket; + } + + public URI getURI() + { + return _uri; + } + + public int getMaxIdleTime() + { + return _client.getMaxIdleTime(); + } + + public String getOrigin() + { + return _client.getOrigin(); + } + + public MaskGen getMaskGen() + { + return _client.getMaskGen(); + } + + public String getAgent() + { + return _client.getAgent(); + } + + public List<Extension> getExtensions() { + return _client.getExtensions(); + } + + @Override + public String toString() + { + return "[" + _uri + ","+_websocket+"]@"+hashCode(); + } + + public boolean cancel(boolean mayInterruptIfRunning) + { + try + { + ByteChannel channel=null; + synchronized (this) + { + if (_connection==null && _exception==null && _channel!=null) + { + channel=_channel; + _channel=null; + } + } + + if (channel!=null) + { + closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"cancelled"); + return true; + } + return false; + } + finally + { + _done.countDown(); + } + } + + public boolean isCancelled() + { + synchronized (this) + { + return _channel==null && _connection==null; + } + } + + public boolean isDone() + { + synchronized (this) + { + return _connection!=null && _exception==null; + } + } + + public org.eclipse.jetty.websocket.WebSocket.Connection get() throws InterruptedException, ExecutionException + { + try + { + return get(Long.MAX_VALUE,TimeUnit.SECONDS); + } + catch(TimeoutException e) + { + throw new IllegalStateException("The universe has ended",e); + } + } + + public org.eclipse.jetty.websocket.WebSocket.Connection get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, + TimeoutException + { + _done.await(timeout,unit); + ByteChannel channel=null; + org.eclipse.jetty.websocket.WebSocket.Connection connection=null; + Throwable exception; + synchronized (this) + { + exception=_exception; + if (_connection==null) + { + exception=_exception; + channel=_channel; + _channel=null; + } + else + connection=_connection.getConnection(); + } + + if (channel!=null) + closeChannel(channel,WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"timeout"); + if (exception!=null) + throw new ExecutionException(exception); + if (connection!=null) + return connection; + throw new TimeoutException(); + } + + private void closeChannel(ByteChannel channel,int code, String message) + { + try + { + _websocket.onClose(code,message); + } + catch(Exception e) + { + __log.warn(e); + } + + try + { + channel.close(); + } + catch(IOException e) + { + __log.debug(e); + } + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketClientFactory.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketClientFactory.java new file mode 100644 index 00000000..f4d4549d --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketClientFactory.java @@ -0,0 +1,634 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.EOFException; +import java.io.IOException; +import java.net.ProtocolException; +import java.nio.channels.SelectionKey; +import java.nio.channels.SocketChannel; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Queue; +import java.util.Random; +import java.util.Set; +import java.util.concurrent.ConcurrentLinkedQueue; + +import javax.net.ssl.SSLEngine; + +import org.eclipse.jetty.http.HttpParser; +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.ByteArrayBuffer; +import org.eclipse.jetty.io.ConnectedEndPoint; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.SimpleBuffers; +import org.eclipse.jetty.io.nio.AsyncConnection; +import org.eclipse.jetty.io.nio.SelectChannelEndPoint; +import org.eclipse.jetty.io.nio.SelectorManager; +import org.eclipse.jetty.io.nio.SslConnection; +import org.eclipse.jetty.util.QuotedStringTokenizer; +import org.eclipse.jetty.util.component.AggregateLifeCycle; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.util.ssl.SslContextFactory; +import org.eclipse.jetty.util.thread.QueuedThreadPool; +import org.eclipse.jetty.util.thread.ThreadPool; + +import android.text.TextUtils; +import android.util.Base64; + +/* ------------------------------------------------------------ */ +/** + * <p>WebSocketClientFactory contains the common components needed by multiple {@link WebSocketClient} instances + * (for example, a {@link ThreadPool}, a {@link SelectorManager NIO selector}, etc).</p> + * <p>WebSocketClients with different configurations should share the same factory to avoid to waste resources.</p> + * <p>If a ThreadPool or MaskGen is passed in the constructor, then it is not added with {@link AggregateLifeCycle#addBean(Object)}, + * so it's lifecycle must be controlled externally. + * + * @see WebSocketClient + * + * modified by KNOWLEDGECODE + */ +public class WebSocketClientFactory extends AggregateLifeCycle +{ + private static final Logger __log = org.eclipse.jetty.util.log.Log.getLogger(WebSocketClientFactory.class.getName()); + private static final ByteArrayBuffer __ACCEPT = new ByteArrayBuffer.CaseInsensitive("Sec-WebSocket-Accept"); + private static final ByteArrayBuffer __PROTOCOL = new ByteArrayBuffer.CaseInsensitive("Sec-WebSocket-Protocol"); + private static final ByteArrayBuffer __EXTENSIONS = new ByteArrayBuffer.CaseInsensitive("Sec-WebSocket-Extensions"); + private final Queue<WebSocketConnection> connections = new ConcurrentLinkedQueue<WebSocketConnection>(); + private final SslContextFactory _sslContextFactory = new SslContextFactory(); + private final ThreadPool _threadPool; + private final WebSocketClientSelector _selector; + private MaskGen _maskGen; + private WebSocketBuffers _buffers; + + /* ------------------------------------------------------------ */ + /** + * <p>Creates a WebSocketClientFactory with the default configuration.</p> + */ + public WebSocketClientFactory() + { + this(null); + } + + /* ------------------------------------------------------------ */ + /** + * <p>Creates a WebSocketClientFactory with the given ThreadPool and the default configuration.</p> + * + * @param threadPool the ThreadPool instance to use + */ + public WebSocketClientFactory(ThreadPool threadPool) + { + this(threadPool, new RandomMaskGen()); + } + + /* ------------------------------------------------------------ */ + /** + * <p>Creates a WebSocketClientFactory with the given ThreadPool and the given MaskGen.</p> + * + * @param threadPool the ThreadPool instance to use + * @param maskGen the MaskGen instance to use + */ + public WebSocketClientFactory(ThreadPool threadPool, MaskGen maskGen) + { + this(threadPool, maskGen, 8192); + } + + /* ------------------------------------------------------------ */ + + /** + * <p>Creates a WebSocketClientFactory with the specified configuration.</p> + * + * @param threadPool the ThreadPool instance to use + * @param maskGen the mask generator to use + * @param bufferSize the read buffer size + */ + public WebSocketClientFactory(ThreadPool threadPool, MaskGen maskGen, int bufferSize) + { + if (threadPool == null) + threadPool = new QueuedThreadPool(); + _threadPool = threadPool; + addBean(_threadPool); + + _buffers = new WebSocketBuffers(bufferSize); + addBean(_buffers); + + _maskGen = maskGen; + addBean(_maskGen); + + _selector = new WebSocketClientSelector(); + addBean(_selector); + + addBean(_sslContextFactory); + } + + /* ------------------------------------------------------------ */ + /** + * @return the SslContextFactory used to configure SSL parameters + */ + public SslContextFactory getSslContextFactory() + { + return _sslContextFactory; + } + + /* ------------------------------------------------------------ */ + /** + * Get the selectorManager. Used to configure the manager. + * + * @return The {@link SelectorManager} instance. + */ + public SelectorManager getSelectorManager() + { + return _selector; + } + + /* ------------------------------------------------------------ */ + /** + * Get the ThreadPool. + * Used to set/query the thread pool configuration. + * + * @return The {@link ThreadPool} + */ + public ThreadPool getThreadPool() + { + return _threadPool; + } + + /* ------------------------------------------------------------ */ + /** + * @return the shared mask generator, or null if no shared mask generator is used + * @see WebSocketClient#getMaskGen() + */ + public MaskGen getMaskGen() + { + return _maskGen; + } + + @Override + protected void doStop() throws Exception + { + closeConnections(); + super.doStop(); + } + + /* ------------------------------------------------------------ */ + /** + * <p>Creates and returns a new instance of a {@link WebSocketClient}, configured with this + * WebSocketClientFactory instance.</p> + * + * @return a new {@link WebSocketClient} instance + */ + public WebSocketClient newWebSocketClient() + { + return new WebSocketClient(this); + } + + protected SSLEngine newSslEngine(SocketChannel channel) throws IOException + { + SSLEngine sslEngine; + if (channel != null) + { + String peerHost = channel.socket().getInetAddress().getHostAddress(); + int peerPort = channel.socket().getPort(); + sslEngine = _sslContextFactory.newSslEngine(peerHost, peerPort); + } + else + { + sslEngine = _sslContextFactory.newSslEngine(); + } + sslEngine.setUseClientMode(true); + sslEngine.beginHandshake(); + + return sslEngine; + } + + protected boolean addConnection(WebSocketConnection connection) + { + return isRunning() && connections.add(connection); + } + + protected boolean removeConnection(WebSocketConnection connection) + { + return connections.remove(connection); + } + + protected void closeConnections() + { + for (WebSocketConnection connection : connections) + connection.shutdown(); + } + + /* ------------------------------------------------------------ */ + /** + * WebSocket Client Selector Manager + */ + class WebSocketClientSelector extends SelectorManager + { + @Override + public boolean dispatch(Runnable task) + { + return _threadPool.dispatch(task); + } + + @Override + protected SelectChannelEndPoint newEndPoint(SocketChannel channel, SelectSet selectSet, final SelectionKey key) throws IOException + { + WebSocketClient.WebSocketFuture holder = (WebSocketClient.WebSocketFuture)key.attachment(); + int maxIdleTime = holder.getMaxIdleTime(); + if (maxIdleTime < 0) + maxIdleTime = (int)getMaxIdleTime(); + SelectChannelEndPoint result = new SelectChannelEndPoint(channel, selectSet, key, maxIdleTime); + AsyncEndPoint endPoint = result; + + // Detect if it is SSL, and wrap the connection if so + if ("wss".equals(holder.getURI().getScheme())) + { + SSLEngine sslEngine = newSslEngine(channel); + SslConnection sslConnection = new SslConnection(sslEngine, endPoint); + endPoint.setConnection(sslConnection); + endPoint = sslConnection.getSslEndPoint(); + } + + AsyncConnection connection = selectSet.getManager().newConnection(channel, endPoint, holder); + endPoint.setConnection(connection); + + return result; + } + + @Override + public AsyncConnection newConnection(SocketChannel channel, AsyncEndPoint endpoint, Object attachment) + { + WebSocketClient.WebSocketFuture holder = (WebSocketClient.WebSocketFuture)attachment; + return new HandshakeConnection(endpoint, holder); + } + + @Override + protected void endPointOpened(SelectChannelEndPoint endpoint) + { + // TODO expose on outer class ?? + } + + @Override + protected void endPointUpgraded(ConnectedEndPoint endpoint, Connection oldConnection) + { + LOG.debug("upgrade {} -> {}", oldConnection, endpoint.getConnection()); + } + + @Override + protected void endPointClosed(SelectChannelEndPoint endpoint) + { + endpoint.getConnection().onClose(); + } + + @Override + protected void connectionFailed(SocketChannel channel, Throwable ex, Object attachment) + { + if (!(attachment instanceof WebSocketClient.WebSocketFuture)) + super.connectionFailed(channel, ex, attachment); + else + { + __log.debug(ex); + WebSocketClient.WebSocketFuture future = (WebSocketClient.WebSocketFuture)attachment; + + future.handshakeFailed(ex); + } + } + } + + /* ------------------------------------------------------------ */ + /** + * Handshake Connection. + * Handles the connection until the handshake succeeds or fails. + */ + class HandshakeConnection extends AbstractConnection implements AsyncConnection + { + private static final String __COOKIE_DELIM="\"\\\n\r\t\f\b%+ ;="; + + private final AsyncEndPoint _endp; + private final WebSocketClient.WebSocketFuture _future; + private final String _key; + private final HttpParser _parser; + private String _accept; + private String _protocol; + private List<String> _extensions; + private String _error; + private ByteArrayBuffer _handshake; + + public HandshakeConnection(AsyncEndPoint endpoint, WebSocketClient.WebSocketFuture future) + { + super(endpoint, System.currentTimeMillis()); + _endp = endpoint; + _future = future; + + byte[] bytes = new byte[16]; + new Random().nextBytes(bytes); + _key = new String(Base64.encodeToString(bytes, Base64.NO_WRAP)); + _extensions = new ArrayList<String>(); + + Buffers buffers = new SimpleBuffers(_buffers.getBuffer(), null); + _parser = new HttpParser(buffers, _endp, new HttpParser.EventHandler() + { + @Override + public void startResponse(Buffer version, int status, Buffer reason) throws IOException + { + if (status != 101) + { + _error = "Bad response status " + status + " " + reason; + _endp.close(); + } + } + + @Override + public void parsedHeader(Buffer name, Buffer value) throws IOException + { + if (__ACCEPT.equals(name)) + _accept = value.toString(); + else if (__PROTOCOL.equals(name)) + _protocol = value.toString(); + else if (__EXTENSIONS.equals(name)) + for (String s : TextUtils.split(value.toString(), ",")) + _extensions.add(s.trim()); + } + + @Override // TODO simone says shouldn't be needed + public void startRequest(Buffer method, Buffer url, Buffer version) throws IOException + { + if (_error == null) + _error = "Bad response: " + method + " " + url + " " + version; + _endp.close(); + } + + @Override // TODO simone says shouldn't be needed + public void content(Buffer ref) throws IOException + { + if (_error == null) + _error = "Bad response. " + ref.length() + "B of content?"; + _endp.close(); + } + }); + } + + private boolean initExtensions(List<Extension> extensions) + { + Map<String, Map<String, String>> map = new HashMap<String, Map<String, String>>(); + Set<String> s = new HashSet<String>(); + + for (String ext : (String[]) _extensions.toArray(new String[]{})) + { + Map<String, String> m = new HashMap<String, String>(); + String key = null; + + for (String p : TextUtils.split(ext, ";")) + { + String[] pp = TextUtils.split(p.trim(), "="); + m.put(pp[0].trim(), pp.length > 1 ? pp[1].trim() : ""); + if (key == null) + key = pp[0].trim(); + if (!s.add(pp[0].trim())) + return false; + } + map.put(key, m); + } + for (Extension extension : extensions) + { + Map<String, String> m = map.remove(extension.getName()); + if (m != null) + { + if (!extension.init(m)) + return false; + } + else + { + extensions.remove(extension); + } + } + if (map.size() > 0) + return false; + + return true; + } + + private boolean handshake() + { + if (_handshake==null) + { + String path = _future.getURI().getPath(); + if (path == null || path.length() == 0) + path = "/"; + + if (_future.getURI().getRawQuery() != null) + path += "?" + _future.getURI().getRawQuery(); + + String origin = _future.getOrigin(); + + StringBuilder request = new StringBuilder(512); + request.append("GET ").append(path).append(" HTTP/1.1\r\n") + .append("Host: ").append(_future.getURI().getHost()).append(":"); + + int port = _future.getURI().getPort(); + if (port <= 0) + { + // fix it + String scheme = _future.getURI().getScheme(); + + if ("ws".equalsIgnoreCase(scheme)) + { + port = 80; + } + else if ("wss".equalsIgnoreCase(scheme)) + { + port = 443; + } + else + { + throw new RuntimeException("No valid port provided for scheme [" + scheme + "]"); + } + } + + request.append(port).append("\r\n"); + + request.append("Upgrade: websocket\r\n") + .append("Connection: Upgrade\r\n"); + + if (origin != null) + request.append("Origin: ").append(origin).append("\r\n"); + + if (_future.getProtocol() != null) + request.append("Sec-WebSocket-Protocol: ").append(_future.getProtocol()).append("\r\n"); + + request.append("pragma: no-cache\r\n"); + + request.append("cache-control: no-cache\r\n"); + + request.append("Sec-WebSocket-Key: ").append(_key).append("\r\n"); + + request.append("Sec-WebSocket-Version: ").append(WebSocketConnectionRFC6455.VERSION).append("\r\n"); + + if (_future.getAgent() != null) + request.append("user-agent: ").append(_future.getAgent()).append("\r\n"); + + if (_future.getExtensions().size() > 0) + { + List<String> extensions = new ArrayList<String>(); + for (Extension ext :_future.getExtensions()) + extensions.add(ext.getParameterizedName()); + request.append("Sec-WebSocket-Extensions: ").append(TextUtils.join(", ", extensions)).append("\r\n"); + } + + Map<String, String> cookies = _future.getCookies(); + if (cookies != null && cookies.size() > 0) + { + for (String cookie : cookies.keySet()) + request.append("Cookie: ") + .append(QuotedStringTokenizer.quoteIfNeeded(cookie, __COOKIE_DELIM)) + .append("=") + .append(QuotedStringTokenizer.quoteIfNeeded(cookies.get(cookie), __COOKIE_DELIM)) + .append("\r\n"); + } + + request.append("\r\n"); + + _handshake=new ByteArrayBuffer(request.toString(), false); + } + + try + { + int flushed = _endp.flush(_handshake); + if (flushed<0) + throw new IOException("incomplete handshake"); + } + catch (IOException e) + { + _future.handshakeFailed(e); + } + return _handshake.length()==0; + } + + public Connection handle() throws IOException + { + while (_endp.isOpen() && !_parser.isComplete()) + { + if (_handshake==null || _handshake.length()>0) + if (!handshake()) + return this; + + if (!_parser.parseAvailable()) + { + if (_endp.isInputShutdown()) + _future.handshakeFailed(new IOException("Incomplete handshake response")); + return this; + } + } + if (_error == null) + { + if (_accept == null) + { + _error = "No Sec-WebSocket-Accept"; + } + else if (!WebSocketConnectionRFC6455.hashKey(_key).equals(_accept)) + { + _error = "Bad Sec-WebSocket-Accept"; + } + else if (!initExtensions(_future.getExtensions())) + { + _error = "Bad Sec-WebSocket-Extension"; + } + else + { + WebSocketConnection connection = newWebSocketConnection(); + + Buffer header = _parser.getHeaderBuffer(); + if (header.hasContent()) + connection.fillBuffersFrom(header); + _buffers.returnBuffer(header); + + _future.onConnection(connection); + + return connection; + } + } + + _endp.close(); + return this; + } + + private WebSocketConnection newWebSocketConnection() throws IOException + { + __log.debug("newWebSocketConnection()"); + return new WebSocketClientConnection( + _future._client.getFactory(), + _future.getWebSocket(), + _endp, + _buffers, + System.currentTimeMillis(), + _future.getMaxIdleTime(), + _protocol, + _future.getExtensions(), + WebSocketConnectionRFC6455.VERSION, + _future.getMaskGen()); + } + + public void onInputShutdown() throws IOException + { + _endp.close(); + } + + public boolean isIdle() + { + return false; + } + + public boolean isSuspended() + { + return false; + } + + public void onClose() + { + if (_error != null) + _future.handshakeFailed(new ProtocolException(_error)); + else + _future.handshakeFailed(new EOFException()); + } + } + + private static class WebSocketClientConnection extends WebSocketConnectionRFC6455 + { + private final WebSocketClientFactory factory; + + public WebSocketClientConnection(WebSocketClientFactory factory, WebSocket webSocket, EndPoint endPoint, WebSocketBuffers buffers, long timeStamp, int maxIdleTime, String protocol, List<Extension> extensions, int draftVersion, MaskGen maskGen) throws IOException + { + super(webSocket, endPoint, buffers, timeStamp, maxIdleTime, protocol, extensions, draftVersion, maskGen); + this.factory = factory; + } + + @Override + public void onClose() + { + super.onClose(); + factory.removeConnection(this); + } + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketConnection.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketConnection.java new file mode 100644 index 00000000..76385f7d --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketConnection.java @@ -0,0 +1,36 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + + +import java.util.List; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.nio.AsyncConnection; + +public interface WebSocketConnection extends AsyncConnection +{ + void fillBuffersFrom(Buffer buffer); + + List<Extension> getExtensions(); + + WebSocket.Connection getConnection(); + + void shutdown(); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java new file mode 100644 index 00000000..c9f2759d --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketConnectionRFC6455.java @@ -0,0 +1,887 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may select to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.security.MessageDigest; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Locale; + +import org.eclipse.jetty.io.AbstractConnection; +import org.eclipse.jetty.io.AsyncEndPoint; +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Connection; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; +import org.eclipse.jetty.websocket.WebSocket.OnBinaryMessage; +import org.eclipse.jetty.websocket.WebSocket.OnControl; +import org.eclipse.jetty.websocket.WebSocket.OnFrame; +import org.eclipse.jetty.websocket.WebSocket.OnTextMessage; + +import android.text.TextUtils; +import android.util.Base64; + +/* ------------------------------------------------------------ */ +/** + * <pre> + * 0 1 2 3 + * 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + * +-+-+-+-+-------+-+-------------+-------------------------------+ + * |F|R|R|R| opcode|M| Payload len | Extended payload length | + * |I|S|S|S| (4) |A| (7) | (16/64) | + * |N|V|V|V| |S| | (if payload len==126/127) | + * | |1|2|3| |K| | | + * +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + + * | Extended payload length continued, if payload len == 127 | + * + - - - - - - - - - - - - - - - +-------------------------------+ + * | |Masking-key, if MASK set to 1 | + * +-------------------------------+-------------------------------+ + * | Masking-key (continued) | Payload Data | + * +-------------------------------- - - - - - - - - - - - - - - - + + * : Payload Data continued ... : + * + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + * | Payload Data continued ... | + * +---------------------------------------------------------------+ + * </pre> + * + * modified by KNOWLEDGECODE + */ +public class WebSocketConnectionRFC6455 extends AbstractConnection implements WebSocketConnection +{ + private static final Logger LOG = Log.getLogger(WebSocketConnectionRFC6455.class); + + final static byte OP_CONTINUATION = 0x00; + final static byte OP_TEXT = 0x01; + final static byte OP_BINARY = 0x02; + final static byte OP_EXT_DATA = 0x03; + + final static byte OP_CONTROL = 0x08; + final static byte OP_CLOSE = 0x08; + final static byte OP_PING = 0x09; + final static byte OP_PONG = 0x0A; + final static byte OP_EXT_CTRL = 0x0B; + + final static int CLOSE_NORMAL=1000; + final static int CLOSE_SHUTDOWN=1001; + final static int CLOSE_PROTOCOL=1002; + final static int CLOSE_BAD_DATA=1003; + final static int CLOSE_UNDEFINED=1004; + final static int CLOSE_NO_CODE=1005; + final static int CLOSE_NO_CLOSE=1006; + final static int CLOSE_BAD_PAYLOAD=1007; + final static int CLOSE_POLICY_VIOLATION=1008; + final static int CLOSE_MESSAGE_TOO_LARGE=1009; + final static int CLOSE_REQUIRED_EXTENSION=1010; + final static int CLOSE_SERVER_ERROR=1011; + final static int CLOSE_FAILED_TLS_HANDSHAKE=1015; + + final static int FLAG_FIN=0x8; + + // Per RFC 6455, section 1.3 - Opening Handshake - this version is "13" + final static int VERSION=13; + + static boolean isLastFrame(byte flags) + { + return (flags&FLAG_FIN)!=0; + } + + static boolean isControlFrame(byte opcode) + { + return (opcode&OP_CONTROL)!=0; + } + + private final static byte[] MAGIC; + private final List<Extension> _extensions; + private final WebSocketParserRFC6455 _parser; + private final WebSocketGeneratorRFC6455 _generator; + private final WebSocketGenerator _outbound; + private final WebSocket _webSocket; + private final OnFrame _onFrame; + private final OnBinaryMessage _onBinaryMessage; + private final OnTextMessage _onTextMessage; + private final OnControl _onControl; + private final String _protocol; + private final ClassLoader _context; + private volatile int _closeCode; + private volatile String _closeMessage; + private volatile boolean _closedIn; + private volatile boolean _closedOut; + private int _maxTextMessageSize=-1; + private int _maxBinaryMessageSize=-1; + + private static final Charset _iso88591 = Charset.forName("ISO-8859-1"); + private static final Charset _utf8 = Charset.forName("UTF-8"); + + static + { + MAGIC="258EAFA5-E914-47DA-95CA-C5AB0DC85B11".getBytes(_iso88591); + } + + private final WebSocket.FrameConnection _connection = new WSFrameConnection(); + + /* ------------------------------------------------------------ */ + public WebSocketConnectionRFC6455(WebSocket websocket, EndPoint endpoint, WebSocketBuffers buffers, long timestamp, int maxIdleTime, String protocol, List<Extension> extensions,int draft, MaskGen maskgen) + throws IOException + { + super(endpoint,timestamp); + + _context=Thread.currentThread().getContextClassLoader(); + + _endp.setMaxIdleTime(maxIdleTime); + + _webSocket = websocket; + _onFrame=_webSocket instanceof OnFrame ? (OnFrame)_webSocket : null; + _onTextMessage=_webSocket instanceof OnTextMessage ? (OnTextMessage)_webSocket : null; + _onBinaryMessage=_webSocket instanceof OnBinaryMessage ? (OnBinaryMessage)_webSocket : null; + _onControl=_webSocket instanceof OnControl ? (OnControl)_webSocket : null; + _generator = new WebSocketGeneratorRFC6455(buffers, _endp,maskgen); + + _extensions=extensions; + WebSocketParser.FrameHandler frameHandler = new WSFrameHandler(); + if (_extensions!=null) + { + int e=0; + for (Extension extension : _extensions) + { + extension.bind( + _connection, + e==extensions.size()-1? frameHandler :extensions.get(e+1), + e==0?_generator:extensions.get(e-1)); + e++; + } + } + + _outbound=(_extensions==null||_extensions.size()==0)?_generator:extensions.get(extensions.size()-1); + WebSocketParser.FrameHandler inbound = (_extensions == null || _extensions.size() == 0) ? frameHandler : extensions.get(0); + + _parser = new WebSocketParserRFC6455(buffers, endpoint, inbound,maskgen==null); + + _protocol=protocol; + + } + + /* ------------------------------------------------------------ */ + public WebSocket.Connection getConnection() + { + return _connection; + } + + /* ------------------------------------------------------------ */ + public List<Extension> getExtensions() + { + if (_extensions==null) + return Collections.emptyList(); + + return _extensions; + } + + /* ------------------------------------------------------------ */ + public Connection handle() throws IOException + { + Thread current = Thread.currentThread(); + ClassLoader oldcontext = current.getContextClassLoader(); + current.setContextClassLoader(_context); + try + { + // handle the framing protocol + boolean progress=true; + + while (progress) + { + int flushed=_generator.flushBuffer(); + int filled=_parser.parseNext(); + + progress = flushed>0 || filled>0; + _endp.flush(); + + if (_endp instanceof AsyncEndPoint && ((AsyncEndPoint)_endp).hasProgressed()) + progress=true; + } + } + catch(IOException e) + { + try + { + if (_endp.isOpen()) + _endp.close(); + } + catch(IOException e2) + { + LOG.ignore(e2); + } + throw e; + } + finally + { + current.setContextClassLoader(oldcontext); + _parser.returnBuffer(); + _generator.returnBuffer(); + if (_endp.isOpen()) + { + if (_closedIn && _closedOut && _outbound.isBufferEmpty()) + _endp.close(); + else if (_endp.isInputShutdown() && !_closedIn) + closeIn(CLOSE_NO_CLOSE,null); + else + checkWriteable(); + } + } + return this; + } + + /* ------------------------------------------------------------ */ + public void onInputShutdown() throws IOException + { + if (!_closedIn) + _endp.close(); + } + + /* ------------------------------------------------------------ */ + public boolean isIdle() + { + return _parser.isBufferEmpty() && _outbound.isBufferEmpty(); + } + + /* ------------------------------------------------------------ */ + @Override + public void onIdleExpired(long idleForMs) + { + closeOut(WebSocketConnectionRFC6455.CLOSE_NORMAL,"Idle for "+idleForMs+"ms > "+_endp.getMaxIdleTime()+"ms"); + } + + /* ------------------------------------------------------------ */ + public boolean isSuspended() + { + return false; + } + + /* ------------------------------------------------------------ */ + public void onClose() + { + final boolean closed; + synchronized (this) + { + closed=_closeCode==0; + if (closed) + _closeCode=WebSocketConnectionRFC6455.CLOSE_NO_CLOSE; + } + if (closed) + _webSocket.onClose(WebSocketConnectionRFC6455.CLOSE_NO_CLOSE,"closed"); + } + + /* ------------------------------------------------------------ */ + public void closeIn(int code,String message) + { + LOG.debug("ClosedIn {} {} {}",this,code,message); + + final boolean closed_out; + final boolean tell_app; + synchronized (this) + { + closed_out=_closedOut; + _closedIn=true; + tell_app=_closeCode==0; + if (tell_app) + { + _closeCode=code; + _closeMessage=message; + } + } + + try + { + if (!closed_out) + closeOut(code,message); + } + finally + { + if (tell_app) + _webSocket.onClose(code,message); + } + } + + /* ------------------------------------------------------------ */ + public void closeOut(int code,String message) + { + LOG.debug("ClosedOut {} {} {}",this,code,message); + + final boolean closed_out; + final boolean tell_app; + synchronized (this) + { + closed_out=_closedOut; + _closedOut=true; + tell_app=_closeCode==0; + if (tell_app) + { + _closeCode=code; + _closeMessage=message; + } + } + + try + { + if (tell_app) + _webSocket.onClose(code,message); + } + finally + { + try + { + if (!closed_out) + { + // Close code 1005/1006/1015 are never to be sent as a status over + // a Close control frame. Code<-1 also means no node. + + if (code < 0 || (code == WebSocketConnectionRFC6455.CLOSE_NO_CODE) || (code == WebSocketConnectionRFC6455.CLOSE_NO_CLOSE) + || (code == WebSocketConnectionRFC6455.CLOSE_FAILED_TLS_HANDSHAKE)) + { + code = -1; + } + else if (code == 0) + { + code = WebSocketConnectionRFC6455.CLOSE_NORMAL; + } + + byte[] bytes = ("xx"+(message==null?"":message)).getBytes(_iso88591); + bytes[0]=(byte)(code/0x100); + bytes[1]=(byte)(code%0x100); + _outbound.addFrame((byte)FLAG_FIN,WebSocketConnectionRFC6455.OP_CLOSE,bytes,0,code>0?bytes.length:0); + _outbound.flush(); + } + } + catch(IOException e) + { + LOG.ignore(e); + } + } + } + + public void shutdown() + { + final WebSocket.Connection connection = _connection; + if (connection != null) + connection.close(CLOSE_SHUTDOWN, null); + } + + /* ------------------------------------------------------------ */ + public void fillBuffersFrom(Buffer buffer) + { + _parser.fill(buffer); + } + + /* ------------------------------------------------------------ */ + private void checkWriteable() + { + if (!_outbound.isBufferEmpty() && _endp instanceof AsyncEndPoint) + { + ((AsyncEndPoint)_endp).scheduleWrite(); + } + } + + protected void onFrameHandshake() + { + if (_onFrame != null) + { + _onFrame.onHandshake(_connection); + } + } + + protected void onWebSocketOpen() + { + _webSocket.onOpen(_connection); + } + + /* ------------------------------------------------------------ */ + private class WSFrameConnection implements WebSocket.FrameConnection + { + private volatile boolean _disconnecting; + + /* ------------------------------------------------------------ */ + public void sendMessage(String content) throws IOException + { + if (_closedOut) + throw new IOException("closedOut " + _closeCode + ":" + _closeMessage); + byte[] data = content.getBytes(_utf8); + _outbound.addFrame((byte)FLAG_FIN, WebSocketConnectionRFC6455.OP_TEXT, data, 0, data.length); + checkWriteable(); + } + + /* ------------------------------------------------------------ */ + public void sendMessage(byte[] content, int offset, int length) throws IOException + { + if (_closedOut) + throw new IOException("closedOut " + _closeCode + ":" + _closeMessage); + _outbound.addFrame((byte)FLAG_FIN, WebSocketConnectionRFC6455.OP_BINARY, content, offset, length); + checkWriteable(); + } + + /* ------------------------------------------------------------ */ + public void sendFrame(byte flags,byte opcode, byte[] content, int offset, int length) throws IOException + { + if (_closedOut) + throw new IOException("closedOut " + _closeCode + ":" + _closeMessage); + _outbound.addFrame(flags, opcode, content, offset, length); + checkWriteable(); + } + + /* ------------------------------------------------------------ */ + public void sendControl(byte ctrl, byte[] data, int offset, int length) throws IOException + { + // TODO: section 5.5 states that control frames MUST never be length > 125 bytes and MUST NOT be fragmented + if (_closedOut) + throw new IOException("closedOut " + _closeCode + ":" + _closeMessage); + _outbound.addFrame((byte)FLAG_FIN, ctrl, data, offset, length); + checkWriteable(); + } + + /* ------------------------------------------------------------ */ + public boolean isMessageComplete(byte flags) + { + return isLastFrame(flags); + } + + /* ------------------------------------------------------------ */ + public boolean isOpen() + { + return _endp!=null&&_endp.isOpen(); + } + + /* ------------------------------------------------------------ */ + public void close(int code, String message) + { + if (_disconnecting) + return; + _disconnecting=true; + WebSocketConnectionRFC6455.this.closeOut(code,message); + } + + /* ------------------------------------------------------------ */ + public void setMaxIdleTime(int ms) + { + try + { + _endp.setMaxIdleTime(ms); + } + catch(IOException e) + { + LOG.warn(e); + } + } + + /* ------------------------------------------------------------ */ + public void setMaxTextMessageSize(int size) + { + _maxTextMessageSize=size; + } + + /* ------------------------------------------------------------ */ + public void setMaxBinaryMessageSize(int size) + { + _maxBinaryMessageSize=size; + } + + /* ------------------------------------------------------------ */ + public int getMaxIdleTime() + { + return _endp.getMaxIdleTime(); + } + + /* ------------------------------------------------------------ */ + public int getMaxTextMessageSize() + { + return _maxTextMessageSize; + } + + /* ------------------------------------------------------------ */ + public int getMaxBinaryMessageSize() + { + return _maxBinaryMessageSize; + } + + /* ------------------------------------------------------------ */ + public String getProtocol() + { + return _protocol; + } + + /* ------------------------------------------------------------ */ + public String getExtensions() + { + List<String> extensions = new ArrayList<String>(); + + for (Extension extension : _extensions) + { + extensions.add(extension.getParameterizedName()); + } + return TextUtils.join(", ", extensions); + } + + /* ------------------------------------------------------------ */ + public byte binaryOpcode() + { + return OP_BINARY; + } + + /* ------------------------------------------------------------ */ + public byte textOpcode() + { + return OP_TEXT; + } + + /* ------------------------------------------------------------ */ + public byte continuationOpcode() + { + return OP_CONTINUATION; + } + + /* ------------------------------------------------------------ */ + public byte finMask() + { + return FLAG_FIN; + } + + /* ------------------------------------------------------------ */ + public boolean isControl(byte opcode) + { + return isControlFrame(opcode); + } + + /* ------------------------------------------------------------ */ + public boolean isText(byte opcode) + { + return opcode==OP_TEXT; + } + + /* ------------------------------------------------------------ */ + public boolean isBinary(byte opcode) + { + return opcode==OP_BINARY; + } + + /* ------------------------------------------------------------ */ + public boolean isContinuation(byte opcode) + { + return opcode==OP_CONTINUATION; + } + + /* ------------------------------------------------------------ */ + public boolean isClose(byte opcode) + { + return opcode==OP_CLOSE; + } + + /* ------------------------------------------------------------ */ + public boolean isPing(byte opcode) + { + return opcode==OP_PING; + } + + /* ------------------------------------------------------------ */ + public boolean isPong(byte opcode) + { + return opcode==OP_PONG; + } + + /* ------------------------------------------------------------ */ + public void close() + { + close(CLOSE_NORMAL,null); + } + + /* ------------------------------------------------------------ */ + public void setAllowFrameFragmentation(boolean allowFragmentation) + { + _parser.setFakeFragments(allowFragmentation); + } + + /* ------------------------------------------------------------ */ + public boolean isAllowFrameFragmentation() + { + return _parser.isFakeFragments(); + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return String.format(Locale.getDefault(), "%s@%x l(%s:%d)<->r(%s:%d)", + getClass().getSimpleName(), + hashCode(), + _endp.getLocalAddr(), + _endp.getLocalPort(), + _endp.getRemoteAddr(), + _endp.getRemotePort()); + } + } + + private class WSFrameHandler implements WebSocketParser.FrameHandler + { + private static final int MAX_CONTROL_FRAME_PAYLOAD = 125; + private static final int INITIAL_CAPACITY = 8192; + private WebSocketBuffer _buffer = new WebSocketBuffer(INITIAL_CAPACITY); + private byte _opcode = -1; + + private boolean excess(int opcode, int length) + { + switch (opcode) + { + case WebSocketConnectionRFC6455.OP_TEXT: + return _maxTextMessageSize > 0 && _maxTextMessageSize < length; + case WebSocketConnectionRFC6455.OP_BINARY: + return _maxBinaryMessageSize > 0 && _maxBinaryMessageSize < length; + default: + return false; + } + } + + public void onFrame(final byte flags, final byte opcode, final Buffer buffer) + { + synchronized (WebSocketConnectionRFC6455.this) + { + // Ignore incoming after a close + if (_closedIn) + { + return; + } + } + + byte[] array = buffer.array(); + int offset = buffer.getIndex(); + int length = buffer.length(); + + if (isControlFrame(opcode) && length > MAX_CONTROL_FRAME_PAYLOAD) + { + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL, "Control frame too large: " + length + " > " + MAX_CONTROL_FRAME_PAYLOAD); + return; + } + + if ((flags & 0x7) != 0) + { + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL, "RSV bits set 0x" + Integer.toHexString(flags)); + return; + } + + // Ignore all frames after error close + if (_closeCode != 0 && _closeCode != CLOSE_NORMAL && opcode != OP_CLOSE) + { + return; + } + + // Deliver frame if websocket is a FrameWebSocket + if (_onFrame != null) + { + if (_onFrame.onFrame(flags, opcode, array, offset, length)) + { + return; + } + } + + if (_onControl != null && isControlFrame(opcode)) + { + if (_onControl.onControl(opcode, array, offset, length)) + { + return; + } + } + + switch (opcode) + { + case WebSocketConnectionRFC6455.OP_TEXT: + case WebSocketConnectionRFC6455.OP_BINARY: + { + if (_opcode != -1) + { + _buffer.clear(); + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL, "Expected Continuation" + Integer.toHexString(opcode)); + return; + } + _opcode = opcode; + } + case WebSocketConnectionRFC6455.OP_CONTINUATION: + { + if (_opcode == -1) + { + _buffer.clear(); + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL, "Bad Continuation"); + return; + } + if (excess(_opcode, _buffer.length() + length)) + { + switch (_opcode) + { + case WebSocketConnectionRFC6455.OP_TEXT: + _connection.close(WebSocketConnectionRFC6455.CLOSE_MESSAGE_TOO_LARGE, "Text message size > " + _maxTextMessageSize + " chars"); + return; + case WebSocketConnectionRFC6455.OP_BINARY: + _connection.close(WebSocketConnectionRFC6455.CLOSE_MESSAGE_TOO_LARGE, "Message size > " + _maxBinaryMessageSize); + return; + } + } + if (isLastFrame(flags)) + { + switch (_opcode) + { + case WebSocketConnectionRFC6455.OP_TEXT: + if (_buffer.length() == 0) + { + _onTextMessage.onMessage(new String(array, offset, length, _utf8)); + } + else + { + _onTextMessage.onMessage(_buffer.append(array, offset, length).toString(_utf8)); + } + break; + case WebSocketConnectionRFC6455.OP_BINARY: + if (_buffer.length() == 0) + { + _onBinaryMessage.onMessage(array, offset, length); + } + else + { + _onBinaryMessage.onMessage(_buffer.append(array, offset, length).array(), 0, _buffer.length()); + } + break; + } + _opcode = -1; + _buffer.clear(); + } + else + { + _buffer.append(array, offset, length); + } + break; + } + + case WebSocketConnectionRFC6455.OP_PING: + { + LOG.debug("PING {}",this); + if (!_closedOut) + { + try + { + _connection.sendControl(WebSocketConnectionRFC6455.OP_PONG, array, offset, length); + } + catch (Throwable e) + { + errorClose(WebSocketConnectionRFC6455.CLOSE_SERVER_ERROR, "Internal Server Error: " + e); + } + } + break; + } + + case WebSocketConnectionRFC6455.OP_PONG: + { + LOG.debug("PONG {}",this); + break; + } + + case WebSocketConnectionRFC6455.OP_CLOSE: + { + int code = WebSocketConnectionRFC6455.CLOSE_NO_CODE; + String message = null; + if (length >= 2) + { + code=(0xff & array[offset]) * 0x100 + (0xff & array[offset + 1]); + + // Validate close status codes. + if (code < WebSocketConnectionRFC6455.CLOSE_NORMAL || + code == WebSocketConnectionRFC6455.CLOSE_UNDEFINED || + code == WebSocketConnectionRFC6455.CLOSE_NO_CLOSE || + code == WebSocketConnectionRFC6455.CLOSE_NO_CODE || + ( code > 1011 && code <= 2999 ) || + code >= 5000 ) + { + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,"Invalid close code " + code); + return; + } + } + else if(buffer.length() == 1) + { + // Invalid length. use status code 1002 (Protocol error) + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,"Invalid payload length of 1"); + return; + } + closeIn(code, message); + break; + } + + default: + errorClose(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,"Bad opcode 0x"+Integer.toHexString(opcode)); + break; + } + } + + private void errorClose(int code, String message) + { + _connection.close(code, message); + + // Brutally drop the connection + try + { + _endp.close(); + } + catch (IOException e) + { + LOG.warn(e.toString()); + LOG.debug(e); + } + } + + public void close(int code,String message) + { + if (code != CLOSE_NORMAL) + LOG.warn("Close: " + code + " " + message); + _connection.close(code, message); + } + + @Override + public String toString() + { + return WebSocketConnectionRFC6455.this.toString() + "FH"; + } + } + + /* ------------------------------------------------------------ */ + public static String hashKey(String key) + { + try + { + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.update(key.getBytes(_utf8)); + md.update(MAGIC); + return Base64.encodeToString(md.digest(), Base64.NO_WRAP); + } + catch (Exception e) + { + throw new RuntimeException(e); + } + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return String.format("%s p=%s g=%s", getClass().getSimpleName(), _parser, _generator); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketGenerator.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketGenerator.java new file mode 100644 index 00000000..10046ac2 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketGenerator.java @@ -0,0 +1,33 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; + + + +/* ------------------------------------------------------------ */ +/** WebSocketGenerator. + */ +public interface WebSocketGenerator +{ + int flush() throws IOException; + boolean isBufferEmpty(); + void addFrame(byte flags,byte opcode, byte[] content, int offset, int length) throws IOException; +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketGeneratorRFC6455.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketGeneratorRFC6455.java new file mode 100644 index 00000000..7fedfb3f --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketGeneratorRFC6455.java @@ -0,0 +1,319 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; +import java.util.Locale; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.io.EofException; + + +/** + * WebSocketGenerator. + * This class generates websocket packets. + * It is fully synchronized because it is likely that async + * threads will call the addMessage methods while other + * threads are flushing the generator. + * + * modified by KNOWLEDGECODE + */ +public class WebSocketGeneratorRFC6455 implements WebSocketGenerator +{ + private final Lock _lock = new ReentrantLock(); + private final WebSocketBuffers _buffers; + private final EndPoint _endp; + private final byte[] _mask = new byte[4]; + private final MaskGen _maskGen; + private Buffer _buffer; + private int _m; + private boolean _opsent; + private boolean _closed; + + public WebSocketGeneratorRFC6455(WebSocketBuffers buffers, EndPoint endp) + { + this(buffers, endp, null); + } + + public WebSocketGeneratorRFC6455(WebSocketBuffers buffers, EndPoint endp, MaskGen maskGen) + { + _buffers = buffers; + _endp = endp; + _maskGen = maskGen; + } + + public Buffer getBuffer() + { + _lock.lock(); + try + { + return _buffer; + } + finally + { + _lock.unlock(); + } + } + + public void addFrame(byte flags, byte opcode, byte[] content, int offset, int length) throws IOException + { + _lock.lock(); + try + { + if (_closed) + throw new EofException("Closed"); + if (opcode == WebSocketConnectionRFC6455.OP_CLOSE) + _closed = true; + + boolean mask = _maskGen != null; + + if (_buffer == null) + _buffer = mask ? _buffers.getBuffer() : _buffers.getDirectBuffer(); + + boolean last = WebSocketConnectionRFC6455.isLastFrame(flags); + + int space = mask ? 14 : 10; + + do + { + if (_opsent) + { + flags &= 0x0b; + opcode = WebSocketConnectionRFC6455.OP_CONTINUATION; + } + opcode = (byte)(((0xf & flags) << 4) + (0xf & opcode)); + _opsent = true; + + int payload = length; + if (payload + space > _buffer.capacity()) + { + // We must fragement, so clear FIN bit + opcode = (byte)(opcode & 0x7F); // Clear the FIN bit + payload = _buffer.capacity() - space; + } + else if (last) + opcode = (byte)(opcode | 0x80); // Set the FIN bit + + // ensure there is space for header + if (_buffer.space() <= space) + { + flushBuffer(); + if (_buffer.space() <= space) + flush(); + } + + // write the opcode and length + if (payload > 0xffff) + { + _buffer.put(new byte[]{ + opcode, + mask ? (byte)0xff : (byte)0x7f, + (byte)0, + (byte)0, + (byte)0, + (byte)0, + (byte)((payload >> 24) & 0xff), + (byte)((payload >> 16) & 0xff), + (byte)((payload >> 8) & 0xff), + (byte)(payload & 0xff)}); + } + else if (payload >= 0x7e) + { + _buffer.put(new byte[]{ + opcode, + mask ? (byte)0xfe : (byte)0x7e, + (byte)(payload >> 8), + (byte)(payload & 0xff)}); + } + else + { + _buffer.put(new byte[]{ + opcode, + (byte)(mask ? (0x80 | payload) : payload)}); + } + + // write mask + if (mask) + { + _maskGen.genMask(_mask); + _m = 0; + _buffer.put(_mask); + } + + // write payload + int remaining = payload; + while (remaining > 0) + { + _buffer.compact(); + int chunk = remaining < _buffer.space() ? remaining : _buffer.space(); + + if (mask) + { + for (int i = 0; i < chunk; i++) + _buffer.put((byte)(content[offset + (payload - remaining) + i] ^ _mask[+_m++ % 4])); + } + else + _buffer.put(content, offset + (payload - remaining), chunk); + + remaining -= chunk; + if (_buffer.space() > 0) + { + // Gently flush the data, issuing a non-blocking write + flushBuffer(); + } + else + { + // Forcibly flush the data, issuing a blocking write + flush(); + if (remaining == 0) + { + // Gently flush the data, issuing a non-blocking write + flushBuffer(); + } + } + } + offset += payload; + length -= payload; + } + while (length > 0); + _opsent = !last; + + if (_buffer != null && _buffer.length() == 0) + { + _buffers.returnBuffer(_buffer); + _buffer = null; + } + } + finally + { + _lock.unlock(); + } + } + + public int flushBuffer() throws IOException + { + if (!_lock.tryLock()) + return 0; + + try + { + if (!_endp.isOpen()) + throw new EofException(); + + if (_buffer != null) + { + int flushed = _buffer.hasContent() ? _endp.flush(_buffer) : 0; + if (_closed && _buffer.length() == 0) + _endp.shutdownOutput(); + return flushed; + } + + return 0; + } + finally + { + _lock.unlock(); + } + } + + public int flush() throws IOException + { + if (!_lock.tryLock()) + return 0; + + try + { + if (_buffer == null) + return 0; + + int result = flushBuffer(); + if (!_endp.isBlocking()) + { + long now = System.currentTimeMillis(); + long end = now + _endp.getMaxIdleTime(); + while (_buffer.length() > 0) + { + boolean ready = _endp.blockWritable(end - now); + if (!ready) + { + now = System.currentTimeMillis(); + if (now < end) + continue; + throw new IOException("Write timeout"); + } + + result += flushBuffer(); + } + } + _buffer.compact(); + return result; + } + finally + { + _lock.unlock(); + } + } + + public boolean isBufferEmpty() + { + _lock.lock(); + try + { + return _buffer == null || _buffer.length() == 0; + } + finally + { + _lock.unlock(); + } + } + + public void returnBuffer() + { + _lock.lock(); + try + { + if (_buffer != null && _buffer.length() == 0) + { + _buffers.returnBuffer(_buffer); + _buffer = null; + } + } + finally + { + _lock.unlock(); + } + } + + @Override + public String toString() + { + // Do NOT use synchronized (this) + // because it's very easy to deadlock when debugging is enabled. + // We do a best effort to print the right toString() and that's it. + Buffer buffer = _buffer; + return String.format(Locale.getDefault(), "%s@%x closed=%b buffer=%d", + getClass().getSimpleName(), + hashCode(), + _closed, + buffer == null ? -1 : buffer.length()); + } +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketParser.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketParser.java new file mode 100644 index 00000000..b53250b5 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketParser.java @@ -0,0 +1,49 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import org.eclipse.jetty.io.Buffer; + +/* ------------------------------------------------------------ */ +/** + * Parser the WebSocket protocol. + * + */ +public interface WebSocketParser +{ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + /* ------------------------------------------------------------ */ + public interface FrameHandler + { + void onFrame(byte flags, byte opcode, Buffer buffer); + void close(int code,String message); + } + + Buffer getBuffer(); + + /** + * @return an indication of progress, normally bytes filled plus events parsed, or -1 for EOF + */ + int parseNext(); + + boolean isBufferEmpty(); + + void fill(Buffer buffer); +} diff --git a/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketParserRFC6455.java b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketParserRFC6455.java new file mode 100644 index 00000000..5b516e19 --- /dev/null +++ b/plugins/cordova-plugin-websocket/src/android/org/eclipse/jetty/websocket/WebSocketParserRFC6455.java @@ -0,0 +1,392 @@ +// +// ======================================================================== +// Copyright (c) 1995-2015 Mort Bay Consulting Pty. Ltd. +// ------------------------------------------------------------------------ +// All rights reserved. This program and the accompanying materials +// are made available under the terms of the Eclipse Public License v1.0 +// and Apache License v2.0 which accompanies this distribution. +// +// The Eclipse Public License is available at +// http://www.eclipse.org/legal/epl-v10.html +// +// The Apache License v2.0 is available at +// http://www.opensource.org/licenses/apache2.0.php +// +// You may elect to redistribute this code under either of these licenses. +// ======================================================================== +// + +package org.eclipse.jetty.websocket; + +import java.io.IOException; + +import org.eclipse.jetty.io.Buffer; +import org.eclipse.jetty.io.Buffers; +import org.eclipse.jetty.io.EndPoint; +import org.eclipse.jetty.util.log.Log; +import org.eclipse.jetty.util.log.Logger; + +/* ------------------------------------------------------------ */ +/** + * Parser the WebSocket protocol. + * + */ +public class WebSocketParserRFC6455 implements WebSocketParser +{ + private static final Logger LOG = Log.getLogger(WebSocketParserRFC6455.class); + + public enum State { + + START(0), OPCODE(1), LENGTH_7(1), LENGTH_16(2), LENGTH_63(8), MASK(4), PAYLOAD(0), DATA(0), SKIP(1), SEEK_EOF(1); + + int _needs; + + State(int needs) + { + _needs=needs; + } + + int getNeeds() + { + return _needs; + } + } + + private final WebSocketBuffers _buffers; + private final EndPoint _endp; + private final FrameHandler _handler; + private final boolean _shouldBeMasked; + private State _state; + private Buffer _buffer; + private byte _flags; + private byte _opcode; + private int _bytesNeeded; + private long _length; + private boolean _masked; + private final byte[] _mask = new byte[4]; + private int _m; + private boolean _skip; + private boolean _fragmentFrames=true; + + /* ------------------------------------------------------------ */ + /** + * @param buffers The buffers to use for parsing. Only the {@link Buffers#getBuffer()} is used. + * This should be a direct buffer if binary data is mostly used or an indirect buffer if utf-8 data + * is mostly used. + * @param endp the endpoint + * @param handler the handler to notify when a parse event occurs + * @param shouldBeMasked whether masking should be handled + */ + public WebSocketParserRFC6455(WebSocketBuffers buffers, EndPoint endp, FrameHandler handler, boolean shouldBeMasked) + { + _buffers=buffers; + _endp=endp; + _handler=handler; + _shouldBeMasked=shouldBeMasked; + _state=State.START; + } + + /* ------------------------------------------------------------ */ + /** + * @return True if fake fragments should be created for frames larger than the buffer. + */ + public boolean isFakeFragments() + { + return _fragmentFrames; + } + + /* ------------------------------------------------------------ */ + /** + * @param fakeFragments True if fake fragments should be created for frames larger than the buffer. + */ + public void setFakeFragments(boolean fakeFragments) + { + _fragmentFrames = fakeFragments; + } + + /* ------------------------------------------------------------ */ + public boolean isBufferEmpty() + { + return _buffer==null || _buffer.length()==0; + } + + /* ------------------------------------------------------------ */ + public Buffer getBuffer() + { + return _buffer; + } + + /* ------------------------------------------------------------ */ + /** Parse to next event. + * Parse to the next {@link WebSocketParser.FrameHandler} event or until no more data is + * available. Fill data from the {@link EndPoint} only as necessary. + * @return An indication of progress or otherwise. -1 indicates EOF, 0 indicates + * that no bytes were read and no messages parsed. A positive number indicates either + * the bytes filled or the messages parsed. + */ + public int parseNext() + { + if (_buffer==null) + _buffer=_buffers.getBuffer(); + + boolean progress=false; + int filled=-1; + + // Loop until a datagram call back or can't fill anymore + while(!progress && (!_endp.isInputShutdown()||_buffer.length()>0)) + { + int available=_buffer.length(); + + // Fill buffer if we need a byte or need length + while (available<(_state==State.SKIP?1:_bytesNeeded)) + { + // compact to mark (set at start of data) + _buffer.compact(); + + // if no space, then the data is too big for buffer + if (_buffer.space() == 0) + { + // Can we send a fake frame? + if (_fragmentFrames && _state==State.DATA) + { + Buffer data =_buffer.get(4*(available/4)); + _buffer.compact(); + if (_masked) + { + if (data.array()==null) + data=_buffer.asMutableBuffer(); + byte[] array = data.array(); + final int end=data.putIndex(); + for (int i=data.getIndex();i<end;i++) + array[i]^=_mask[_m++%4]; + } + + // System.err.printf("%s %s %s >>\n",TypeUtil.toHexString(_flags),TypeUtil.toHexString(_opcode),data.length()); + _bytesNeeded-=data.length(); + progress=true; + _handler.onFrame((byte)(_flags&(0xff^WebSocketConnectionRFC6455.FLAG_FIN)), _opcode, data); + + _opcode=WebSocketConnectionRFC6455.OP_CONTINUATION; + } + + if (_buffer.space() == 0) + throw new IllegalStateException("FULL: "+_state+" "+_bytesNeeded+">"+_buffer.capacity()); + } + + // catch IOExceptions (probably EOF) and try to parse what we have + try + { + filled=_endp.isInputShutdown()?-1:_endp.fill(_buffer); + available=_buffer.length(); + // System.err.printf(">> filled %d/%d%n",filled,available); + if (filled<=0) + break; + } + catch(IOException e) + { + LOG.debug(e); + filled=-1; + break; + } + } + // Did we get enough? + if (available<(_state==State.SKIP?1:_bytesNeeded)) + break; + + // if we are here, then we have sufficient bytes to process the current state. + // Parse the buffer byte by byte (unless it is STATE_DATA) + byte b; + while (_state!=State.DATA && available>=(_state==State.SKIP?1:_bytesNeeded)) + { + switch (_state) + { + case START: + _skip=false; + _state=_opcode==WebSocketConnectionRFC6455.OP_CLOSE?State.SEEK_EOF:State.OPCODE; + _bytesNeeded=_state.getNeeds(); + continue; + + case OPCODE: + b=_buffer.get(); + available--; + _opcode=(byte)(b&0xf); + _flags=(byte)(0xf&(b>>4)); + + if (WebSocketConnectionRFC6455.isControlFrame(_opcode)&&!WebSocketConnectionRFC6455.isLastFrame(_flags)) + { + LOG.warn("Fragmented Control from "+_endp); + _handler.close(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,"Fragmented control"); + progress=true; + _skip=true; + } + + _state=State.LENGTH_7; + _bytesNeeded=_state.getNeeds(); + + continue; + + case LENGTH_7: + b=_buffer.get(); + available--; + _masked=(b&0x80)!=0; + b=(byte)(0x7f&b); + + switch(b) + { + case 0x7f: + _length=0; + _state=State.LENGTH_63; + break; + case 0x7e: + _length=0; + _state=State.LENGTH_16; + break; + default: + _length=(0x7f&b); + _state=_masked?State.MASK:State.PAYLOAD; + } + _bytesNeeded=_state.getNeeds(); + continue; + + case LENGTH_16: + b=_buffer.get(); + available--; + _length = _length*0x100 + (0xff&b); + if (--_bytesNeeded==0) + { + if (_length>_buffer.capacity() && !_fragmentFrames) + { + progress=true; + _handler.close(WebSocketConnectionRFC6455.CLOSE_POLICY_VIOLATION,"frame size "+_length+">"+_buffer.capacity()); + _skip=true; + } + + _state=_masked?State.MASK:State.PAYLOAD; + _bytesNeeded=_state.getNeeds(); + } + continue; + + case LENGTH_63: + b=_buffer.get(); + available--; + _length = _length*0x100 + (0xff&b); + if (--_bytesNeeded==0) + { + _bytesNeeded=(int)_length; + if (_length>=_buffer.capacity() && !_fragmentFrames) + { + progress=true; + _handler.close(WebSocketConnectionRFC6455.CLOSE_POLICY_VIOLATION,"frame size "+_length+">"+_buffer.capacity()); + _skip=true; + } + + _state=_masked?State.MASK:State.PAYLOAD; + _bytesNeeded=_state.getNeeds(); + } + continue; + + case MASK: + _buffer.get(_mask,0,4); + _m=0; + available-=4; + _state=State.PAYLOAD; + _bytesNeeded=_state.getNeeds(); + break; + + case PAYLOAD: + _bytesNeeded=(int)_length; + _state=_skip?State.SKIP:State.DATA; + break; + + case DATA: + break; + + case SKIP: + int skip=Math.min(available,_bytesNeeded); + progress=true; + _buffer.skip(skip); + available-=skip; + _bytesNeeded-=skip; + if (_bytesNeeded==0) + _state=State.START; + break; + + case SEEK_EOF: + progress=true; + _buffer.skip(available); + available=0; + break; + } + } + + if (_state==State.DATA && available>=_bytesNeeded) + { + if ( _masked!=_shouldBeMasked) + { + _buffer.skip(_bytesNeeded); + _state=State.START; + progress=true; + _handler.close(WebSocketConnectionRFC6455.CLOSE_PROTOCOL,"Not masked"); + } + else + { + Buffer data =_buffer.get(_bytesNeeded); + if (_masked) + { + if (data.array()==null) + data=_buffer.asMutableBuffer(); + byte[] array = data.array(); + final int end=data.putIndex(); + for (int i=data.getIndex();i<end;i++) + array[i]^=_mask[_m++%4]; + } + + // System.err.printf("%s %s %s >>\n",TypeUtil.toHexString(_flags),TypeUtil.toHexString(_opcode),data.length()); + + progress=true; + _handler.onFrame(_flags, _opcode, data); + _bytesNeeded=0; + _state=State.START; + } + + break; + } + } + + return progress?1:filled; + } + + /* ------------------------------------------------------------ */ + public void fill(Buffer buffer) + { + if (buffer!=null && buffer.length()>0) + { + if (_buffer==null) + _buffer=_buffers.getBuffer(); + + _buffer.put(buffer); + buffer.clear(); + } + } + + /* ------------------------------------------------------------ */ + public void returnBuffer() + { + if (_buffer!=null && _buffer.length()==0) + { + _buffers.returnBuffer(_buffer); + _buffer=null; + } + } + + /* ------------------------------------------------------------ */ + @Override + public String toString() + { + return String.format("%s@%x state=%s buffer=%s", + getClass().getSimpleName(), + hashCode(), + _state, + _buffer); + } +} |
