├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── pom.xml └── src ├── main ├── java │ └── org │ │ └── red5 │ │ └── net │ │ └── websocket │ │ ├── Constants.java │ │ ├── SecureWebSocketConfiguration.java │ │ ├── WebSocketConnection.java │ │ ├── WebSocketException.java │ │ ├── WebSocketHandler.java │ │ ├── WebSocketPlugin.java │ │ ├── WebSocketScope.java │ │ ├── WebSocketScopeManager.java │ │ ├── WebSocketTransport.java │ │ ├── codec │ │ ├── WebSocketCodecFactory.java │ │ ├── WebSocketDecoder.java │ │ ├── WebSocketEncoder.java │ │ └── extension │ │ │ ├── PerMessageDeflateExt.java │ │ │ └── WebSocketExtension.java │ │ ├── listener │ │ ├── DefaultWebSocketDataListener.java │ │ ├── IWebSocketDataListener.java │ │ ├── IWebSocketScopeListener.java │ │ └── WebSocketDataListener.java │ │ ├── model │ │ ├── ConnectionType.java │ │ ├── HandshakeRequest.java │ │ ├── HandshakeResponse.java │ │ ├── MessageType.java │ │ ├── Packet.java │ │ ├── WSMessage.java │ │ └── WebSocketEvent.java │ │ └── util │ │ └── IdGenerator.java └── resources │ └── logback-websocket.xml └── test ├── java └── org │ └── red5 │ └── net │ └── websocket │ └── WebSocketServerTest.java └── resources └── logback-test.xml /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | .settings/ 3 | .project 4 | .classpath 5 | .DS_Store 6 | 7 | *.class 8 | *.zip 9 | *.tar.gz 10 | *~ 11 | 12 | Thumbs.db 13 | build.sh 14 | log/ 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | jdk: 3 | - oraclejdk8 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | red5-websocket 2 | ============== 3 | 4 | Websocket plug-in for Red5 5 | 6 | This plugin is meant to provide websocket functionality for applications running in red5. The code is constructed to comply with rfc6455. 7 | 8 | http://tools.ietf.org/html/rfc6455 9 | 10 | Special thanks to Takahiko Toda and Dhruv Chopra for the initial ideas and source. 11 | 12 | Configuration 13 | -------------- 14 | 15 | Add the WebSocket transport to the jee-container.xml or red5.xml. If placing it in the red5.xml, ensure the bean comes after the plugin launcher entry. 16 | 17 | To bind to one or many IP addresses and ports: 18 | 19 | ```xml 20 | 21 | 22 | 23 | 192.168.1.174 24 | 192.168.1.174:8080 25 | 192.168.1.174:10080 26 | 27 | 28 | 29 | ``` 30 | 31 | If you don't want to specify the IP: 32 | ``` 33 | 34 | 35 | 36 | 37 | ``` 38 | To support secure communication (wss) add this: 39 | 40 | ```xml 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 192.168.1.174:10081 53 | 54 | 55 | 56 | ``` 57 | If you are not using unlimited strength JCE (you are outside the US), you may have to specify the cipher suites as shown below: 58 | ```xml 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 69 | TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA 70 | TLS_ECDHE_RSA_WITH_RC4_128_SHA 71 | TLS_RSA_WITH_AES_128_CBC_SHA256 72 | TLS_RSA_WITH_AES_128_CBC_SHA 73 | SSL_RSA_WITH_RC4_128_SHA 74 | 75 | 76 | 77 | 78 | TLSv1 79 | TLSv1.1 80 | TLSv1.2 81 | 82 | 83 | 84 | 85 | 86 | 87 | 192.168.1.174:10081 88 | 89 | 90 | 91 | 92 | ``` 93 | 94 | 95 | Adding WebSocket to an Application 96 | ------------------------ 97 | 98 | To enable websocket support in your application, add this to your appStart() method: 99 | 100 | ``` 101 | WebSocketScopeManager manager = ((WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin")).getManager(scope); 102 | manager.setApplication(this); 103 | ``` 104 | 105 | For clean-up add this to appStop(): 106 | 107 | ``` 108 | WebSocketScopeManager manager = ((WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin")).getManager(scope); 109 | manager.stop(); 110 | ``` 111 | 112 | Security Features 113 | ------------------- 114 | Since WebSockets don't implement Same Origin Policy (SOP) nor Cross-Origin Resource Sharing (CORS), we've implemented a means to restrict access via configuration using SOP / CORS logic. To configure the security features, edit your `conf/jee-container.xml` file and locate the bean displayed below: 115 | ```xml 116 | 117 | 118 | 119 | ${ws.host}:${ws.port} 120 | 121 | 122 | 123 | 124 | 125 | 126 | localhost 127 | red5.org 128 | 129 | 130 | 131 | ``` 132 | Properties: 133 | * [sameOriginPolicy](https://www.w3.org/Security/wiki/Same_Origin_Policy) - Enables or disables SOP. The logic differs from standard web SOP by *NOT* enforcing protocol and port. 134 | * [crossOriginPolicy](https://www.w3.org/Security/wiki/CORS) - Enables or disables CORS. This option pairs with the `allowedOrigins` array. 135 | * allowedOrigins - The list or host names or fqdn which are to be permitted access. The default if none are specified is `*` which equates to any or all. 136 | 137 | 138 | 139 | Test Page 140 | ------------------- 141 | 142 | Replace the wsUri variable with your applications path. 143 | 144 | ``` 145 | 146 | 147 | WebSocket Test 148 |

WebSocket Test

