├── .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 |
--------------------------------------------------------------------------------