151 | ``` 152 | 153 | Demo application project 154 | ---------------- 155 | https://github.com/Red5/red5-websocket-chat 156 | 157 | Pre-compiled JAR 158 | ---------------- 159 | You can find [compiled artifacts via Maven](http://mvnrepository.com/artifact/org.red5/websocket) 160 | 161 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | org.red5 5 | red5-parent 6 | 1.0.10-M9 7 | 8 | 4.0.0 9 | websocket 10 | jar 11 | websocket 12 | 1.16.15 13 | Red5 WebSocket plugin 14 | https://github.com/Red5/red5-websocket 15 | 16 | Red5 17 | https://github.com/Red5 18 | 19 | 20 | 21 | Apache 2 22 | http://www.apache.org/licenses/LICENSE-2.0.txt 23 | repo 24 | A business-friendly OSS license 25 | 26 | 27 | 28 | 29 | red5-interest 30 | https://groups.google.com/forum/?fromgroups#!forum/red5interest 31 | 32 | 33 | 34 | github 35 | https://github.com/Red5/red5-websocket/issues 36 | 37 | 38 | https://github.com/Red5/red5-websocket.git 39 | scm:git:git@github.com:Red5/red5-websocket.git 40 | scm:git:git@github.com:Red5/red5-websocket.git 41 | 42 | 43 | 44 | Paul Gregoire 45 | mondain@gmail.com 46 | 47 | 48 | 49 | 1.59 50 | 51 | 52 | install 53 | 54 | 55 | maven-jar-plugin 56 | 57 | 58 | 59 | true 60 | 61 | 62 | org.red5.net.websocket.WebSocketPlugin 63 | 64 | 65 | 66 | 67 | 68 | maven-source-plugin 69 | 70 | 71 | maven-javadoc-plugin 72 | 73 | 74 | maven-dependency-plugin 75 | 76 | 77 | org.apache.felix 78 | maven-bundle-plugin 79 | 80 | 81 | 82 | 83 | 84 | org.red5 85 | red5-server 86 | ${project.parent.version} 87 | jar 88 | compile 89 | 90 | 91 | org.bouncycastle 92 | bcprov-jdk15on 93 | ${bc.version} 94 | 95 | 96 | org.bouncycastle 97 | bctls-jdk15on 98 | ${bc.version} 99 | 100 | 101 | org.slf4j 102 | slf4j-api 103 | 104 | 105 | org.slf4j 106 | jcl-over-slf4j 107 | ${slf4j.version} 108 | 109 | 110 | org.slf4j 111 | jul-to-slf4j 112 | ${slf4j.version} 113 | 114 | 115 | org.slf4j 116 | log4j-over-slf4j 117 | ${slf4j.version} 118 | 119 | 120 | ch.qos.logback 121 | logback-classic 122 | ${logback.version} 123 | 124 | 125 | org.apache.mina 126 | mina-core 127 | ${mina.version} 128 | bundle 129 | 130 | 131 | org.apache.mina 132 | mina-integration-beans 133 | ${mina.version} 134 | bundle 135 | 136 | 137 | org.springframework 138 | spring-aop 139 | ${spring.version} 140 | 141 | 142 | commons-logging 143 | commons-logging 144 | 145 | 146 | 147 | 148 | org.springframework 149 | spring-beans 150 | ${spring.version} 151 | 152 | 153 | commons-logging 154 | commons-logging 155 | 156 | 157 | 158 | 159 | org.springframework 160 | spring-context 161 | 162 | 163 | commons-logging 164 | commons-logging 165 | 166 | 167 | 168 | 169 | org.springframework 170 | spring-context-support 171 | 172 | 173 | commons-logging 174 | commons-logging 175 | 176 | 177 | 178 | 179 | org.springframework 180 | spring-core 181 | 182 | 183 | commons-logging 184 | commons-logging 185 | 186 | 187 | 188 | 189 | org.springframework 190 | spring-expression 191 | ${spring.version} 192 | 193 | 194 | commons-logging 195 | commons-logging 196 | 197 | 198 | 199 | 200 | org.springframework 201 | spring-test 202 | ${spring.version} 203 | test 204 | 205 | 206 | commons-logging 207 | commons-logging 208 | 209 | 210 | 211 | 212 | junit 213 | junit 214 | 215 | 216 | org.glassfish.tyrus.bundles 217 | tyrus-standalone-client-jdk 218 | 1.13.1 219 | test 220 | 221 | 222 | 223 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/Constants.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.io.UnsupportedEncodingException; 22 | 23 | import org.apache.mina.core.buffer.IoBuffer; 24 | 25 | /** 26 | * Convenience class for holding constants. 27 | * 28 | * @author Paul Gregoire 29 | */ 30 | public class Constants { 31 | 32 | public static final String MANAGER = "ws.manager"; 33 | 34 | public static final String SCOPE = "ws.scope"; 35 | 36 | public final static String CONNECTION = "ws.connection"; 37 | 38 | public final static String SESSION = "session"; 39 | 40 | public static final Object WS_HANDSHAKE = "ws.handshake"; 41 | 42 | public final static String WS_HEADER_KEY = "Sec-WebSocket-Key"; 43 | 44 | public final static String WS_HEADER_VERSION = "Sec-WebSocket-Version"; 45 | 46 | public final static String WS_HEADER_EXTENSIONS = "Sec-WebSocket-Extensions"; 47 | 48 | public final static String WS_HEADER_PROTOCOL = "Sec-WebSocket-Protocol"; 49 | 50 | public final static String HTTP_HEADER_HOST = "Host"; 51 | 52 | public final static String HTTP_HEADER_ORIGIN = "Origin"; 53 | 54 | public final static String HTTP_HEADER_USERAGENT = "User-Agent"; 55 | 56 | public final static String WS_HEADER_FORWARDED = "X-Forwarded-For"; 57 | 58 | public final static String WS_HEADER_REAL_IP = "X-Real-IP"; 59 | 60 | public final static String WS_HEADER_GENERIC_PREFIX = "X-"; 61 | 62 | public static final String URI_QS_PARAMETERS = "querystring-parameters"; 63 | 64 | // used to determine if close message was written 65 | public static final String STATUS_CLOSE_WRITTEN = "close.written"; 66 | 67 | // magic string for websockets 68 | public static final String WEBSOCKET_MAGIC_STRING = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; 69 | 70 | public static final byte[] CRLF = { 0x0d, 0x0a }; 71 | 72 | public static final byte[] END_OF_REQ = { 0x0d, 0x0a, 0x0d, 0x0a }; 73 | 74 | // simple text content to go with our close message / packet 75 | public static final byte[] CLOSE_MESSAGE_BYTES; 76 | 77 | public static final Boolean HANDSHAKE_COMPLETE = Boolean.TRUE; 78 | 79 | public static final String IDLE_COUNTER = "idle.counter"; 80 | 81 | static { 82 | IoBuffer buf = IoBuffer.allocate(16); 83 | buf.setAutoExpand(true); 84 | // 2 byte unsigned code per rfc6455 5.5.1 85 | buf.putUnsigned((short) 1000); // normal close 86 | // this should never fail, but never say never... 87 | try { 88 | buf.put("Normal close".getBytes("UTF8")); 89 | } catch (UnsupportedEncodingException e) { 90 | e.printStackTrace(); 91 | } 92 | buf.flip(); 93 | CLOSE_MESSAGE_BYTES = new byte[buf.remaining()]; 94 | buf.get(CLOSE_MESSAGE_BYTES); 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/SecureWebSocketConfiguration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.io.File; 22 | import java.io.NotActiveException; 23 | import java.security.KeyStore; 24 | import java.security.Provider; 25 | import java.security.Security; 26 | import java.util.ArrayList; 27 | import java.util.Arrays; 28 | import java.util.Collection; 29 | import java.util.List; 30 | 31 | import javax.net.ssl.SNIHostName; 32 | import javax.net.ssl.SNIMatcher; 33 | import javax.net.ssl.SNIServerName; 34 | import javax.net.ssl.SSLContext; 35 | import javax.net.ssl.SSLParameters; 36 | 37 | import org.apache.mina.filter.ssl.KeyStoreFactory; 38 | import org.apache.mina.filter.ssl.SslContextFactory; 39 | import org.apache.mina.filter.ssl.SslFilter; 40 | import org.bouncycastle.jce.provider.BouncyCastleProvider; 41 | import org.slf4j.Logger; 42 | import org.slf4j.LoggerFactory; 43 | 44 | /** 45 | * Provides configuration support for WSS protocol. 46 | * 47 | * @author Paul Gregoire (mondain@gmail.com) 48 | */ 49 | public class SecureWebSocketConfiguration { 50 | 51 | private static Logger log = LoggerFactory.getLogger(SecureWebSocketConfiguration.class); 52 | 53 | /** 54 | * Password for accessing the keystore. 55 | */ 56 | private String keystorePassword; 57 | 58 | /** 59 | * Password for accessing the truststore. 60 | */ 61 | private String truststorePassword; 62 | 63 | /** 64 | * Stores the keystore path. 65 | */ 66 | private String keystoreFile; 67 | 68 | /** 69 | * Stores the truststore path. 70 | */ 71 | private String truststoreFile; 72 | 73 | /** 74 | * Names of the SSL cipher suites which are currently enabled for use. 75 | */ 76 | private String[] cipherSuites; 77 | 78 | /** 79 | * Names of the protocol versions which are currently enabled for use. 80 | */ 81 | private String[] protocols; 82 | 83 | static { 84 | // add bouncycastle security provider 85 | //int insertedAt = Security.insertProviderAt(new BouncyCastleProvider(), 1); 86 | //log.debug("BC provider inserted at position: {}", insertedAt); 87 | // org.bouncycastle.jsse.provider.BouncyCastleJsseProvider 88 | //insertedAt = Security.insertProviderAt(new BouncyCastleJsseProvider(), 2); 89 | //log.debug("BC JSSE provider inserted at position: {}", insertedAt); 90 | Security.addProvider(new BouncyCastleProvider()); 91 | if (log.isDebugEnabled()) { 92 | Provider[] providers = Security.getProviders(); 93 | for (Provider provider : providers) { 94 | log.debug("Provider: {} = {}", provider.getName(), provider.getInfo()); 95 | } 96 | } 97 | } 98 | 99 | public SslFilter getSslFilter() throws Exception { 100 | if (keystoreFile == null || truststoreFile == null) { 101 | throw new NotActiveException("Keystore or truststore are null"); 102 | } 103 | SSLContext context = getSslContext(); 104 | if (context == null) { 105 | throw new NotActiveException("SSLContext is null"); 106 | } 107 | // create the ssl filter using server mode 108 | SslFilter sslFilter = new SslFilter(context); 109 | if (cipherSuites != null) { 110 | sslFilter.setEnabledCipherSuites(cipherSuites); 111 | } 112 | if (protocols != null) { 113 | if (log.isDebugEnabled()) { 114 | log.debug("Using these protocols: {}", Arrays.toString(protocols)); 115 | } 116 | sslFilter.setEnabledProtocols(protocols); 117 | } 118 | return sslFilter; 119 | } 120 | 121 | private SSLContext getSslContext() { 122 | // create the ssl context 123 | SSLContext sslContext = null; 124 | try { 125 | log.debug("Keystore: {}", keystoreFile); 126 | File keyStore = new File(keystoreFile); 127 | log.trace("Keystore - read: {} path: {}", keyStore.canRead(), keyStore.getCanonicalPath()); 128 | log.debug("Truststore: {}", truststoreFile); 129 | File trustStore = new File(truststoreFile); 130 | log.trace("Truststore - read: {} path: {}", trustStore.canRead(), trustStore.getCanonicalPath()); 131 | if (keyStore.exists() && trustStore.exists()) { 132 | // ssl context factory 133 | final SslContextFactory sslContextFactory = new SslContextFactory(); 134 | /* Skip all the provider overrides until we get the service startup bug figured out 135 | // enforce TLSv1.2 otherwise we may get a lesser protocol 136 | sslContextFactory.setProtocol("TLSv1.2"); 137 | // get provider 138 | Provider prov = Security.getProvider(BouncyCastleJsseProvider.PROVIDER_NAME); 139 | if (prov != null) { 140 | if (log.isDebugEnabled()) { 141 | for (String prop : prov.stringPropertyNames()) { 142 | log.debug("Property name: {}", prop); 143 | } 144 | Set svcs = prov.getServices(); 145 | for (Service svc : svcs) { 146 | log.debug("Service - type: {} class: {}", svc.getType(), svc.getClassName()); 147 | } 148 | } 149 | sslContextFactory.setProvider(BouncyCastleJsseProvider.PROVIDER_NAME); 150 | //keyStoreFactory.setProvider(BouncyCastleJsseProvider.PROVIDER_NAME); 151 | //trustStoreFactory.setProvider(BouncyCastleJsseProvider.PROVIDER_NAME); 152 | } else { 153 | // use whatever default is available 154 | log.debug("BouncyCastleJsseProvider not found"); 155 | } 156 | */ 157 | // keystore 158 | final KeyStoreFactory keyStoreFactory = new KeyStoreFactory(); 159 | keyStoreFactory.setDataFile(keyStore); 160 | keyStoreFactory.setPassword(keystorePassword); 161 | // truststore 162 | final KeyStoreFactory trustStoreFactory = new KeyStoreFactory(); 163 | trustStoreFactory.setDataFile(trustStore); 164 | trustStoreFactory.setPassword(truststorePassword); 165 | // get keystore 166 | final KeyStore ks = keyStoreFactory.newInstance(); 167 | sslContextFactory.setKeyManagerFactoryKeyStore(ks); 168 | // get truststore 169 | final KeyStore ts = trustStoreFactory.newInstance(); 170 | sslContextFactory.setTrustManagerFactoryKeyStore(ts); 171 | sslContextFactory.setKeyManagerFactoryKeyStorePassword(keystorePassword); 172 | // get ssl context 173 | sslContext = sslContextFactory.newInstance(); 174 | log.debug("SSL provider: {}", sslContext.getProvider()); 175 | // SNI state 176 | boolean sniEnabled = Boolean.valueOf(System.getProperty("jsse.enableSNIExtension", "false")); 177 | // get ssl context parameters 178 | SSLParameters params = sslContext.getDefaultSSLParameters(); 179 | if (log.isDebugEnabled()) { 180 | log.debug("SSL context params - need client auth: {} want client auth: {} endpoint id algorithm: {}", params.getNeedClientAuth(), params.getWantClientAuth(), params.getEndpointIdentificationAlgorithm()); 181 | String[] supportedProtocols = params.getProtocols(); 182 | if (supportedProtocols != null) { 183 | for (String protocol : supportedProtocols) { 184 | log.debug("SSL context supported protocol: {}", protocol); 185 | } 186 | } else { 187 | log.debug("No protocols"); 188 | } 189 | String[] supportedCiphers = params.getCipherSuites(); 190 | if (supportedCiphers != null) { 191 | for (String cipher : supportedCiphers) { 192 | log.debug("SSL context supported cipher: {}", cipher); 193 | } 194 | } else { 195 | log.debug("No ciphers"); 196 | } 197 | // http://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SNIExamples 198 | // https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html#SNIExtension 199 | log.debug("SNI extension enabled: {}", sniEnabled); 200 | List serverNames = params.getServerNames(); 201 | if (serverNames != null) { 202 | for (SNIServerName sname : serverNames) { 203 | log.debug("SNI server name: {}", sname); 204 | } 205 | } else { 206 | log.debug("No SNI server names specified"); 207 | } 208 | Collection sniMatchers = params.getSNIMatchers(); 209 | if (sniMatchers != null) { 210 | for (SNIMatcher sniMatcher : sniMatchers) { 211 | log.debug("SNI matcher: {}", sniMatcher); 212 | } 213 | } else { 214 | log.debug("No SNI matchers specified"); 215 | } 216 | } 217 | if (sniEnabled) { 218 | SNIMatcher matcher = SNIHostName.createSNIMatcher(""); 219 | Collection matchers = new ArrayList<>(1); 220 | matchers.add(matcher); 221 | params.setSNIMatchers(matchers); 222 | } 223 | } else { 224 | log.warn("Keystore or Truststore file does not exist"); 225 | } 226 | } catch (Exception ex) { 227 | log.error("Exception getting SSL context", ex); 228 | } 229 | return sslContext; 230 | } 231 | 232 | /** 233 | * Password used to access the keystore file. 234 | * 235 | * @param password 236 | */ 237 | public void setKeystorePassword(String password) { 238 | this.keystorePassword = password; 239 | } 240 | 241 | /** 242 | * Password used to access the truststore file. 243 | * 244 | * @param password 245 | */ 246 | public void setTruststorePassword(String password) { 247 | this.truststorePassword = password; 248 | } 249 | 250 | /** 251 | * Set keystore data from a file. 252 | * 253 | * @param path 254 | * contains keystore 255 | */ 256 | public void setKeystoreFile(String path) { 257 | this.keystoreFile = path; 258 | } 259 | 260 | /** 261 | * Set truststore file path. 262 | * 263 | * @param path 264 | * contains truststore 265 | */ 266 | public void setTruststoreFile(String path) { 267 | this.truststoreFile = path; 268 | } 269 | 270 | public String[] getCipherSuites() { 271 | return cipherSuites; 272 | } 273 | 274 | public void setCipherSuites(String[] cipherSuites) { 275 | this.cipherSuites = cipherSuites; 276 | } 277 | 278 | public String[] getProtocols() { 279 | return protocols; 280 | } 281 | 282 | public void setProtocols(String[] protocols) { 283 | this.protocols = protocols; 284 | } 285 | 286 | } 287 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketConnection.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.io.UnsupportedEncodingException; 22 | import java.util.ArrayList; 23 | import java.util.Arrays; 24 | import java.util.List; 25 | import java.util.Map; 26 | import java.util.Optional; 27 | import java.util.concurrent.ConcurrentLinkedQueue; 28 | import java.util.concurrent.atomic.AtomicBoolean; 29 | 30 | import org.apache.commons.lang3.StringUtils; 31 | import org.apache.mina.core.buffer.IoBuffer; 32 | import org.apache.mina.core.future.CloseFuture; 33 | import org.apache.mina.core.future.IoFutureListener; 34 | import org.apache.mina.core.future.WriteFuture; 35 | import org.apache.mina.core.session.IoSession; 36 | import org.red5.net.websocket.model.ConnectionType; 37 | import org.red5.net.websocket.model.HandshakeResponse; 38 | import org.red5.net.websocket.model.MessageType; 39 | import org.red5.net.websocket.model.Packet; 40 | import org.red5.net.websocket.model.WSMessage; 41 | import org.red5.net.websocket.util.IdGenerator; 42 | import org.red5.server.plugin.PluginRegistry; 43 | import org.slf4j.Logger; 44 | import org.slf4j.LoggerFactory; 45 | 46 | /** 47 | * WebSocketConnection
48 | * This class represents a WebSocket connection with a client (browser).
49 | * {@link https://tools.ietf.org/html/rfc6455} 50 | * 51 | * @author Paul Gregoire 52 | */ 53 | public class WebSocketConnection { 54 | 55 | private static final Logger log = LoggerFactory.getLogger(WebSocketConnection.class); 56 | 57 | // session id 58 | private final long id = IdGenerator.generateId(); 59 | 60 | // type of this connection; default is web / http 61 | private ConnectionType type = ConnectionType.WEB; 62 | 63 | private AtomicBoolean connected = new AtomicBoolean(false); 64 | 65 | private IoSession session; 66 | 67 | private String host; 68 | 69 | private String path; 70 | 71 | private String origin; 72 | 73 | // secure or not 74 | private boolean secure; 75 | 76 | /** 77 | * Contains http headers and other web-socket information from the initial request. 78 | */ 79 | private Map headers; 80 | 81 | /** 82 | * Contains uri parameters from the initial request. 83 | */ 84 | private Map querystringParameters; 85 | 86 | /** 87 | * Extensions enabled on this connection. 88 | */ 89 | private Map extensions; 90 | 91 | /** 92 | * Connection protocol (ex. chat, json, etc) 93 | */ 94 | private String protocol; 95 | 96 | /** 97 | * Policy enforcement. 98 | */ 99 | private boolean sameOriginPolicy, crossOriginPolicy; 100 | 101 | /** 102 | * Allowed origins. 103 | */ 104 | private List allowedOrigins; 105 | 106 | /** 107 | * Timeout to wait for the handshake response to be written. 108 | */ 109 | private long handshakeWriteTimeout; 110 | 111 | /** 112 | * Timeout to wait for handshake latch to be completed. Used to prevent sending to an socket that's not ready. 113 | */ 114 | private long latchTimeout; 115 | 116 | // temporary send queue 117 | private ConcurrentLinkedQueue queue = new ConcurrentLinkedQueue<>(); 118 | 119 | private WriteFuture handshakeWriteFuture; 120 | 121 | /** 122 | * constructor 123 | */ 124 | public WebSocketConnection(IoSession session) { 125 | this.session = session; 126 | // store connection in the current session 127 | session.setAttribute(Constants.CONNECTION, this); 128 | // use initial configuration from WebSocketTransport 129 | sameOriginPolicy = WebSocketTransport.isSameOriginPolicy(); 130 | crossOriginPolicy = WebSocketTransport.isCrossOriginPolicy(); 131 | if (crossOriginPolicy) { 132 | allowedOrigins = new ArrayList<>(); 133 | for (String origin : WebSocketTransport.getAllowedOrigins()) { 134 | allowedOrigins.add(origin); 135 | } 136 | log.debug("allowedOrigins: {}", allowedOrigins); 137 | } 138 | handshakeWriteTimeout = WebSocketTransport.getHandshakeWriteTimeout(); 139 | latchTimeout = WebSocketTransport.getLatchTimeout(); 140 | } 141 | 142 | /** 143 | * Receive data from a client. 144 | * 145 | * @param message 146 | */ 147 | public void receive(WSMessage message) { 148 | log.trace("receive message"); 149 | if (isConnected()) { 150 | WebSocketPlugin plugin = (WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin"); 151 | Optional optional = Optional.ofNullable((WebSocketScopeManager) session.getAttribute(Constants.MANAGER)); 152 | WebSocketScopeManager manager = optional.isPresent() ? optional.get() : plugin.getManager(path); 153 | WebSocketScope scope = manager.getScope(path); 154 | scope.onMessage(message); 155 | } else { 156 | log.warn("Not connected"); 157 | } 158 | } 159 | 160 | /** 161 | * Sends the handshake response. 162 | * 163 | * @param wsResponse 164 | */ 165 | public void sendHandshakeResponse(HandshakeResponse wsResponse) { 166 | log.debug("Writing handshake on session: {}", session.getId()); 167 | // create write future 168 | handshakeWriteFuture = session.write(wsResponse); 169 | handshakeWriteFuture.addListener(new IoFutureListener() { 170 | 171 | @Override 172 | public void operationComplete(WriteFuture future) { 173 | IoSession sess = future.getSession(); 174 | if (future.isWritten()) { 175 | // handshake is finished 176 | log.debug("Handshake write success! {}", sess.getId()); 177 | // set completed flag 178 | sess.setAttribute(Constants.HANDSHAKE_COMPLETE); 179 | // set connected state on ws connection 180 | if (connected.compareAndSet(false, true)) { 181 | try { 182 | // send queued packets 183 | queue.forEach(entry -> { 184 | sess.write(entry); 185 | queue.remove(entry); 186 | }); 187 | } catch (Exception e) { 188 | log.warn("Exception draining queued packets on session: {}", sess.getId(), e); 189 | } 190 | } 191 | } else { 192 | log.warn("Handshake write failed from: {} to: {}", sess.getLocalAddress(), sess.getRemoteAddress()); 193 | } 194 | } 195 | 196 | }); 197 | } 198 | 199 | /** 200 | * Sends WebSocket packet to the client. 201 | * 202 | * @param packet 203 | */ 204 | public void send(Packet packet) { 205 | if (log.isTraceEnabled()) { 206 | log.trace("send packet: {}", packet); 207 | } 208 | // no handshake flag, queue the packet 209 | if (session.containsAttribute(Constants.HANDSHAKE_COMPLETE)) { 210 | try { 211 | // clear any queued items first 212 | queue.forEach(entry -> { 213 | session.write(entry); 214 | queue.remove(entry); 215 | }); 216 | } catch (Exception e) { 217 | log.warn("Exception draining queued packets on session: {}", session.getId(), e); 218 | } 219 | // process the incoming packet 220 | session.write(packet); 221 | } else { 222 | if (handshakeWriteFuture != null) { 223 | log.warn("Handshake is not complete yet on session: {} written? {}", session.getId(), handshakeWriteFuture.isWritten(), handshakeWriteFuture.getException()); 224 | } else { 225 | log.warn("Handshake is not complete yet on session: {}", session.getId()); 226 | } 227 | // not queuing pings 228 | MessageType type = packet.getType(); 229 | if (type != MessageType.PING && type != MessageType.PONG) { 230 | log.info("Placing {} message in session: {} queue", type, session.getId()); 231 | queue.offer(packet); 232 | } 233 | } 234 | } 235 | 236 | /** 237 | * Sends text to the client. 238 | * 239 | * @param data 240 | * string data 241 | * @throws UnsupportedEncodingException 242 | */ 243 | public void send(String data) throws UnsupportedEncodingException { 244 | log.trace("send message: {}", data); 245 | // process the incoming string 246 | if (StringUtils.isNotBlank(data)) { 247 | send(Packet.build(data.getBytes("UTF8"), MessageType.TEXT)); 248 | } else { 249 | throw new UnsupportedEncodingException("Cannot send a null string"); 250 | } 251 | } 252 | 253 | /** 254 | * Sends binary data to the client. 255 | * 256 | * @param buf 257 | */ 258 | public void send(byte[] buf) { 259 | if (log.isTraceEnabled()) { 260 | log.trace("send binary: {}", Arrays.toString(buf)); 261 | } 262 | // send the incoming bytes 263 | send(Packet.build(buf)); 264 | } 265 | 266 | /** 267 | * Sends a ping to the client. 268 | * 269 | * @param buf 270 | */ 271 | public void sendPing(byte[] buf) { 272 | if (log.isTraceEnabled()) { 273 | log.trace("send ping: {}", buf); 274 | } 275 | // send ping 276 | send(Packet.build(buf, MessageType.PING)); 277 | } 278 | 279 | /** 280 | * Sends a pong back to the client; normally in response to a ping. 281 | * 282 | * @param buf 283 | */ 284 | public void sendPong(byte[] buf) { 285 | if (log.isTraceEnabled()) { 286 | log.trace("send pong: {}", buf); 287 | } 288 | // send pong 289 | send(Packet.build(buf, MessageType.PONG)); 290 | } 291 | 292 | /** 293 | * close Connection 294 | */ 295 | public void close() { 296 | if (connected.compareAndSet(true, false)) { 297 | // remove handshake flag 298 | session.removeAttribute(Constants.HANDSHAKE_COMPLETE); 299 | // clear the delay queue 300 | queue.clear(); 301 | // whether to attempt a nice close or a forceful one 302 | if (WebSocketTransport.isNiceClose()) { 303 | // send a proper ws close 304 | Packet packet = Packet.build(Constants.CLOSE_MESSAGE_BYTES, MessageType.CLOSE); 305 | WriteFuture writeFuture = session.write(packet); 306 | writeFuture.addListener(new IoFutureListener() { 307 | 308 | @Override 309 | public void operationComplete(WriteFuture future) { 310 | if (future.isWritten()) { 311 | log.debug("Close message written"); 312 | // only set on success for now to skip boolean check later 313 | session.setAttribute(Constants.STATUS_CLOSE_WRITTEN, Boolean.TRUE); 314 | } 315 | future.removeListener(this); 316 | } 317 | 318 | }); 319 | // adjust close routine to allow for flushing 320 | CloseFuture closeFuture = session.closeOnFlush(); 321 | closeFuture.addListener(new IoFutureListener() { 322 | 323 | public void operationComplete(CloseFuture future) { 324 | if (future.isClosed()) { 325 | log.debug("Connection is closed"); 326 | } else { 327 | log.debug("Connection is not yet closed"); 328 | } 329 | future.removeListener(this); 330 | } 331 | 332 | }); 333 | } else { 334 | // force close 335 | CloseFuture closeFuture = session.closeNow(); 336 | closeFuture.addListener(new IoFutureListener() { 337 | 338 | public void operationComplete(CloseFuture future) { 339 | if (future.isClosed()) { 340 | log.debug("Connection is closed"); 341 | } else { 342 | log.debug("Connection is not yet closed"); 343 | } 344 | future.removeListener(this); 345 | } 346 | 347 | }); 348 | } 349 | } 350 | } 351 | 352 | /** 353 | * Close with an associated error status. 354 | * 355 | * @param statusCode 356 | * @param errResponse 357 | */ 358 | public void close(int statusCode, HandshakeResponse errResponse) { 359 | log.warn("Closing connection with status: {}", statusCode); 360 | // remove handshake flag 361 | session.removeAttribute(Constants.HANDSHAKE_COMPLETE); 362 | // clear the delay queue 363 | queue.clear(); 364 | // send http error response 365 | session.write(errResponse); 366 | // whether to attempt a nice close or a forceful one 367 | if (WebSocketTransport.isNiceClose()) { 368 | // now send close packet with error code 369 | IoBuffer buf = IoBuffer.allocate(16); 370 | buf.setAutoExpand(true); 371 | // all errors except 403 will use 1002 372 | buf.putUnsigned((short) statusCode); 373 | try { 374 | if (statusCode == 1008) { 375 | // if its a 403 forbidden 376 | buf.put("Policy Violation".getBytes("UTF8")); 377 | } else { 378 | buf.put("Protocol error".getBytes("UTF8")); 379 | } 380 | } catch (Exception e) { 381 | // shouldnt be any text encoding issues... 382 | } 383 | buf.flip(); 384 | byte[] errBytes = new byte[buf.remaining()]; 385 | buf.get(errBytes); 386 | // construct the packet 387 | Packet packet = Packet.build(errBytes, MessageType.CLOSE); 388 | WriteFuture writeFuture = session.write(packet); 389 | writeFuture.addListener(new IoFutureListener() { 390 | 391 | @Override 392 | public void operationComplete(WriteFuture future) { 393 | if (future.isWritten()) { 394 | log.debug("Close message written"); 395 | // only set on success for now to skip boolean check later 396 | session.setAttribute(Constants.STATUS_CLOSE_WRITTEN, Boolean.TRUE); 397 | } 398 | future.removeListener(this); 399 | } 400 | 401 | }); 402 | // adjust close routine to allow for flushing 403 | CloseFuture closeFuture = session.closeOnFlush(); 404 | closeFuture.addListener(new IoFutureListener() { 405 | 406 | public void operationComplete(CloseFuture future) { 407 | if (future.isClosed()) { 408 | log.debug("Connection is closed"); 409 | } else { 410 | log.debug("Connection is not yet closed"); 411 | } 412 | future.removeListener(this); 413 | } 414 | 415 | }); 416 | } else { 417 | // force close 418 | CloseFuture closeFuture = session.closeNow(); 419 | closeFuture.addListener(new IoFutureListener() { 420 | 421 | public void operationComplete(CloseFuture future) { 422 | if (future.isClosed()) { 423 | log.debug("Connection is closed"); 424 | } else { 425 | log.debug("Connection is not yet closed"); 426 | } 427 | future.removeListener(this); 428 | } 429 | 430 | }); 431 | } 432 | log.debug("Close complete"); 433 | } 434 | 435 | public ConnectionType getType() { 436 | return type; 437 | } 438 | 439 | public void setType(ConnectionType type) { 440 | this.type = type; 441 | } 442 | 443 | /** 444 | * @return the connected 445 | */ 446 | public boolean isConnected() { 447 | return connected.get(); 448 | } 449 | 450 | /** 451 | * On connected, set flag. 452 | */ 453 | public void setConnected() { 454 | connected.compareAndSet(false, true); 455 | } 456 | 457 | /** 458 | * @return the host 459 | */ 460 | public String getHost() { 461 | return String.format("%s://%s%s", (secure ? "wss" : "ws"), host, path); 462 | } 463 | 464 | /** 465 | * @param host 466 | * the host to set 467 | */ 468 | public void setHost(String host) { 469 | this.host = host; 470 | } 471 | 472 | /** 473 | * @return the origin 474 | */ 475 | public String getOrigin() { 476 | return origin; 477 | } 478 | 479 | /** 480 | * @param origin 481 | * the origin to set 482 | */ 483 | public void setOrigin(String origin) { 484 | this.origin = origin; 485 | } 486 | 487 | public boolean isSecure() { 488 | return secure; 489 | } 490 | 491 | public void setSecure(boolean secure) { 492 | this.secure = secure; 493 | } 494 | 495 | /** 496 | * @return the session 497 | */ 498 | public IoSession getSession() { 499 | return session; 500 | } 501 | 502 | public String getPath() { 503 | return path; 504 | } 505 | 506 | /** 507 | * @param path 508 | * the path to set 509 | */ 510 | public void setPath(String path) { 511 | if (path.charAt(path.length() - 1) == '/') { 512 | this.path = path.substring(0, path.length() - 1); 513 | } else { 514 | this.path = path; 515 | } 516 | } 517 | 518 | /** 519 | * Returns the connection id. 520 | * 521 | * @return id 522 | */ 523 | public long getId() { 524 | return id; 525 | } 526 | 527 | /** 528 | * Returns true if this connection is a web-based connection. 529 | * 530 | * @return true if web and false if direct 531 | */ 532 | public boolean isWebConnection() { 533 | return type == ConnectionType.WEB; 534 | } 535 | 536 | /** 537 | * Sets the incoming headers. 538 | * 539 | * @param headers 540 | */ 541 | public void setHeaders(Map headers) { 542 | this.headers = headers; 543 | } 544 | 545 | public Map getHeaders() { 546 | return headers; 547 | } 548 | 549 | public Map getQuerystringParameters() { 550 | return querystringParameters; 551 | } 552 | 553 | public void setQuerystringParameters(Map querystringParameters) { 554 | this.querystringParameters = querystringParameters; 555 | } 556 | 557 | /** 558 | * Returns whether or not extensions are enabled on this connection. 559 | * 560 | * @return true if extensions are enabled, false otherwise 561 | */ 562 | public boolean hasExtensions() { 563 | return extensions != null && !extensions.isEmpty(); 564 | } 565 | 566 | /** 567 | * Returns enabled extensions. 568 | * 569 | * @return extensions 570 | */ 571 | public Map getExtensions() { 572 | return extensions; 573 | } 574 | 575 | /** 576 | * Sets the extensions. 577 | * 578 | * @param extensions 579 | */ 580 | public void setExtensions(Map extensions) { 581 | this.extensions = extensions; 582 | } 583 | 584 | /** 585 | * Returns the extensions list as a comma separated string as specified by the rfc. 586 | * 587 | * @return extension list string or null if no extensions are enabled 588 | */ 589 | public String getExtensionsAsString() { 590 | String extensionsList = null; 591 | if (extensions != null) { 592 | StringBuilder sb = new StringBuilder(); 593 | for (String key : extensions.keySet()) { 594 | sb.append(key); 595 | sb.append("; "); 596 | } 597 | extensionsList = sb.toString().trim(); 598 | } 599 | return extensionsList; 600 | } 601 | 602 | /** 603 | * Returns whether or not a protocol is enabled on this connection. 604 | * 605 | * @return true if protocol is enabled, false otherwise 606 | */ 607 | public boolean hasProtocol() { 608 | return protocol != null; 609 | } 610 | 611 | /** 612 | * Returns the protocol enabled on this connection. 613 | * 614 | * @return protocol 615 | */ 616 | public String getProtocol() { 617 | return protocol; 618 | } 619 | 620 | /** 621 | * Sets the protocol. 622 | * 623 | * @param protocol 624 | */ 625 | public void setProtocol(String protocol) { 626 | this.protocol = protocol; 627 | } 628 | 629 | public long getHandshakeWriteTimeout() { 630 | return handshakeWriteTimeout; 631 | } 632 | 633 | public void setHandshakeWriteTimeout(long handshakeWriteTimeout) { 634 | this.handshakeWriteTimeout = handshakeWriteTimeout; 635 | } 636 | 637 | public long getLatchTimeout() { 638 | return latchTimeout; 639 | } 640 | 641 | public void setLatchTimeout(long latchTimeout) { 642 | this.latchTimeout = latchTimeout; 643 | } 644 | 645 | public boolean isSameOriginPolicy() { 646 | return sameOriginPolicy; 647 | } 648 | 649 | public boolean isCrossOriginPolicy() { 650 | return crossOriginPolicy; 651 | } 652 | 653 | public void addOrigin(String origin) { 654 | if (allowedOrigins == null) { 655 | allowedOrigins = new ArrayList<>(); 656 | } 657 | allowedOrigins.add(origin); 658 | } 659 | 660 | public boolean removeOrigin(String origin) { 661 | return allowedOrigins.remove(origin); 662 | } 663 | 664 | public void clearOrigins() { 665 | allowedOrigins.clear(); 666 | } 667 | 668 | public boolean isValidOrigin(String origin) { 669 | if (allowedOrigins != null) { 670 | // short-cut 671 | if (allowedOrigins.contains("*")) { 672 | return true; 673 | } 674 | return allowedOrigins.contains(origin); 675 | } 676 | return true; 677 | } 678 | 679 | @Override 680 | public String toString() { 681 | return "WebSocketConnection [id=" + id + ", type=" + type + ", host=" + host + ", origin=" + origin + ", path=" + path + ", secure=" + secure + ", connected=" + connected + ", remote=" + (session != null ? session.getRemoteAddress().toString() : "unk") + "]"; 682 | } 683 | 684 | } 685 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketException.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | /** 22 | * exception for WebSocketPlugin 23 | * 24 | * @author Toda Takahiko 25 | */ 26 | public class WebSocketException extends Exception { 27 | 28 | private static final long serialVersionUID = 3534955883722927241L; 29 | 30 | public WebSocketException(String message) { 31 | super(message); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketHandler.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import org.apache.mina.core.future.CloseFuture; 22 | import org.apache.mina.core.future.IoFutureListener; 23 | import org.apache.mina.core.service.IoHandlerAdapter; 24 | import org.apache.mina.core.session.IdleStatus; 25 | import org.apache.mina.core.session.IoSession; 26 | import org.apache.mina.core.write.WriteRequestQueue; 27 | import org.red5.net.websocket.model.WSMessage; 28 | import org.red5.server.plugin.PluginRegistry; 29 | import org.slf4j.Logger; 30 | import org.slf4j.LoggerFactory; 31 | 32 | /** 33 | * WebSocketHandler 34 | * 35 | *
 36 |  * IoHandlerAdapter for webSocket
 37 |  * 
38 | * 39 | * @author Toda Takahiko 40 | * @author Paul Gregoire 41 | */ 42 | public class WebSocketHandler extends IoHandlerAdapter { 43 | 44 | private static final Logger log = LoggerFactory.getLogger(WebSocketHandler.class); 45 | 46 | /** {@inheritDoc} */ 47 | @Override 48 | public void messageReceived(IoSession session, Object message) throws Exception { 49 | if (log.isTraceEnabled()) { 50 | log.trace("Message received (session: {}) {}", session.getId(), message); 51 | } 52 | if (message instanceof WSMessage) { 53 | WebSocketConnection conn = (WebSocketConnection) session.getAttribute(Constants.CONNECTION); 54 | if (conn != null) { 55 | conn.receive((WSMessage) message); 56 | } 57 | } else { 58 | log.trace("Non-WSMessage received {}", message); 59 | } 60 | } 61 | 62 | /** {@inheritDoc} */ 63 | @Override 64 | public void messageSent(IoSession session, Object message) throws Exception { 65 | if (log.isTraceEnabled()) { 66 | log.trace("Message sent (session: {}) read: {} write: {}\n{}", session.getId(), session.getReadBytes(), session.getWrittenBytes(), String.valueOf(message)); 67 | } 68 | } 69 | 70 | /** {@inheritDoc} */ 71 | @Override 72 | public void sessionIdle(IoSession session, IdleStatus status) throws Exception { 73 | //if (log.isTraceEnabled()) { 74 | log.info("Idle (session: {}) local: {} remote: {}\nread: {} write: {}", session.getId(), session.getLocalAddress(), session.getRemoteAddress(), session.getReadBytes(), session.getWrittenBytes()); 75 | //} 76 | int idleCount = 1; 77 | if (session.containsAttribute(Constants.IDLE_COUNTER)) { 78 | idleCount = (int) session.getAttribute(Constants.IDLE_COUNTER); 79 | idleCount += 1; 80 | } else { 81 | session.setAttribute(Constants.IDLE_COUNTER, idleCount); 82 | } 83 | // get the existing reference to a ws connection 84 | WebSocketConnection conn = (WebSocketConnection) session.getAttribute(Constants.CONNECTION); 85 | // after the first idle we force-close 86 | if (conn != null && idleCount == 1) { 87 | // close the idle socket 88 | conn.close(); 89 | } else { 90 | log.info("Force closing idle session: {}", session); 91 | // clear write queue 92 | WriteRequestQueue writeQueue = session.getWriteRequestQueue(); 93 | if (!writeQueue.isEmpty(session)) { 94 | writeQueue.clear(session); 95 | } 96 | // force close the session 97 | final CloseFuture future = session.closeNow(); 98 | IoFutureListener listener = new IoFutureListener() { 99 | 100 | public void operationComplete(CloseFuture future) { 101 | // now connection should be closed 102 | log.info("Close operation completed {}: {}", session.getId(), future.isClosed()); 103 | future.removeListener(this); 104 | } 105 | 106 | }; 107 | future.addListener(listener); 108 | } 109 | } 110 | 111 | /** {@inheritDoc} */ 112 | @Override 113 | public void sessionClosed(IoSession session) throws Exception { 114 | log.trace("Session {} closed", session.getId()); 115 | // remove connection from scope 116 | WebSocketConnection conn = (WebSocketConnection) session.removeAttribute(Constants.CONNECTION); 117 | if (conn != null) { 118 | // remove from the manager 119 | WebSocketPlugin plugin = (WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin"); 120 | if (plugin != null) { 121 | String path = conn.getPath(); 122 | if (path != null) { 123 | WebSocketScopeManager manager = (WebSocketScopeManager) session.removeAttribute(Constants.MANAGER); 124 | if (manager == null) { 125 | manager = plugin.getManager(path); 126 | } 127 | if (manager != null) { 128 | manager.removeConnection(conn); 129 | } else { 130 | log.debug("WebSocket manager was not found"); 131 | } 132 | } else { 133 | log.debug("WebSocket connection path was null"); 134 | } 135 | } else { 136 | log.debug("WebSocket plugin was not found"); 137 | } 138 | } else { 139 | log.debug("WebSocket connection was null"); 140 | } 141 | super.sessionClosed(session); 142 | } 143 | 144 | /** {@inheritDoc} */ 145 | @Override 146 | public void exceptionCaught(IoSession session, Throwable cause) throws Exception { 147 | log.warn("Exception (session: {})", session.getId(), cause); 148 | // get the existing reference to a ws connection 149 | WebSocketConnection conn = (WebSocketConnection) session.getAttribute(Constants.CONNECTION); 150 | if (conn != null) { 151 | // close the socket 152 | conn.close(); 153 | } 154 | } 155 | 156 | } 157 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketPlugin.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.util.Arrays; 22 | import java.util.Map.Entry; 23 | import java.util.concurrent.ConcurrentHashMap; 24 | import java.util.concurrent.ConcurrentMap; 25 | 26 | import org.red5.server.Server; 27 | import org.red5.server.adapter.MultiThreadedApplicationAdapter; 28 | import org.red5.server.api.scope.IScope; 29 | import org.red5.server.plugin.Red5Plugin; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | /** 34 | * WebSocketPlugin 35 | *
36 | * This program will be called by red5 PluginLauncher and hold the application Context or Application Adapter 37 | * 38 | * @author Toda Takahiko 39 | * @author Paul Gregoire 40 | */ 41 | public class WebSocketPlugin extends Red5Plugin { 42 | 43 | private Logger log = LoggerFactory.getLogger(WebSocketPlugin.class); 44 | 45 | // holds application scopes and their associated websocket scope manager 46 | private static ConcurrentMap managerMap = new ConcurrentHashMap<>(); 47 | 48 | public WebSocketPlugin() { 49 | log.trace("WebSocketPlugin ctor"); 50 | } 51 | 52 | /** 53 | * {@inheritDoc} 54 | */ 55 | @Override 56 | public void doStart() throws Exception { 57 | super.doStart(); 58 | log.trace("WebSocketPlugin start"); 59 | } 60 | 61 | @Override 62 | public void doStop() throws Exception { 63 | if (!managerMap.isEmpty()) { 64 | for (Entry entry : managerMap.entrySet()) { 65 | entry.getValue().stop(); 66 | } 67 | managerMap.clear(); 68 | } 69 | super.doStop(); 70 | } 71 | 72 | /** 73 | * {@inheritDoc} 74 | */ 75 | @Override 76 | public String getName() { 77 | return "WebSocketPlugin"; 78 | } 79 | 80 | /** 81 | * {@inheritDoc} 82 | */ 83 | @Override 84 | public Server getServer() { 85 | return super.getServer(); 86 | } 87 | 88 | /** 89 | * Returns a WebSocketScopeManager for a given scope. 90 | * 91 | * @param scope 92 | * @return WebSocketScopeManager if registered for the given scope and null otherwise 93 | */ 94 | public WebSocketScopeManager getManager(IScope scope) { 95 | return managerMap.get(scope); 96 | } 97 | 98 | /** 99 | * Returns a WebSocketScopeManager for a given path. 100 | * 101 | * @param path 102 | * @return WebSocketScopeManager if registered for the given path and null otherwise 103 | */ 104 | public WebSocketScopeManager getManager(String path) { 105 | log.debug("getManager: {}", path); 106 | // determine what the app scope name is 107 | String[] parts = path.split("\\/"); 108 | if (log.isTraceEnabled()) { 109 | log.trace("Path parts: {}", Arrays.toString(parts)); 110 | } 111 | if (parts.length > 1) { 112 | // skip default in a path if it exists in slot #1 113 | String name = !"default".equals(parts[1]) ? parts[1] : ((parts.length >= 3) ? parts[2] : parts[1]); 114 | if (log.isDebugEnabled()) { 115 | log.debug("Managers: {}", managerMap.entrySet()); 116 | } 117 | for (Entry entry : managerMap.entrySet()) { 118 | IScope appScope = entry.getKey(); 119 | if (appScope.getName().equals(name)) { 120 | log.debug("Application scope name matches path: {}", name); 121 | return entry.getValue(); 122 | } else if (log.isTraceEnabled()) { 123 | log.trace("Application scope name: {} didnt match path: {}", appScope.getName(), name); 124 | } 125 | } 126 | } 127 | return null; 128 | } 129 | 130 | @Deprecated 131 | public WebSocketScopeManager getManager() { 132 | throw new UnsupportedOperationException("Use getManager(IScope scope) instead"); 133 | } 134 | 135 | /** 136 | * Removes and returns the WebSocketScopeManager for the given scope if it exists and returns null if it does not. 137 | * 138 | * @param scope Scope for which the manager is registered 139 | * @return WebSocketScopeManager if registered for the given path and null otherwise 140 | */ 141 | public WebSocketScopeManager removeManager(IScope scope) { 142 | return managerMap.remove(scope); 143 | } 144 | 145 | /** 146 | * {@inheritDoc} 147 | */ 148 | @Override 149 | public void setApplication(MultiThreadedApplicationAdapter application) { 150 | log.info("WebSocketPlugin application: {}", application); 151 | // get the app scope 152 | final IScope appScope = application.getScope(); 153 | // put if not already there 154 | managerMap.putIfAbsent(appScope, new WebSocketScopeManager()); 155 | // add the app scope to the manager 156 | managerMap.get(appScope).setApplication(appScope); 157 | super.setApplication(application); 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketScope.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import java.util.Set; 24 | import java.util.concurrent.CopyOnWriteArraySet; 25 | 26 | import org.red5.net.websocket.listener.IWebSocketDataListener; 27 | import org.red5.net.websocket.model.WSMessage; 28 | import org.red5.server.api.scope.IScope; 29 | import org.red5.server.plugin.PluginRegistry; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | import org.springframework.beans.factory.DisposableBean; 33 | import org.springframework.beans.factory.InitializingBean; 34 | 35 | /** 36 | * WebSocketScope contains an IScope and keeps track of WebSocketConnection and IWebSocketDataListener instances. 37 | * 38 | * @author Paul Gregoire (mondain@gmail.com) 39 | */ 40 | public class WebSocketScope implements InitializingBean, DisposableBean { 41 | 42 | private static Logger log = LoggerFactory.getLogger(WebSocketScope.class); 43 | 44 | protected CopyOnWriteArraySet conns = new CopyOnWriteArraySet<>(); 45 | 46 | protected CopyOnWriteArraySet listeners = new CopyOnWriteArraySet<>(); 47 | 48 | protected IScope scope; 49 | 50 | protected String path = "default"; 51 | 52 | public WebSocketScope() { 53 | } 54 | 55 | public WebSocketScope(IScope scope) { 56 | log.debug("Creating WebSocket scope for: {}", scope); 57 | setScope(scope); 58 | setPath(String.format("/%s", scope.getName())); 59 | } 60 | 61 | @Override 62 | public void afterPropertiesSet() throws Exception { 63 | register(); 64 | } 65 | 66 | @Override 67 | public void destroy() throws Exception { 68 | unregister(); 69 | } 70 | 71 | /** 72 | * Registers with the WebSocketScopeManager. 73 | */ 74 | public void register() { 75 | log.info("Application scope: {}", scope); 76 | // set the logger to the app scope 77 | //log = Red5LoggerFactory.getLogger(WebSocketScope.class, scope.getName()); 78 | WebSocketScopeManager manager = ((WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin")).getManager(scope); 79 | manager.setApplication(scope); 80 | log.info("WebSocket app added: {}", scope.getName()); 81 | manager.addWebSocketScope(this); 82 | log.info("WebSocket scope added"); 83 | } 84 | 85 | /** 86 | * Un-registers from the WebSocketScopeManager. 87 | */ 88 | public void unregister() { 89 | // clean up the connections by first closing them 90 | for (WebSocketConnection conn : conns) { 91 | conn.close(); 92 | } 93 | conns.clear(); 94 | // clean up the listeners by first stopping them 95 | for (IWebSocketDataListener listener : listeners) { 96 | listener.stop(); 97 | } 98 | listeners.clear(); 99 | } 100 | 101 | /** 102 | * Returns the set of connections. 103 | * 104 | * @return conns 105 | */ 106 | public Set getConns() { 107 | return conns; 108 | } 109 | 110 | /** 111 | * Returns the associated scope. 112 | * 113 | * @return scope 114 | */ 115 | public IScope getScope() { 116 | return scope; 117 | } 118 | 119 | /** 120 | * Sets the associated scope. 121 | * 122 | * @param scope 123 | */ 124 | public void setScope(IScope scope) { 125 | this.scope = scope; 126 | // set this ws scope as an attribute for ez lookup 127 | this.scope.setAttribute(Constants.SCOPE, this); 128 | } 129 | 130 | /** 131 | * Sets the path. 132 | * 133 | * @param path 134 | */ 135 | public void setPath(String path) { 136 | this.path = path; // /room/name 137 | } 138 | 139 | /** 140 | * Returns the path of the scope. 141 | * 142 | * @return path 143 | */ 144 | public String getPath() { 145 | return path; 146 | } 147 | 148 | /** 149 | * Add new connection on scope. 150 | * 151 | * @param conn 152 | * WebSocketConnection 153 | */ 154 | public void addConnection(WebSocketConnection conn) { 155 | conns.add(conn); 156 | for (IWebSocketDataListener listener : listeners) { 157 | listener.onWSConnect(conn); 158 | } 159 | } 160 | 161 | /** 162 | * Remove connection from scope. 163 | * 164 | * @param conn 165 | * WebSocketConnection 166 | */ 167 | public void removeConnection(WebSocketConnection conn) { 168 | conns.remove(conn); 169 | for (IWebSocketDataListener listener : listeners) { 170 | listener.onWSDisconnect(conn); 171 | } 172 | } 173 | 174 | /** 175 | * Add new listener on scope. 176 | * 177 | * @param listener 178 | * IWebSocketDataListener 179 | */ 180 | public void addListener(IWebSocketDataListener listener) { 181 | log.info("addListener: {}", listener); 182 | listeners.add(listener); 183 | } 184 | 185 | /** 186 | * Remove listener from scope. 187 | * 188 | * @param listener 189 | * IWebSocketDataListener 190 | */ 191 | public void removeListener(IWebSocketDataListener listener) { 192 | log.info("removeListener: {}", listener); 193 | listeners.remove(listener); 194 | } 195 | 196 | /** 197 | * Add new listeners on scope. 198 | * 199 | * @param listeners 200 | * list of IWebSocketDataListener 201 | */ 202 | public void setListeners(Collection listeners) { 203 | log.trace("setListeners: {}", listeners); 204 | this.listeners.addAll(listeners); 205 | } 206 | 207 | /** 208 | * Returns the listeners in an unmodifiable set. 209 | * 210 | * @return listeners 211 | */ 212 | public Set getListeners() { 213 | return Collections.unmodifiableSet(listeners); 214 | } 215 | 216 | /** 217 | * Check the scope state. 218 | * 219 | * @return true:still have relation 220 | */ 221 | public boolean isValid() { 222 | return (conns.size() + listeners.size()) > 0; 223 | } 224 | 225 | /** 226 | * Message received from client and passed on to the listeners. 227 | * 228 | * @param message 229 | */ 230 | public void onMessage(WSMessage message) { 231 | log.trace("Listeners: {}", listeners.size()); 232 | for (IWebSocketDataListener listener : listeners) { 233 | try { 234 | listener.onWSMessage(message); 235 | } catch (Exception e) { 236 | log.warn("onMessage exception", e); 237 | } 238 | } 239 | } 240 | 241 | @Override 242 | public int hashCode() { 243 | final int prime = 31; 244 | int result = 1; 245 | result = prime * result + ((path == null) ? 0 : path.hashCode()); 246 | return result; 247 | } 248 | 249 | @Override 250 | public boolean equals(Object obj) { 251 | if (this == obj) 252 | return true; 253 | if (obj == null) 254 | return false; 255 | if (getClass() != obj.getClass()) 256 | return false; 257 | WebSocketScope other = (WebSocketScope) obj; 258 | if (path == null) { 259 | if (other.path != null) 260 | return false; 261 | } else if (!path.equals(other.path)) 262 | return false; 263 | return true; 264 | } 265 | 266 | @Override 267 | public String toString() { 268 | return "WebSocketScope [path=" + path + ", listeners=" + listeners.size() + ", connections=" + conns.size() + "]"; 269 | } 270 | 271 | } 272 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketScopeManager.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.util.concurrent.ConcurrentHashMap; 22 | import java.util.concurrent.ConcurrentMap; 23 | import java.util.concurrent.CopyOnWriteArraySet; 24 | 25 | import org.red5.net.websocket.listener.DefaultWebSocketDataListener; 26 | import org.red5.net.websocket.listener.IWebSocketDataListener; 27 | import org.red5.net.websocket.listener.IWebSocketScopeListener; 28 | import org.red5.net.websocket.model.WebSocketEvent; 29 | import org.red5.server.api.IContext; 30 | import org.red5.server.api.scope.IScope; 31 | import org.slf4j.Logger; 32 | import org.slf4j.LoggerFactory; 33 | 34 | /** 35 | * Manages websocket scopes and listeners. 36 | * 37 | * @author Toda Takahiko 38 | * @author Paul Gregoire 39 | */ 40 | public class WebSocketScopeManager { 41 | 42 | private static final Logger log = LoggerFactory.getLogger(WebSocketScopeManager.class); 43 | 44 | // reference to the owning application scope 45 | private IScope appScope; 46 | 47 | // all the ws scopes that the manager is responsible for 48 | private ConcurrentMap scopes = new ConcurrentHashMap<>(); 49 | 50 | // scope listeners 51 | private CopyOnWriteArraySet scopeListeners = new CopyOnWriteArraySet<>(); 52 | 53 | // currently active rooms 54 | private CopyOnWriteArraySet activeRooms = new CopyOnWriteArraySet<>(); 55 | 56 | // whether or not to copy listeners from parent to child on create 57 | protected boolean copyListeners = true; 58 | 59 | public void addListener(IWebSocketScopeListener listener) { 60 | scopeListeners.add(listener); 61 | } 62 | 63 | public void removeListener(IWebSocketScopeListener listener) { 64 | scopeListeners.remove(listener); 65 | } 66 | 67 | /** 68 | * Returns the enable state of a given path. 69 | * 70 | * @param path 71 | * scope / context path 72 | * @return enabled if registered as active and false otherwise 73 | */ 74 | public boolean isEnabled(String path) { 75 | if (path.startsWith("/")) { 76 | // start after the leading slash 77 | int roomSlashPos = path.indexOf('/', 1); 78 | if (roomSlashPos == -1) { 79 | // check application level scope 80 | path = path.substring(1); 81 | } else { 82 | // check room level scope 83 | path = path.substring(1, roomSlashPos); 84 | } 85 | } 86 | boolean enabled = activeRooms.contains(path); 87 | log.debug("Enabled check on path: {} enabled: {}", path, enabled); 88 | return enabled; 89 | } 90 | 91 | /** 92 | * Adds a scope to the enabled applications. 93 | * 94 | * @param scope 95 | * the application scope 96 | */ 97 | public void addScope(IScope scope) { 98 | String app = scope.getName(); 99 | // add the name to the collection (no '/' prefix) 100 | activeRooms.add(app); 101 | // check the context for a predefined websocket scope 102 | IContext ctx = scope.getContext(); 103 | if (ctx != null && ctx.hasBean("webSocketScopeDefault")) { 104 | log.debug("WebSocket scope found in context"); 105 | WebSocketScope wsScope = (WebSocketScope) scope.getContext().getBean("webSocketScopeDefault"); 106 | if (wsScope != null) { 107 | log.trace("Default WebSocketScope has {} listeners", wsScope.getListeners().size()); 108 | } 109 | // add to scopes 110 | scopes.put(String.format("/%s", app), wsScope); 111 | } else { 112 | log.debug("Creating a new scope"); 113 | // add a default scope and listener if none are defined 114 | WebSocketScope wsScope = new WebSocketScope(); 115 | wsScope.setScope(scope); 116 | wsScope.setPath(String.format("/%s", app)); 117 | if (wsScope.getListeners().isEmpty()) { 118 | log.debug("adding default listener"); 119 | wsScope.addListener(new DefaultWebSocketDataListener()); 120 | } 121 | notifyListeners(WebSocketEvent.SCOPE_CREATED, wsScope); 122 | // add to scopes 123 | addWebSocketScope(wsScope); 124 | } 125 | } 126 | 127 | private void notifyListeners(WebSocketEvent event, WebSocketScope wsScope) { 128 | for (IWebSocketScopeListener listener : scopeListeners) { 129 | switch (event) { 130 | case SCOPE_CREATED: 131 | listener.scopeCreated(wsScope); 132 | break; 133 | default: 134 | 135 | } 136 | } 137 | } 138 | 139 | /** 140 | * Removes the application scope. 141 | * 142 | * @param scope 143 | * the application scope 144 | */ 145 | public void removeApplication(IScope scope) { 146 | activeRooms.remove(scope.getName()); 147 | } 148 | 149 | /** 150 | * Adds a websocket scope. 151 | * 152 | * @param webSocketScope 153 | */ 154 | public void addWebSocketScope(WebSocketScope webSocketScope) { 155 | String path = webSocketScope.getPath(); 156 | if (scopes.putIfAbsent(path, webSocketScope) == null) { 157 | log.info("addWebSocketScope: {}", webSocketScope); 158 | notifyListeners(WebSocketEvent.SCOPE_ADDED, webSocketScope); 159 | } 160 | } 161 | 162 | /** 163 | * Removes a websocket scope. 164 | * 165 | * @param webSocketScope 166 | */ 167 | public void removeWebSocketScope(WebSocketScope webSocketScope) { 168 | log.info("removeWebSocketScope: {}", webSocketScope); 169 | WebSocketScope wsScope = scopes.remove(webSocketScope.getPath()); 170 | if (wsScope != null) { 171 | notifyListeners(WebSocketEvent.SCOPE_REMOVED, wsScope); 172 | } 173 | } 174 | 175 | /** 176 | * Add the connection on scope. 177 | * 178 | * @param conn 179 | * WebSocketConnection 180 | */ 181 | public void addConnection(WebSocketConnection conn) { 182 | WebSocketScope scope = getScope(conn); 183 | scope.addConnection(conn); 184 | } 185 | 186 | /** 187 | * Remove connection from scope. 188 | * 189 | * @param conn 190 | * WebSocketConnection 191 | */ 192 | public void removeConnection(WebSocketConnection conn) { 193 | if (conn != null) { 194 | WebSocketScope scope = getScope(conn); 195 | if (scope != null) { 196 | scope.removeConnection(conn); 197 | if (!scope.isValid()) { 198 | // scope is not valid. delete this. 199 | removeWebSocketScope(scope); 200 | } 201 | } 202 | } 203 | } 204 | 205 | /** 206 | * Add the listener on scope via its path. 207 | * 208 | * @param listener 209 | * IWebSocketDataListener 210 | * @param path 211 | */ 212 | public void addListener(IWebSocketDataListener listener, String path) { 213 | log.trace("addListener: {}", listener); 214 | WebSocketScope scope = getScope(path); 215 | if (scope != null) { 216 | scope.addListener(listener); 217 | } else { 218 | log.info("Scope not found for path: {}", path); 219 | } 220 | } 221 | 222 | /** 223 | * Remove listener from scope via its path. 224 | * 225 | * @param listener 226 | * IWebSocketDataListener 227 | * @param path 228 | */ 229 | public void removeListener(IWebSocketDataListener listener, String path) { 230 | log.trace("removeListener: {}", listener); 231 | WebSocketScope scope = getScope(path); 232 | if (scope != null) { 233 | scope.removeListener(listener); 234 | if (!scope.isValid()) { 235 | // scope is not valid. delete this 236 | removeWebSocketScope(scope); 237 | } 238 | } else { 239 | log.info("Scope not found for path: {}", path); 240 | } 241 | } 242 | 243 | /** 244 | * Create a web socket scope. Use the IWebSocketScopeListener interface to configure the created scope. 245 | * 246 | * @param path 247 | */ 248 | public void makeScope(String path) { 249 | log.debug("makeScope: {}", path); 250 | WebSocketScope wsScope = null; 251 | if (!scopes.containsKey(path)) { 252 | // new websocket scope 253 | wsScope = new WebSocketScope(); 254 | wsScope.setPath(path); 255 | notifyListeners(WebSocketEvent.SCOPE_CREATED, wsScope); 256 | addWebSocketScope(wsScope); 257 | log.debug("Use the IWebSocketScopeListener interface to be notified of new scopes"); 258 | } else { 259 | log.debug("Scope already exists: {}", path); 260 | } 261 | } 262 | 263 | /** 264 | * Create a web socket scope from a server IScope. Use the IWebSocketScopeListener interface to configure the created scope. 265 | * 266 | * @param scope 267 | */ 268 | public void makeScope(IScope scope) { 269 | log.debug("makeScope: {}", scope); 270 | String path = scope.getContextPath(); 271 | WebSocketScope wsScope = null; 272 | if (!scopes.containsKey(path)) { 273 | // add the name to the collection (no '/' prefix) 274 | activeRooms.add(scope.getName()); 275 | // new websocket scope for the server scope 276 | wsScope = new WebSocketScope(); 277 | wsScope.setPath(path); 278 | wsScope.setScope(scope); 279 | notifyListeners(WebSocketEvent.SCOPE_CREATED, wsScope); 280 | addWebSocketScope(wsScope); 281 | log.debug("Use the IWebSocketScopeListener interface to be notified of new scopes"); 282 | } else { 283 | log.debug("Scope already exists: {}", path); 284 | } 285 | } 286 | 287 | /** 288 | * Get the corresponding scope. 289 | * 290 | * @param path 291 | * scope path 292 | * @return scope 293 | */ 294 | public WebSocketScope getScope(String path) { 295 | log.debug("getScope: {}", path); 296 | WebSocketScope scope = scopes.get(path); 297 | // if we dont find a scope, go for default 298 | if (scope == null) { 299 | scope = scopes.get("default"); 300 | } 301 | log.debug("Returning: {}", scope); 302 | return scope; 303 | } 304 | 305 | /** 306 | * Get the corresponding scope, if none exists, make new one. 307 | * 308 | * @param conn 309 | * @return wsScope 310 | */ 311 | private WebSocketScope getScope(WebSocketConnection conn) { 312 | if (log.isTraceEnabled()) { 313 | log.trace("Scopes: {}", scopes); 314 | } 315 | log.debug("getScope: {}", conn); 316 | WebSocketScope wsScope; 317 | String path = conn.getPath(); 318 | if (!scopes.containsKey(path)) { 319 | // check for default scope 320 | if (!scopes.containsKey("default")) { 321 | wsScope = new WebSocketScope(); 322 | wsScope.setPath(path); 323 | notifyListeners(WebSocketEvent.SCOPE_CREATED, wsScope); 324 | addWebSocketScope(wsScope); 325 | //log.debug("Use the IWebSocketScopeListener interface to be notified of new scopes"); 326 | } else { 327 | path = "default"; 328 | } 329 | } 330 | wsScope = scopes.get(path); 331 | log.debug("Returning: {}", wsScope); 332 | return wsScope; 333 | } 334 | 335 | /** 336 | * Stops this manager and the scopes contained within. 337 | */ 338 | public void stop() { 339 | for (WebSocketScope scope : scopes.values()) { 340 | scope.unregister(); 341 | } 342 | } 343 | 344 | /** 345 | * Set the application scope for this manager. 346 | * 347 | * @param appScope 348 | */ 349 | public void setApplication(IScope appScope) { 350 | log.debug("Application scope: {}", appScope); 351 | this.appScope = appScope; 352 | // add the name to the collection (no '/' prefix) 353 | activeRooms.add(appScope.getName()); 354 | } 355 | 356 | public void setCopyListeners(boolean copy) { 357 | this.copyListeners = copy; 358 | } 359 | 360 | @Override 361 | public String toString() { 362 | return String.format("App scope: %s%nActive rooms: %s%nWS scopes: %s%nWS listeners: %s%n", appScope, activeRooms, scopes, scopeListeners); 363 | } 364 | 365 | } 366 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/WebSocketTransport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket; 20 | 21 | import java.io.IOException; 22 | import java.net.InetSocketAddress; 23 | import java.util.Arrays; 24 | import java.util.HashSet; 25 | import java.util.List; 26 | import java.util.Set; 27 | 28 | import org.apache.mina.core.filterchain.DefaultIoFilterChainBuilder; 29 | import org.apache.mina.core.service.IoHandlerAdapter; 30 | import org.apache.mina.core.service.IoService; 31 | import org.apache.mina.core.service.IoServiceListener; 32 | import org.apache.mina.core.session.IdleStatus; 33 | import org.apache.mina.core.session.IoSession; 34 | import org.apache.mina.filter.codec.ProtocolCodecFilter; 35 | import org.apache.mina.filter.logging.LoggingFilter; 36 | import org.apache.mina.filter.ssl.SslFilter; 37 | import org.apache.mina.transport.socket.SocketAcceptor; 38 | import org.apache.mina.transport.socket.SocketSessionConfig; 39 | import org.apache.mina.transport.socket.nio.NioSocketAcceptor; 40 | import org.red5.net.websocket.codec.WebSocketCodecFactory; 41 | import org.slf4j.Logger; 42 | import org.slf4j.LoggerFactory; 43 | import org.springframework.beans.factory.DisposableBean; 44 | import org.springframework.beans.factory.InitializingBean; 45 | 46 | /** 47 | * WebSocketTransport
48 | * this class will be instanced in red5.xml(or other xml files). * will make port listen... 49 | * 50 | * @author Toda Takahiko 51 | * @author Paul Gregoire 52 | */ 53 | public class WebSocketTransport implements InitializingBean, DisposableBean { 54 | 55 | private static final Logger log = LoggerFactory.getLogger(WebSocketTransport.class); 56 | 57 | private int sendBufferSize = 2048; 58 | 59 | private int receiveBufferSize = 2048; 60 | 61 | private int port = 80; 62 | 63 | private Set addresses = new HashSet<>(); 64 | 65 | private int writeTimeout = 30; 66 | 67 | // use -1 to disable idle timeout handling 68 | private int idleTimeout = 60; 69 | 70 | // whether to attempt using close messages or just force closing 71 | private static boolean niceClose; 72 | 73 | /** 74 | * Timeout to wait for the handshake response to be written. 75 | */ 76 | private static long handshakeWriteTimeout = 5000L; 77 | 78 | /** 79 | * Timeout to wait for handshake latch to be completed. Used to prevent sending to an socket that's not ready. 80 | */ 81 | private static long latchTimeout = handshakeWriteTimeout; 82 | 83 | private IoHandlerAdapter ioHandler; 84 | 85 | private SocketAcceptor acceptor; 86 | 87 | private SecureWebSocketConfiguration secureConfig; 88 | 89 | // Same origin policy enable/disabled 90 | private static boolean sameOriginPolicy; 91 | 92 | // Cross-origin policy enable/disabled 93 | private static boolean crossOriginPolicy; 94 | 95 | // Cross-origin names 96 | private static String[] allowedOrigins = new String[] { "*" }; 97 | 98 | /** 99 | * Creates the i/o handler and nio acceptor; ports and addresses are bound. 100 | * 101 | * @throws IOException 102 | */ 103 | @Override 104 | public void afterPropertiesSet() throws Exception { 105 | // create the nio acceptor 106 | acceptor = new NioSocketAcceptor(Runtime.getRuntime().availableProcessors() * 4); 107 | acceptor.addListener(new IoServiceListener() { 108 | 109 | @Override 110 | public void serviceActivated(IoService service) throws Exception { 111 | //log.debug("serviceActivated: {}", service); 112 | } 113 | 114 | @Override 115 | public void serviceIdle(IoService service, IdleStatus idleStatus) throws Exception { 116 | //logger.debug("serviceIdle: {} status: {}", service, idleStatus); 117 | } 118 | 119 | @Override 120 | public void serviceDeactivated(IoService service) throws Exception { 121 | //log.debug("serviceDeactivated: {}", service); 122 | } 123 | 124 | @Override 125 | public void sessionCreated(IoSession session) throws Exception { 126 | log.debug("sessionCreated: {}", session); 127 | //log.trace("Acceptor sessions: {}", acceptor.getManagedSessions()); 128 | } 129 | 130 | @Override 131 | public void sessionClosed(IoSession session) throws Exception { 132 | log.debug("sessionClosed: {}", session); 133 | } 134 | 135 | @Override 136 | public void sessionDestroyed(IoSession session) throws Exception { 137 | //log.debug("sessionDestroyed: {}", session); 138 | } 139 | 140 | }); 141 | // configure the acceptor 142 | SocketSessionConfig sessionConf = acceptor.getSessionConfig(); 143 | sessionConf.setReuseAddress(true); 144 | sessionConf.setTcpNoDelay(true); 145 | sessionConf.setSendBufferSize(sendBufferSize); 146 | sessionConf.setReadBufferSize(receiveBufferSize); 147 | // prevent the background blocking queue 148 | sessionConf.setUseReadOperation(false); 149 | // seconds 150 | sessionConf.setWriteTimeout(writeTimeout); 151 | // set an idle time in seconds 152 | if (idleTimeout > 0) { 153 | sessionConf.setIdleTime(IdleStatus.BOTH_IDLE, idleTimeout); 154 | } 155 | // close sessions when the acceptor is stopped 156 | acceptor.setCloseOnDeactivation(true); 157 | // requested maximum length of the queue of incoming connections 158 | acceptor.setBacklog(64); 159 | acceptor.setReuseAddress(true); 160 | // instance the websocket handler 161 | if (ioHandler == null) { 162 | ioHandler = new WebSocketHandler(); 163 | } 164 | log.trace("I/O handler: {}", ioHandler); 165 | acceptor.setHandler(ioHandler); 166 | DefaultIoFilterChainBuilder chain = acceptor.getFilterChain(); 167 | // if handling wss init the config 168 | SslFilter sslFilter = null; 169 | if (secureConfig != null) { 170 | try { 171 | sslFilter = secureConfig.getSslFilter(); 172 | chain.addFirst("sslFilter", sslFilter); 173 | } catch (Exception e) { 174 | log.warn("SSL configuration failed, websocket will not be secure", e); 175 | } 176 | } 177 | if (log.isTraceEnabled()) { 178 | chain.addLast("logger", new LoggingFilter()); 179 | } 180 | // add the websocket codec factory 181 | chain.addLast("protocol", new ProtocolCodecFilter(new WebSocketCodecFactory())); 182 | if (addresses.isEmpty()) { 183 | if (sslFilter != null) { 184 | log.info("WebSocket (wss) will be bound to port {}", port); 185 | } else { 186 | log.info("WebSocket (ws) will be bound to port {}", port); 187 | } 188 | acceptor.bind(new InetSocketAddress(port)); 189 | } else { 190 | if (sslFilter != null) { 191 | log.info("WebSocket (wss) will be bound to {}", addresses); 192 | } else { 193 | log.info("WebSocket (ws) will be bound to {}", addresses); 194 | } 195 | try { 196 | // loop through the addresses and bind 197 | Set socketAddresses = new HashSet(); 198 | for (String addr : addresses) { 199 | if (addr.indexOf(':') != -1) { 200 | String[] parts = addr.split(":"); 201 | socketAddresses.add(new InetSocketAddress(parts[0], Integer.valueOf(parts[1]))); 202 | } else { 203 | socketAddresses.add(new InetSocketAddress(addr, port)); 204 | } 205 | } 206 | log.debug("Binding to {}", socketAddresses.toString()); 207 | acceptor.bind(socketAddresses); 208 | } catch (Exception e) { 209 | log.warn("Exception occurred during resolve / bind", e); 210 | } 211 | } 212 | log.info("Started {} websocket transport. Timeouts - idle: {} write: {}", (isSecure() ? "secure" : ""), idleTimeout, writeTimeout); 213 | if (log.isDebugEnabled()) { 214 | log.debug("Acceptor sizes - send: {} recv: {}", acceptor.getSessionConfig().getSendBufferSize(), acceptor.getSessionConfig().getReadBufferSize()); 215 | } 216 | } 217 | 218 | /** 219 | * Ports and addresses are unbound (stop listening). 220 | */ 221 | @Override 222 | public void destroy() throws Exception { 223 | log.info("stopped {} websocket transport", (isSecure() ? "secure" : "")); 224 | acceptor.unbind(); 225 | } 226 | 227 | public void setAddresses(List addrs) { 228 | for (String addr : addrs) { 229 | addresses.add(addr); 230 | } 231 | } 232 | 233 | /** 234 | * @param port 235 | * the port to set 236 | */ 237 | public void setPort(int port) { 238 | this.port = port; 239 | } 240 | 241 | /** 242 | * @param sendBufferSize 243 | * the sendBufferSize to set 244 | */ 245 | public void setSendBufferSize(int sendBufferSize) { 246 | this.sendBufferSize = sendBufferSize; 247 | } 248 | 249 | /** 250 | * @param receiveBufferSize 251 | * the receiveBufferSize to set 252 | */ 253 | public void setReceiveBufferSize(int receiveBufferSize) { 254 | this.receiveBufferSize = receiveBufferSize; 255 | } 256 | 257 | /** 258 | * Write timeout. 259 | * 260 | * @param writeTimeout 261 | */ 262 | public void setWriteTimeout(int writeTimeout) { 263 | this.writeTimeout = writeTimeout; 264 | } 265 | 266 | /** 267 | * Idle timeout. 268 | * 269 | * @param idleTimeout 270 | */ 271 | public void setIdleTimeout(int idleTimeout) { 272 | this.idleTimeout = idleTimeout; 273 | } 274 | 275 | public static long getHandshakeWriteTimeout() { 276 | return handshakeWriteTimeout; 277 | } 278 | 279 | public static void setHandshakeWriteTimeout(long handshakeWriteTimeout) { 280 | WebSocketTransport.handshakeWriteTimeout = handshakeWriteTimeout; 281 | } 282 | 283 | public static long getLatchTimeout() { 284 | return latchTimeout; 285 | } 286 | 287 | public static void setLatchTimeout(long latchTimeout) { 288 | WebSocketTransport.latchTimeout = latchTimeout; 289 | } 290 | 291 | /** 292 | * @param connectionThreads 293 | * the connectionThreads to set 294 | */ 295 | @Deprecated 296 | public void setConnectionThreads(int connectionThreads) { 297 | } 298 | 299 | /** 300 | * @param ioThreads 301 | * the ioThreads to set 302 | */ 303 | @Deprecated 304 | public void setIoThreads(int ioThreads) { 305 | } 306 | 307 | public boolean isSecure() { 308 | return secureConfig != null; 309 | } 310 | 311 | public void setIoHandler(IoHandlerAdapter ioHandler) { 312 | this.ioHandler = ioHandler; 313 | } 314 | 315 | public void setSecureConfig(SecureWebSocketConfiguration secureConfig) { 316 | this.secureConfig = secureConfig; 317 | } 318 | 319 | public static boolean isNiceClose() { 320 | return niceClose; 321 | } 322 | 323 | public static void setNiceClose(boolean niceClose) { 324 | WebSocketTransport.niceClose = niceClose; 325 | } 326 | 327 | public static boolean isSameOriginPolicy() { 328 | return sameOriginPolicy; 329 | } 330 | 331 | public void setSameOriginPolicy(boolean sameOriginPolicy) { 332 | WebSocketTransport.sameOriginPolicy = sameOriginPolicy; 333 | } 334 | 335 | public static boolean isCrossOriginPolicy() { 336 | return crossOriginPolicy; 337 | } 338 | 339 | public void setCrossOriginPolicy(boolean crossOriginPolicy) { 340 | WebSocketTransport.crossOriginPolicy = crossOriginPolicy; 341 | } 342 | 343 | public static String[] getAllowedOrigins() { 344 | return allowedOrigins; 345 | } 346 | 347 | public void setAllowedOrigins(String[] allowedOrigins) { 348 | WebSocketTransport.allowedOrigins = allowedOrigins; 349 | log.info("allowedOrigins: {}", Arrays.toString(WebSocketTransport.allowedOrigins)); 350 | } 351 | 352 | } 353 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/codec/WebSocketCodecFactory.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.codec; 20 | 21 | import org.apache.mina.core.session.IoSession; 22 | import org.apache.mina.filter.codec.ProtocolCodecFactory; 23 | import org.apache.mina.filter.codec.ProtocolDecoder; 24 | import org.apache.mina.filter.codec.ProtocolEncoder; 25 | 26 | /** 27 | * Codec Factory used for creating websocket filter. 28 | */ 29 | public class WebSocketCodecFactory implements ProtocolCodecFactory { 30 | 31 | private final ProtocolEncoder encoder; 32 | 33 | private final ProtocolDecoder decoder; 34 | 35 | public WebSocketCodecFactory() { 36 | encoder = new WebSocketEncoder(); 37 | decoder = new WebSocketDecoder(); 38 | } 39 | 40 | @Override 41 | public ProtocolDecoder getDecoder(IoSession session) throws Exception { 42 | return decoder; 43 | } 44 | 45 | @Override 46 | public ProtocolEncoder getEncoder(IoSession session) throws Exception { 47 | return encoder; 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/codec/WebSocketDecoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.codec; 20 | 21 | import java.security.MessageDigest; 22 | import java.security.NoSuchAlgorithmException; 23 | import java.util.Arrays; 24 | import java.util.HashMap; 25 | import java.util.Map; 26 | import java.util.Set; 27 | 28 | import org.apache.mina.core.buffer.IoBuffer; 29 | import org.apache.mina.core.session.IoSession; 30 | import org.apache.mina.filter.codec.CumulativeProtocolDecoder; 31 | import org.apache.mina.filter.codec.ProtocolDecoderOutput; 32 | import org.bouncycastle.util.encoders.Base64; 33 | import org.red5.net.websocket.Constants; 34 | import org.red5.net.websocket.WebSocketConnection; 35 | import org.red5.net.websocket.WebSocketException; 36 | import org.red5.net.websocket.WebSocketPlugin; 37 | import org.red5.net.websocket.WebSocketScopeManager; 38 | import org.red5.net.websocket.WebSocketTransport; 39 | import org.red5.net.websocket.listener.IWebSocketDataListener; 40 | import org.red5.net.websocket.model.ConnectionType; 41 | import org.red5.net.websocket.model.HandshakeResponse; 42 | import org.red5.net.websocket.model.MessageType; 43 | import org.red5.net.websocket.model.WSMessage; 44 | import org.red5.server.plugin.PluginRegistry; 45 | import org.slf4j.Logger; 46 | import org.slf4j.LoggerFactory; 47 | 48 | /** 49 | * This class handles the websocket decoding and its handshake process. A warning is loggged if WebSocket version 13 is not detected.
50 | * Decodes incoming buffers in a manner that makes the sender transparent to the decoders further up in the filter chain. If the sender is a native client then the buffer is simply passed through. If the sender is a websocket, it will extract the content out from the dataframe and parse it before passing it along the filter chain. 51 | * 52 | * @see Mozilla - Writing WebSocket Servers 53 | * 54 | * @author Dhruv Chopra 55 | * @author Paul Gregoire 56 | */ 57 | public class WebSocketDecoder extends CumulativeProtocolDecoder { 58 | 59 | private static final Logger log = LoggerFactory.getLogger(WebSocketDecoder.class); 60 | 61 | private static final String DECODER_STATE_KEY = "decoder-state"; 62 | 63 | private static final String DECODED_MESSAGE_KEY = "decoded-message"; 64 | 65 | private static final String DECODED_MESSAGE_TYPE_KEY = "decoded-message-type"; 66 | 67 | private static final String DECODED_MESSAGE_FRAGMENTS_KEY = "decoded-message-fragments"; 68 | 69 | /** 70 | * Keeps track of the decoding state of a frame. Byte values start at -128 as a flag to indicate they are not set. 71 | */ 72 | private final class DecoderState { 73 | // keep track of fin == 0 to indicate a fragment 74 | byte fin = Byte.MIN_VALUE; 75 | 76 | byte opCode = Byte.MIN_VALUE; 77 | 78 | byte mask = Byte.MIN_VALUE; 79 | 80 | int frameLen = 0; 81 | 82 | // payload 83 | byte[] payload; 84 | 85 | @Override 86 | public String toString() { 87 | return "DecoderState [fin=" + fin + ", opCode=" + opCode + ", mask=" + mask + ", frameLen=" + frameLen + "]"; 88 | } 89 | } 90 | 91 | @Override 92 | protected boolean doDecode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws Exception { 93 | IoBuffer resultBuffer; 94 | WebSocketConnection conn = (WebSocketConnection) session.getAttribute(Constants.CONNECTION); 95 | if (conn == null) { 96 | log.debug("Decode start pos: {}", in.position()); 97 | // first message on a new connection, check if its from a websocket or a native socket 98 | if (doHandShake(session, in)) { 99 | log.debug("Decode end pos: {} limit: {}", in.position(), in.limit()); 100 | // websocket handshake was successful. Don't write anything to output as we want to abstract the handshake request message from the handler 101 | if (in.position() != in.limit()) { 102 | in.position(in.limit()); 103 | } 104 | return true; 105 | } else if (session.containsAttribute(Constants.WS_HANDSHAKE)) { 106 | // more still expected to come in before HS is completed 107 | return false; 108 | } else { 109 | // message is from a native socket. Simply wrap and pass through 110 | resultBuffer = IoBuffer.wrap(in.array(), 0, in.limit()); 111 | in.position(in.limit()); 112 | out.write(resultBuffer); 113 | } 114 | } else if (conn.isWebConnection()) { 115 | // grab decoding state 116 | DecoderState decoderState = (DecoderState) session.getAttribute(DECODER_STATE_KEY); 117 | if (decoderState == null) { 118 | decoderState = new DecoderState(); 119 | session.setAttribute(DECODER_STATE_KEY, decoderState); 120 | } 121 | // there is incoming data from the websocket, decode it 122 | decodeIncommingData(in, session); 123 | // this will be null until all the fragments are collected 124 | WSMessage message = (WSMessage) session.getAttribute(DECODED_MESSAGE_KEY); 125 | if (log.isDebugEnabled()) { 126 | log.debug("State: {} message: {}", decoderState, message); 127 | } 128 | if (message != null) { 129 | // set the originating connection on the message 130 | message.setConnection(conn); 131 | // write the message 132 | out.write(message); 133 | // remove decoded message 134 | session.removeAttribute(DECODED_MESSAGE_KEY); 135 | } else { 136 | // there was not enough data in the buffer to parse 137 | return false; 138 | } 139 | } else { 140 | // session is known to be from a native socket. So simply wrap and pass through 141 | byte[] arr = new byte[in.remaining()]; 142 | in.get(arr); 143 | out.write(IoBuffer.wrap(arr)); 144 | } 145 | return true; 146 | } 147 | 148 | /** 149 | * Try parsing the message as a websocket handshake request. If it is such a request, then send the corresponding handshake response (as in Section 4.2.2 RFC 6455). 150 | */ 151 | @SuppressWarnings("unchecked") 152 | private boolean doHandShake(IoSession session, IoBuffer in) { 153 | if (log.isDebugEnabled()) { 154 | log.debug("Handshake: {}", in); 155 | } 156 | // incoming data 157 | byte[] data = null; 158 | // check for existing HS data 159 | if (session.containsAttribute(Constants.WS_HANDSHAKE)) { 160 | byte[] tmp = (byte[]) session.getAttribute(Constants.WS_HANDSHAKE); 161 | // size to hold existing and incoming 162 | data = new byte[tmp.length + in.remaining()]; 163 | System.arraycopy(tmp, 0, data, 0, tmp.length); 164 | // get incoming bytes 165 | in.get(data, tmp.length, in.remaining()); 166 | } else { 167 | // size for incoming bytes 168 | data = new byte[in.remaining()]; 169 | // get incoming bytes 170 | in.get(data); 171 | } 172 | // ensure the incoming data is complete (ends with crlfcrlf) 173 | byte[] tail = Arrays.copyOfRange(data, data.length - 4, data.length); 174 | if (!Arrays.equals(tail, Constants.END_OF_REQ)) { 175 | // accumulate the HS data 176 | session.setAttribute(Constants.WS_HANDSHAKE, data); 177 | return false; 178 | } 179 | // create the connection obj 180 | WebSocketConnection conn = new WebSocketConnection(session); 181 | // mark as secure if using ssl 182 | if (session.getFilterChain().contains("sslFilter")) { 183 | conn.setSecure(true); 184 | } 185 | try { 186 | Map headers = parseClientRequest(conn, new String(data)); 187 | if (log.isTraceEnabled()) { 188 | log.trace("Header map: {}", headers); 189 | } 190 | if (!headers.isEmpty() && headers.containsKey(Constants.WS_HEADER_KEY)) { 191 | // add the headers to the connection, they may be of use to implementers 192 | conn.setHeaders(headers); 193 | // add query string parameters 194 | if (headers.containsKey(Constants.URI_QS_PARAMETERS)) { 195 | conn.setQuerystringParameters((Map) headers.remove(Constants.URI_QS_PARAMETERS)); 196 | } 197 | // check the version 198 | if (!"13".equals(headers.get(Constants.WS_HEADER_VERSION))) { 199 | log.info("Version 13 was not found in the request, communications may fail"); 200 | } 201 | // get the path 202 | String path = conn.getPath(); 203 | // get the scope manager 204 | WebSocketScopeManager manager = (WebSocketScopeManager) session.getAttribute(Constants.MANAGER); 205 | if (manager == null) { 206 | WebSocketPlugin plugin = (WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin"); 207 | manager = plugin.getManager(path); 208 | } 209 | // store manager in the current session 210 | session.setAttribute(Constants.MANAGER, manager); 211 | // TODO add handling for extensions 212 | 213 | // TODO expand handling for protocols requested by the client, instead of just echoing back 214 | if (headers.containsKey(Constants.WS_HEADER_PROTOCOL)) { 215 | boolean protocolSupported = false; 216 | String protocol = (String) headers.get(Constants.WS_HEADER_PROTOCOL); 217 | log.debug("Protocol '{}' found in the request", protocol); 218 | // add protocol to the connection 219 | conn.setProtocol(protocol); 220 | // TODO check listeners for "protocol" support 221 | Set listeners = manager.getScope(path).getListeners(); 222 | for (IWebSocketDataListener listener : listeners) { 223 | if (listener.getProtocol().equals(protocol)) { 224 | //log.debug("Scope has listener support for the {} protocol", protocol); 225 | protocolSupported = true; 226 | break; 227 | } 228 | } 229 | log.debug("Scope listener does{} support the '{}' protocol", (protocolSupported ? "" : "n't"), protocol); 230 | } 231 | // add connection to the manager 232 | manager.addConnection(conn); 233 | // prepare response and write it to the directly to the session 234 | HandshakeResponse wsResponse = buildHandshakeResponse(conn, (String) headers.get(Constants.WS_HEADER_KEY)); 235 | // pass the handshake response to the ws connection so it can be sent outside the io thread and allow the decode to complete 236 | conn.sendHandshakeResponse(wsResponse); 237 | // remove the chunk attr 238 | session.removeAttribute(Constants.WS_HANDSHAKE); 239 | return true; 240 | } 241 | // set connection as native / direct 242 | conn.setType(ConnectionType.DIRECT); 243 | } catch (Exception e) { 244 | // input is not a websocket handshake request 245 | log.warn("Handshake failed", e); 246 | } 247 | return false; 248 | } 249 | 250 | /** 251 | * Parse the client request and return a map containing the header contents. If the requested application is not enabled, return a 400 error. 252 | * 253 | * @param conn 254 | * @param requestData 255 | * @return map of headers 256 | * @throws WebSocketException 257 | */ 258 | private Map parseClientRequest(WebSocketConnection conn, String requestData) throws WebSocketException { 259 | String[] request = requestData.split("\r\n"); 260 | if (log.isDebugEnabled()) { 261 | log.debug("Request: {}", Arrays.toString(request)); 262 | } 263 | // host and origin for validation purposes 264 | String host = null, origin = null; 265 | Map map = new HashMap<>(); 266 | for (int i = 0; i < request.length; i++) { 267 | log.trace("Request {}: {}", i, request[i]); 268 | if (request[i].startsWith("GET ") || request[i].startsWith("POST ") || request[i].startsWith("PUT ")) { 269 | // "GET /chat/room1?id=publisher1 HTTP/1.1" 270 | // split it on space 271 | String requestPath = request[i].split("\\s+")[1]; 272 | // get the path data for handShake 273 | int start = requestPath.indexOf('/'); 274 | int end = requestPath.length(); 275 | int ques = requestPath.indexOf('?'); 276 | if (ques > 0) { 277 | end = ques; 278 | } 279 | log.trace("Request path: {} to {} ques: {}", start, end, ques); 280 | String path = requestPath.substring(start, end).trim(); 281 | log.trace("Client request path: {}", path); 282 | conn.setPath(path); 283 | // check for '?' or included query string 284 | if (ques > 0) { 285 | // parse any included query string 286 | String qs = requestPath.substring(ques).trim(); 287 | log.trace("Request querystring: {}", qs); 288 | map.put(Constants.URI_QS_PARAMETERS, parseQuerystring(qs)); 289 | } 290 | // get the manager 291 | WebSocketPlugin plugin = (WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin"); 292 | if (plugin != null) { 293 | log.trace("Found plugin"); 294 | WebSocketScopeManager manager = plugin.getManager(path); 295 | log.trace("Manager was found? : {}", manager); 296 | // only check that the application is enabled, not the room or sub levels 297 | if (manager != null && manager.isEnabled(path)) { 298 | log.trace("Path enabled: {}", path); 299 | } else { 300 | // invalid scope or its application is not enabled, send disconnect message 301 | conn.close(1002, build400Response(conn)); 302 | throw new WebSocketException("Handshake failed, path not enabled"); 303 | } 304 | } else { 305 | log.warn("Plugin lookup failed"); 306 | conn.close(1002, build400Response(conn)); 307 | throw new WebSocketException("Handshake failed, missing plugin"); 308 | } 309 | } else if (request[i].contains(Constants.WS_HEADER_KEY)) { 310 | map.put(Constants.WS_HEADER_KEY, extractHeaderValue(request[i])); 311 | } else if (request[i].contains(Constants.WS_HEADER_VERSION)) { 312 | map.put(Constants.WS_HEADER_VERSION, extractHeaderValue(request[i])); 313 | } else if (request[i].contains(Constants.WS_HEADER_EXTENSIONS)) { 314 | map.put(Constants.WS_HEADER_EXTENSIONS, extractHeaderValue(request[i])); 315 | } else if (request[i].contains(Constants.WS_HEADER_PROTOCOL)) { 316 | map.put(Constants.WS_HEADER_PROTOCOL, extractHeaderValue(request[i])); 317 | } else if (request[i].contains(Constants.HTTP_HEADER_HOST)) { 318 | // get the host data 319 | host = extractHeaderValue(request[i]); 320 | conn.setHost(host); 321 | } else if (request[i].contains(Constants.HTTP_HEADER_ORIGIN)) { 322 | // get the origin data 323 | origin = extractHeaderValue(request[i]); 324 | conn.setOrigin(origin); 325 | } else if (request[i].contains(Constants.HTTP_HEADER_USERAGENT)) { 326 | map.put(Constants.HTTP_HEADER_USERAGENT, extractHeaderValue(request[i])); 327 | } else if (request[i].startsWith(Constants.WS_HEADER_GENERIC_PREFIX)) { 328 | map.put(getHeaderName(request[i]), extractHeaderValue(request[i])); 329 | } 330 | } 331 | // policy checking 332 | boolean validOrigin = true; 333 | if (conn.isSameOriginPolicy()) { 334 | // if SOP / origin validation is enabled, verify the origin 335 | String trimmedHost = host; 336 | // strip protocol if its there 337 | if (host.startsWith("http")) { 338 | trimmedHost = host.substring(host.indexOf("//") + 1); 339 | } 340 | // chop off port etc.. 341 | int colonIndex = trimmedHost.indexOf(':'); 342 | if (colonIndex > 0) { 343 | trimmedHost = trimmedHost.substring(0, colonIndex); 344 | } 345 | log.debug("Trimmed host: {}", trimmedHost); 346 | validOrigin = origin.contains(trimmedHost); 347 | log.debug("Same Origin? {}", validOrigin); 348 | } 349 | if (conn.isCrossOriginPolicy()) { 350 | // if CORS is enabled 351 | validOrigin = conn.isValidOrigin(origin); 352 | log.debug("Origin {} valid? {}", origin, validOrigin); 353 | } 354 | if (!validOrigin) { 355 | conn.close(1008, build403Response(conn)); 356 | throw new WebSocketException(String.format("Policy failure - SOP enabled: %b CORS enabled: %b", WebSocketTransport.isSameOriginPolicy(), WebSocketTransport.isCrossOriginPolicy())); 357 | } else { 358 | log.debug("Origin is valid"); 359 | } 360 | return map; 361 | } 362 | 363 | /** 364 | * Returns the trimmed header name. 365 | * 366 | * @param requestHeader 367 | * @return value 368 | */ 369 | private String getHeaderName(String requestHeader) { 370 | return requestHeader.substring(0, requestHeader.indexOf(':')).trim(); 371 | } 372 | 373 | /** 374 | * Returns the trimmed header value. 375 | * 376 | * @param requestHeader 377 | * @return value 378 | */ 379 | private String extractHeaderValue(String requestHeader) { 380 | return requestHeader.substring(requestHeader.indexOf(':') + 1).trim(); 381 | } 382 | 383 | /** 384 | * Build a handshake response based on the given client key. 385 | * 386 | * @param clientKey 387 | * @return response 388 | * @throws WebSocketException 389 | */ 390 | private HandshakeResponse buildHandshakeResponse(WebSocketConnection conn, String clientKey) throws WebSocketException { 391 | if (log.isDebugEnabled()) { 392 | log.debug("buildHandshakeResponse: {} client key: {}", conn, clientKey); 393 | } 394 | byte[] accept; 395 | try { 396 | // performs the accept creation routine from RFC6455 @see RFC6455 397 | // concatenate the key and magic string, then SHA1 hash and base64 encode 398 | MessageDigest md = MessageDigest.getInstance("SHA1"); 399 | accept = Base64.encode(md.digest((clientKey + Constants.WEBSOCKET_MAGIC_STRING).getBytes())); 400 | } catch (NoSuchAlgorithmException e) { 401 | throw new WebSocketException("Algorithm is missing"); 402 | } 403 | // make up reply data... 404 | IoBuffer buf = IoBuffer.allocate(308); 405 | buf.setAutoExpand(true); 406 | buf.put("HTTP/1.1 101 Switching Protocols".getBytes()); 407 | buf.put(Constants.CRLF); 408 | buf.put("Upgrade: websocket".getBytes()); 409 | buf.put(Constants.CRLF); 410 | buf.put("Connection: Upgrade".getBytes()); 411 | buf.put(Constants.CRLF); 412 | buf.put("Server: Red5".getBytes()); 413 | buf.put(Constants.CRLF); 414 | buf.put("Sec-WebSocket-Version-Server: 13".getBytes()); 415 | buf.put(Constants.CRLF); 416 | buf.put(String.format("Sec-WebSocket-Origin: %s", conn.getOrigin()).getBytes()); 417 | buf.put(Constants.CRLF); 418 | buf.put(String.format("Sec-WebSocket-Location: %s", conn.getHost()).getBytes()); 419 | buf.put(Constants.CRLF); 420 | // send back extensions if enabled 421 | if (conn.hasExtensions()) { 422 | buf.put(String.format("Sec-WebSocket-Extensions: %s", conn.getExtensionsAsString()).getBytes()); 423 | buf.put(Constants.CRLF); 424 | } 425 | // send back protocol if enabled 426 | if (conn.hasProtocol()) { 427 | buf.put(String.format("Sec-WebSocket-Protocol: %s", conn.getProtocol()).getBytes()); 428 | buf.put(Constants.CRLF); 429 | } 430 | buf.put(String.format("Sec-WebSocket-Accept: %s", new String(accept)).getBytes()); 431 | buf.put(Constants.CRLF); 432 | buf.put(Constants.CRLF); 433 | // if any bytes follow this crlf, the follow-up data will be corrupted 434 | if (log.isTraceEnabled()) { 435 | log.trace("Handshake response size: {}", buf.limit()); 436 | } 437 | return new HandshakeResponse(buf); 438 | } 439 | 440 | /** 441 | * Build an HTTP 400 "Bad Request" response. 442 | * 443 | * @return response 444 | * @throws WebSocketException 445 | */ 446 | private HandshakeResponse build400Response(WebSocketConnection conn) throws WebSocketException { 447 | if (log.isDebugEnabled()) { 448 | log.debug("build400Response: {}", conn); 449 | } 450 | // make up reply data... 451 | IoBuffer buf = IoBuffer.allocate(32); 452 | buf.setAutoExpand(true); 453 | buf.put("HTTP/1.1 400 Bad Request".getBytes()); 454 | buf.put(Constants.CRLF); 455 | buf.put("Sec-WebSocket-Version-Server: 13".getBytes()); 456 | buf.put(Constants.CRLF); 457 | buf.put(Constants.CRLF); 458 | if (log.isTraceEnabled()) { 459 | log.trace("Handshake error response size: {}", buf.limit()); 460 | } 461 | return new HandshakeResponse(buf); 462 | } 463 | 464 | /** 465 | * Build an HTTP 403 "Forbidden" response. 466 | * 467 | * @return response 468 | * @throws WebSocketException 469 | */ 470 | private HandshakeResponse build403Response(WebSocketConnection conn) throws WebSocketException { 471 | if (log.isDebugEnabled()) { 472 | log.debug("build403Response: {}", conn); 473 | } 474 | // make up reply data... 475 | IoBuffer buf = IoBuffer.allocate(32); 476 | buf.setAutoExpand(true); 477 | buf.put("HTTP/1.1 403 Forbidden".getBytes()); 478 | buf.put(Constants.CRLF); 479 | buf.put("Sec-WebSocket-Version-Server: 13".getBytes()); 480 | buf.put(Constants.CRLF); 481 | buf.put(Constants.CRLF); 482 | if (log.isTraceEnabled()) { 483 | log.trace("Handshake error response size: {}", buf.limit()); 484 | } 485 | return new HandshakeResponse(buf); 486 | } 487 | 488 | /** 489 | * Decode the in buffer according to the Section 5.2. RFC 6455. If there are multiple websocket dataframes in the buffer, this will parse all and return one complete decoded buffer. 490 | * 491 | *
492 |      * 	  0                   1                   2                   3
493 |      * 	  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
494 |      * 	 +-+-+-+-+-------+-+-------------+-------------------------------+
495 |      * 	 |F|R|R|R| opcode|M| Payload len |    Extended payload length    |
496 |      * 	 |I|S|S|S|  (4)  |A|     (7)     |             (16/64)           |
497 |      * 	 |N|V|V|V|       |S|             |   (if payload len==126/127)   |
498 |      * 	 | |1|2|3|       |K|             |                               |
499 |      * 	 +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - +
500 |      * 	 |     Extended payload length continued, if payload len == 127  |
501 |      * 	 + - - - - - - - - - - - - - - - +-------------------------------+
502 |      * 	 |                               |Masking-key, if MASK set to 1  |
503 |      * 	 +-------------------------------+-------------------------------+
504 |      * 	 | Masking-key (continued)       |          Payload Data         |
505 |      * 	 +-------------------------------- - - - - - - - - - - - - - - - +
506 |      * 	 :                     Payload Data continued ...                :
507 |      * 	 + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
508 |      * 	 |                     Payload Data continued ...                |
509 |      * 	 +---------------------------------------------------------------+
510 |      * 
511 | * 512 | * @param in 513 | * @param session 514 | */ 515 | public static void decodeIncommingData(IoBuffer in, IoSession session) { 516 | log.trace("Decoding: {}", in); 517 | // get decoder state 518 | DecoderState decoderState = (DecoderState) session.getAttribute(DECODER_STATE_KEY); 519 | if (decoderState.fin == Byte.MIN_VALUE) { 520 | byte frameInfo = in.get(); 521 | // get FIN (1 bit) 522 | //log.debug("frameInfo: {}", Integer.toBinaryString((frameInfo & 0xFF) + 256)); 523 | decoderState.fin = (byte) ((frameInfo >>> 7) & 1); 524 | log.trace("FIN: {}", decoderState.fin); 525 | // the next 3 bits are for RSV1-3 (not used here at the moment) 526 | // get the opcode (4 bits) 527 | decoderState.opCode = (byte) (frameInfo & 0x0f); 528 | log.trace("Opcode: {}", decoderState.opCode); 529 | // opcodes 3-7 and b-f are reserved for non-control frames 530 | } 531 | if (decoderState.mask == Byte.MIN_VALUE) { 532 | byte frameInfo2 = in.get(); 533 | // get mask bit (1 bit) 534 | decoderState.mask = (byte) ((frameInfo2 >>> 7) & 1); 535 | log.trace("Mask: {}", decoderState.mask); 536 | // get payload length (7, 7+16, 7+64 bits) 537 | decoderState.frameLen = (frameInfo2 & (byte) 0x7F); 538 | log.trace("Payload length: {}", decoderState.frameLen); 539 | if (decoderState.frameLen == 126) { 540 | decoderState.frameLen = in.getUnsignedShort(); 541 | log.trace("Payload length updated: {}", decoderState.frameLen); 542 | } else if (decoderState.frameLen == 127) { 543 | long extendedLen = in.getLong(); 544 | if (extendedLen >= Integer.MAX_VALUE) { 545 | log.error("Data frame is too large for this implementation. Length: {}", extendedLen); 546 | } else { 547 | decoderState.frameLen = (int) extendedLen; 548 | } 549 | log.trace("Payload length updated: {}", decoderState.frameLen); 550 | } 551 | } 552 | // ensure enough bytes left to fill payload, if masked add 4 additional bytes 553 | if (decoderState.frameLen + (decoderState.mask == 1 ? 4 : 0) > in.remaining()) { 554 | log.info("Not enough data available to decode, socket may be closed/closing"); 555 | } else { 556 | // if the data is masked (xor'd) 557 | if (decoderState.mask == 1) { 558 | // get the mask key 559 | byte maskKey[] = new byte[4]; 560 | for (int i = 0; i < 4; i++) { 561 | maskKey[i] = in.get(); 562 | } 563 | /* now un-mask frameLen bytes as per Section 5.3 RFC 6455 564 | Octet i of the transformed data ("transformed-octet-i") is the XOR of 565 | octet i of the original data ("original-octet-i") with octet at index 566 | i modulo 4 of the masking key ("masking-key-octet-j"): 567 | j = i MOD 4 568 | transformed-octet-i = original-octet-i XOR masking-key-octet-j 569 | */ 570 | decoderState.payload = new byte[decoderState.frameLen]; 571 | for (int i = 0; i < decoderState.frameLen; i++) { 572 | byte maskedByte = in.get(); 573 | decoderState.payload[i] = (byte) (maskedByte ^ maskKey[i % 4]); 574 | } 575 | } else { 576 | decoderState.payload = new byte[decoderState.frameLen]; 577 | in.get(decoderState.payload); 578 | } 579 | // if FIN == 0 we have fragments 580 | if (decoderState.fin == 0) { 581 | // store the fragment and continue 582 | IoBuffer fragments = (IoBuffer) session.getAttribute(DECODED_MESSAGE_FRAGMENTS_KEY); 583 | if (fragments == null) { 584 | fragments = IoBuffer.allocate(decoderState.frameLen); 585 | fragments.setAutoExpand(true); 586 | session.setAttribute(DECODED_MESSAGE_FRAGMENTS_KEY, fragments); 587 | // store message type since following type may be a continuation 588 | MessageType messageType = MessageType.CLOSE; 589 | switch (decoderState.opCode) { 590 | case 0: // continuation 591 | messageType = MessageType.CONTINUATION; 592 | break; 593 | case 1: // text 594 | messageType = MessageType.TEXT; 595 | break; 596 | case 2: // binary 597 | messageType = MessageType.BINARY; 598 | break; 599 | case 9: // ping 600 | messageType = MessageType.PING; 601 | break; 602 | case 0xa: // pong 603 | messageType = MessageType.PONG; 604 | break; 605 | } 606 | session.setAttribute(DECODED_MESSAGE_TYPE_KEY, messageType); 607 | } 608 | fragments.put(decoderState.payload); 609 | // remove decoder state 610 | session.removeAttribute(DECODER_STATE_KEY); 611 | } else { 612 | // create a message 613 | WSMessage message = new WSMessage(); 614 | // check for previously set type from the first fragment (if we have fragments) 615 | MessageType messageType = (MessageType) session.getAttribute(DECODED_MESSAGE_TYPE_KEY); 616 | if (messageType == null) { 617 | switch (decoderState.opCode) { 618 | case 0: // continuation 619 | messageType = MessageType.CONTINUATION; 620 | break; 621 | case 1: // text 622 | messageType = MessageType.TEXT; 623 | break; 624 | case 2: // binary 625 | messageType = MessageType.BINARY; 626 | break; 627 | case 9: // ping 628 | messageType = MessageType.PING; 629 | break; 630 | case 0xa: // pong 631 | messageType = MessageType.PONG; 632 | break; 633 | case 8: // close 634 | messageType = MessageType.CLOSE; 635 | // handler or listener should close upon receipt 636 | break; 637 | default: 638 | // TODO throw ex? 639 | log.info("Unhandled opcode: {}", decoderState.opCode); 640 | } 641 | } 642 | // set message type 643 | message.setMessageType(messageType); 644 | // check for fragments and piece them together, otherwise just send the single completed frame 645 | IoBuffer fragments = (IoBuffer) session.removeAttribute(DECODED_MESSAGE_FRAGMENTS_KEY); 646 | if (fragments != null) { 647 | fragments.put(decoderState.payload); 648 | fragments.flip(); 649 | message.setPayload(fragments); 650 | } else { 651 | // add the payload 652 | message.addPayload(decoderState.payload); 653 | } 654 | // set the message on the session 655 | session.setAttribute(DECODED_MESSAGE_KEY, message); 656 | // remove decoder state 657 | session.removeAttribute(DECODER_STATE_KEY); 658 | // remove type 659 | session.removeAttribute(DECODED_MESSAGE_TYPE_KEY); 660 | } 661 | } 662 | } 663 | 664 | /** 665 | * Returns a map of key / value pairs from a given querystring. 666 | * 667 | * @param query 668 | * @return k/v map 669 | */ 670 | public static Map parseQuerystring(String query) { 671 | String[] params = query.split("&"); 672 | Map map = new HashMap(); 673 | for (String param : params) { 674 | String[] nameValue = param.split("="); 675 | map.put(nameValue[0], nameValue[1]); 676 | } 677 | return map; 678 | } 679 | 680 | } 681 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/codec/WebSocketEncoder.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.codec; 20 | 21 | import org.apache.mina.core.buffer.IoBuffer; 22 | import org.apache.mina.core.session.IoSession; 23 | import org.apache.mina.filter.codec.ProtocolEncoderAdapter; 24 | import org.apache.mina.filter.codec.ProtocolEncoderOutput; 25 | import org.red5.net.websocket.Constants; 26 | import org.red5.net.websocket.WebSocketConnection; 27 | import org.red5.net.websocket.model.HandshakeRequest; 28 | import org.red5.net.websocket.model.HandshakeResponse; 29 | import org.red5.net.websocket.model.Packet; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | /** 34 | * Encodes incoming buffers in a manner that makes the receiving client type transparent to the encoders further up in the filter chain. If the receiving client is a native client then the buffer contents are simply passed through. If the receiving client is a websocket, it will encode the buffer contents in to WebSocket DataFrame before passing it along the filter chain. 35 | * 36 | * Note: you must wrap the IoBuffer you want to send around a WebSocketCodecPacket instance. 37 | * 38 | * @author Dhruv Chopra 39 | * @author Paul Gregoire 40 | */ 41 | public class WebSocketEncoder extends ProtocolEncoderAdapter { 42 | 43 | private static final Logger log = LoggerFactory.getLogger(WebSocketEncoder.class); 44 | 45 | @Override 46 | public void encode(IoSession session, Object message, ProtocolEncoderOutput out) throws Exception { 47 | WebSocketConnection conn = (WebSocketConnection) session.getAttribute(Constants.CONNECTION); 48 | IoBuffer resultBuffer; 49 | if (message instanceof Packet) { 50 | Packet packet = (Packet) message; 51 | // if the connection is not native / direct, add websocket encoding 52 | resultBuffer = conn.isWebConnection() ? encodeOutgoingData(packet) : packet.getData(); 53 | } else if (message instanceof HandshakeResponse) { 54 | HandshakeResponse resp = (HandshakeResponse) message; 55 | resultBuffer = resp.getResponse(); 56 | } else if (message instanceof HandshakeRequest) { 57 | HandshakeRequest req = (HandshakeRequest) message; 58 | resultBuffer = req.getRequest(); 59 | } else { 60 | throw new Exception("message not a websocket type"); 61 | } 62 | out.write(resultBuffer); 63 | } 64 | 65 | // Encode the in buffer according to the Section 5.2. RFC 6455 66 | public static IoBuffer encodeOutgoingData(Packet packet) { 67 | log.debug("encode outgoing: {}", packet); 68 | // get the payload data 69 | IoBuffer data = packet.getData(); 70 | // get the frame length based on the byte count 71 | int frameLen = data.limit(); 72 | // start with frame length + 2b (header info) 73 | IoBuffer buffer = IoBuffer.allocate(frameLen + 2, false); 74 | buffer.setAutoExpand(true); 75 | // set the proper flags / opcode for the data 76 | byte frameInfo = (byte) (1 << 7); 77 | switch (packet.getType()) { 78 | case TEXT: 79 | if (log.isTraceEnabled()) { 80 | log.trace("Encoding text frame \r\n{}", new String(packet.getData().array())); 81 | } 82 | frameInfo = (byte) (frameInfo | 1); 83 | break; 84 | case BINARY: 85 | log.trace("Encoding binary frame"); 86 | frameInfo = (byte) (frameInfo | 2); 87 | break; 88 | case CLOSE: 89 | frameInfo = (byte) (frameInfo | 8); 90 | break; 91 | case CONTINUATION: 92 | frameInfo = (byte) (frameInfo | 0); 93 | break; 94 | case PING: 95 | log.trace("ping out"); 96 | frameInfo = (byte) (frameInfo | 9); 97 | break; 98 | case PONG: 99 | log.trace("pong out"); 100 | frameInfo = (byte) (frameInfo | 0xa); 101 | break; 102 | default: 103 | break; 104 | } 105 | buffer.put(frameInfo); 106 | // set the frame length 107 | log.trace("Frame length {} ", frameLen); 108 | if (frameLen <= 125) { 109 | buffer.put((byte) ((byte) frameLen & (byte) 0x7F)); 110 | } else if (frameLen > 125 && frameLen <= 65535) { 111 | buffer.put((byte) ((byte) 126 & (byte) 0x7F)); 112 | buffer.putShort((short) frameLen); 113 | } else { 114 | buffer.put((byte) ((byte) 127 & (byte) 0x7F)); 115 | buffer.putLong((int) frameLen); 116 | } 117 | buffer.put(data); 118 | buffer.flip(); 119 | if (log.isTraceEnabled()) { 120 | log.trace("Encoded: {}", buffer); 121 | } 122 | return buffer; 123 | } 124 | 125 | } 126 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/codec/extension/PerMessageDeflateExt.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.codec.extension; 20 | 21 | /** 22 | * Extension to WebSocket which provides per-message deflate. 23 | * 24 | * @see IETF Draft 25 | * @see Configuration and optimization 26 | * 27 | * @author Paul Gregoire 28 | */ 29 | public class PerMessageDeflateExt implements WebSocketExtension { 30 | 31 | private static final String id = "permessage-deflate"; 32 | 33 | /** {@inheritDoc} */ 34 | public String getId() { 35 | return id; 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/codec/extension/WebSocketExtension.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.codec.extension; 20 | 21 | /** 22 | * Common interface for WebSocket extensions. 23 | * 24 | * @author Paul Gregoire 25 | */ 26 | public interface WebSocketExtension { 27 | 28 | /** 29 | * Returns the extensions identifying string. 30 | * 31 | * @return id 32 | */ 33 | String getId(); 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/listener/DefaultWebSocketDataListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.listener; 20 | 21 | import java.io.UnsupportedEncodingException; 22 | import java.util.Set; 23 | 24 | import org.red5.net.websocket.WebSocketConnection; 25 | import org.red5.net.websocket.WebSocketPlugin; 26 | import org.red5.net.websocket.WebSocketScope; 27 | import org.red5.net.websocket.WebSocketScopeManager; 28 | import org.red5.net.websocket.model.WSMessage; 29 | import org.red5.server.plugin.PluginRegistry; 30 | import org.slf4j.Logger; 31 | import org.slf4j.LoggerFactory; 32 | 33 | /** 34 | * Default WebSocket data listener. In this default implementation, all messages are echoed back to every connection in the current scope. 35 | * 36 | * @author Paul Gregoire (mondain@gmail.com) 37 | */ 38 | public class DefaultWebSocketDataListener extends WebSocketDataListener { 39 | 40 | private static final Logger log = LoggerFactory.getLogger(DefaultWebSocketDataListener.class); 41 | 42 | @Override 43 | public void onWSConnect(WebSocketConnection conn) { 44 | log.info("Connect: {}", conn); 45 | } 46 | 47 | @Override 48 | public void onWSDisconnect(WebSocketConnection conn) { 49 | log.info("Disconnect: {}", conn); 50 | } 51 | 52 | @Override 53 | public void onWSMessage(WSMessage message) { 54 | // assume we have text 55 | String msg = new String(message.getPayload().array()); 56 | log.info("onWSMessage: {}", msg); 57 | // get the path 58 | String path = message.getPath(); 59 | // just echo back the message 60 | WebSocketScopeManager manager = ((WebSocketPlugin) PluginRegistry.getPlugin("WebSocketPlugin")).getManager(path); 61 | if (manager != null) { 62 | // get the ws scope 63 | WebSocketScope wsScope = manager.getScope(path); 64 | Set conns = wsScope.getConns(); 65 | for (WebSocketConnection conn : conns) { 66 | log.debug("Echoing to {}", conn); 67 | try { 68 | conn.send(msg); 69 | } catch (UnsupportedEncodingException e) { 70 | log.warn("Encoding issue with the message data: {}", message, e); 71 | } 72 | } 73 | } else { 74 | log.info("No manager found for path: {}", path); 75 | } 76 | } 77 | 78 | @Override 79 | public void stop() { 80 | log.info("Stop"); 81 | } 82 | 83 | } 84 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/listener/IWebSocketDataListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.listener; 20 | 21 | import org.red5.net.websocket.WebSocketConnection; 22 | import org.red5.net.websocket.model.WSMessage; 23 | 24 | /** 25 | * Listener for WebSocket events. 26 | */ 27 | public interface IWebSocketDataListener { 28 | 29 | /** 30 | * Returns the protocol for which this listener is interested. 31 | * 32 | * @return protocol 33 | */ 34 | public String getProtocol(); 35 | 36 | /** 37 | * Sets the protocol for which this listener is interested. 38 | * 39 | * @param protocol 40 | */ 41 | public void setProtocol(String protocol); 42 | 43 | /** 44 | * Dispatch message. 45 | * 46 | * @param message 47 | */ 48 | public void onWSMessage(WSMessage message); 49 | 50 | /** 51 | * Connect a WebSocket client. 52 | * 53 | * @param conn 54 | * WebSocketConnection 55 | */ 56 | public void onWSConnect(WebSocketConnection conn); 57 | 58 | /** 59 | * Disconnect WebSocket client. 60 | * 61 | * @param conn 62 | * WebSocketConnection 63 | */ 64 | public void onWSDisconnect(WebSocketConnection conn); 65 | 66 | /** 67 | * Stops the listener. 68 | */ 69 | public void stop(); 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/listener/IWebSocketScopeListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2016 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.listener; 20 | 21 | import org.red5.net.websocket.WebSocketScope; 22 | 23 | public interface IWebSocketScopeListener { 24 | 25 | void scopeCreated(WebSocketScope wsScope); 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/listener/WebSocketDataListener.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.listener; 20 | 21 | /** 22 | * Adapter class for WebSocket data listener interface. 23 | * 24 | * @author Paul Gregoire 25 | */ 26 | public abstract class WebSocketDataListener implements IWebSocketDataListener { 27 | 28 | /** 29 | * The protocol which this listener is interested in handling. 30 | */ 31 | protected String protocol = "undefined"; 32 | 33 | /** {@inheritDoc} */ 34 | @Override 35 | public String getProtocol() { 36 | return protocol; 37 | } 38 | 39 | /** {@inheritDoc} */ 40 | @Override 41 | public void setProtocol(String protocol) { 42 | this.protocol = protocol; 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/ConnectionType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | public enum ConnectionType { 22 | 23 | DIRECT, WEB; 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/HandshakeRequest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | import org.apache.mina.core.buffer.IoBuffer; 22 | 23 | /** 24 | * Wraps around a string that represents a websocket handshake request from the browser to the server. 25 | * 26 | * @author Paul Gregoire 27 | */ 28 | public class HandshakeRequest { 29 | 30 | private final IoBuffer req; 31 | 32 | public HandshakeRequest(IoBuffer req) { 33 | this.req = req; 34 | req.flip(); 35 | } 36 | 37 | public IoBuffer getRequest() { 38 | return req; 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/HandshakeResponse.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | import org.apache.mina.core.buffer.IoBuffer; 22 | 23 | /** 24 | * Wraps around a string that represents a websocket handshake response from the server to the browser. 25 | * 26 | * @author Dhruv Chopra 27 | * @author Paul Gregoire 28 | */ 29 | public class HandshakeResponse { 30 | 31 | private final IoBuffer response; 32 | 33 | public HandshakeResponse(IoBuffer response) { 34 | this.response = response; 35 | response.flip(); 36 | } 37 | 38 | public IoBuffer getResponse() { 39 | return response; 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/MessageType.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | public enum MessageType { 22 | 23 | CONTINUATION((byte) 0), TEXT((byte) 1), BINARY((byte) 2), CLOSE((byte) 8), PING((byte) 9), PONG((byte) 0xa); 24 | 25 | private byte type; 26 | 27 | MessageType(byte type) { 28 | this.type = type; 29 | } 30 | 31 | byte getType() { 32 | return type; 33 | } 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/Packet.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | import org.apache.mina.core.buffer.IoBuffer; 22 | 23 | /** 24 | * Defines the class whose objects are understood by websocket encoder. 25 | * 26 | * @author Dhruv Chopra 27 | * @author Paul Gregoire 28 | */ 29 | public class Packet { 30 | 31 | private final IoBuffer data; 32 | 33 | private final MessageType type; 34 | 35 | private Packet(byte[] buf) { 36 | this.data = IoBuffer.wrap(buf); 37 | this.type = MessageType.BINARY; 38 | } 39 | 40 | private Packet(byte[] buf, MessageType type) { 41 | this.data = IoBuffer.wrap(buf); 42 | this.type = type; 43 | } 44 | 45 | /** 46 | * Returns the data. 47 | * 48 | * @return data 49 | */ 50 | public IoBuffer getData() { 51 | return data; 52 | } 53 | 54 | /** 55 | * Returns the message type. 56 | * 57 | * @return type 58 | */ 59 | public MessageType getType() { 60 | return type; 61 | } 62 | 63 | /** 64 | * Builds the packet which just wraps the IoBuffer. 65 | * 66 | * @param buf 67 | * @return packet 68 | */ 69 | public static Packet build(byte[] buf) { 70 | return new Packet(buf); 71 | } 72 | 73 | /** 74 | * Builds the packet which just wraps the IoBuffer. 75 | * 76 | * @param buffer 77 | * @return packet 78 | */ 79 | public static Packet build(IoBuffer buffer) { 80 | if (buffer.hasArray()) { 81 | return new Packet(buffer.array()); 82 | } 83 | byte[] buf = new byte[buffer.remaining()]; 84 | buffer.get(buf); 85 | return new Packet(buf); 86 | } 87 | 88 | public static Packet build(MessageType type) { 89 | return new Packet(new byte[0], type); 90 | } 91 | 92 | public static Packet build(byte[] bytes, MessageType type) { 93 | return new Packet(bytes, type); 94 | } 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/WSMessage.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | import java.io.UnsupportedEncodingException; 22 | import java.lang.ref.WeakReference; 23 | 24 | import org.apache.mina.core.buffer.IoBuffer; 25 | import org.red5.net.websocket.WebSocketConnection; 26 | 27 | /** 28 | * Represents incoming WebSocket data which has been decoded. 29 | * 30 | * @author Paul Gregoire (mondain@gmail.com) 31 | */ 32 | public class WSMessage { 33 | 34 | // message type 35 | private MessageType messageType; 36 | 37 | // the originating connection for this message 38 | private WeakReference connection; 39 | 40 | // payload 41 | private IoBuffer payload; 42 | 43 | // the path on which this message originated 44 | private String path; 45 | 46 | // creation time 47 | private long timeStamp = System.currentTimeMillis(); 48 | 49 | /** 50 | * Returns the payload data as a UTF8 string. 51 | * 52 | * @return string 53 | * @throws UnsupportedEncodingException 54 | */ 55 | public String getMessageAsString() throws UnsupportedEncodingException { 56 | return new String(payload.array(), "UTF8").trim(); 57 | } 58 | 59 | public MessageType getMessageType() { 60 | return messageType; 61 | } 62 | 63 | public void setMessageType(MessageType messageType) { 64 | this.messageType = messageType; 65 | } 66 | 67 | public WebSocketConnection getConnection() { 68 | return connection.get(); 69 | } 70 | 71 | public void setConnection(WebSocketConnection connection) { 72 | this.connection = new WeakReference(connection); 73 | // set the connections path on the message 74 | setPath(connection.getPath()); 75 | } 76 | 77 | /** 78 | * Returns the payload. 79 | * 80 | * @return payload 81 | */ 82 | public IoBuffer getPayload() { 83 | return payload.flip(); 84 | } 85 | 86 | public void setPayload(IoBuffer payload) { 87 | this.payload = payload; 88 | } 89 | 90 | /** 91 | * Adds additional payload data. 92 | * 93 | * @param additionalPayload 94 | */ 95 | public void addPayload(IoBuffer additionalPayload) { 96 | if (payload == null) { 97 | payload = IoBuffer.allocate(additionalPayload.remaining()); 98 | payload.setAutoExpand(true); 99 | } 100 | this.payload.put(additionalPayload); 101 | } 102 | 103 | /** 104 | * Adds additional payload data. 105 | * 106 | * @param additionalPayload 107 | */ 108 | public void addPayload(byte[] additionalPayload) { 109 | if (payload == null) { 110 | payload = IoBuffer.allocate(additionalPayload.length); 111 | payload.setAutoExpand(true); 112 | } 113 | this.payload.put(additionalPayload); 114 | } 115 | 116 | public boolean isPayloadComplete() { 117 | return !payload.hasRemaining(); 118 | } 119 | 120 | public long getTimeStamp() { 121 | return timeStamp; 122 | } 123 | 124 | public String getPath() { 125 | return path; 126 | } 127 | 128 | public void setPath(String path) { 129 | this.path = path; 130 | } 131 | 132 | @Override 133 | public String toString() { 134 | return "WSMessage [messageType=" + messageType + ", timeStamp=" + timeStamp + ", path=" + path + ", payload=" + payload + "]"; 135 | } 136 | 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/model/WebSocketEvent.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.model; 20 | 21 | /** 22 | * WebSocket event enumeration. 23 | * 24 | * @author Paul Gregoire 25 | */ 26 | public enum WebSocketEvent { 27 | 28 | SCOPE_CREATED, SCOPE_ADDED, SCOPE_REMOVED; 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/org/red5/net/websocket/util/IdGenerator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * RED5 Open Source Flash Server - https://github.com/red5 3 | * 4 | * Copyright 2006-2015 by respective authors (see below). All rights reserved. 5 | * 6 | * Licensed under the Apache License, Version 2.0 (the "License"); 7 | * you may not use this file except in compliance with the License. 8 | * You may obtain a copy of the License at 9 | * 10 | * http://www.apache.org/licenses/LICENSE-2.0 11 | * 12 | * Unless required by applicable law or agreed to in writing, software 13 | * distributed under the License is distributed on an "AS IS" BASIS, 14 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | * See the License for the specific language governing permissions and 16 | * limitations under the License. 17 | */ 18 | 19 | package org.red5.net.websocket.util; 20 | 21 | import java.util.concurrent.ThreadLocalRandom; 22 | 23 | import org.bouncycastle.crypto.digests.SHA1Digest; 24 | import org.bouncycastle.crypto.prng.DigestRandomGenerator; 25 | 26 | /** 27 | * Id generator. 28 | * 29 | * @author Paul Gregoire 30 | */ 31 | public class IdGenerator { 32 | 33 | private static final DigestRandomGenerator random = new DigestRandomGenerator(new SHA1Digest()); 34 | 35 | /** 36 | * Returns a cryptographically generated id. 37 | * 38 | * @return id 39 | */ 40 | public static final long generateId() { 41 | long id = 0; 42 | // add new seed material from current time 43 | random.addSeedMaterial(ThreadLocalRandom.current().nextLong()); 44 | // get a new id 45 | byte[] bytes = new byte[16]; 46 | // get random bytes 47 | random.nextBytes(bytes); 48 | for (int i = 0; i < bytes.length; i++) { 49 | id += ((long) bytes[i] & 0xffL) << (8 * i); 50 | } 51 | //System.out.println("Id: " + id); 52 | return id; 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/logback-websocket.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | websocket 4 | 5 | 6 | log/websocket.log 7 | false 8 | UTF-8 9 | false 10 | true 11 | 12 | 13 | %date [%thread] %-5level %logger{35} - %msg%n 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/test/resources/logback-test.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | [%p] [%thread] %logger - %msg%n 6 | 7 | 8 | 9 | 10 | target/test.log 11 | false 12 | 13 | %d{ISO8601} [%thread] %-5level %logger{35} - %msg%n 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | --------------------------------------------------------------------------------