Skip to content

Commit c528168

Browse files
committed
Refactor TLS names and flow; add RTMPS doc draft
1 parent 5e9b4b1 commit c528168

File tree

7 files changed

+205
-141
lines changed

7 files changed

+205
-141
lines changed

RTMPS.md

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# RTMPS
2+
3+
RTMPS is a secure version of RTMP that uses TLS/SSL to encrypt the data. This is a guide to setting up RTMPS with Red5. An example keystore and truststore creation process will be explained as these files are required for the RTMPS feature. Examples will be provided for both the server and client side which will demonstrate how to use RTMPS and PKCS12 type keystores; JKS keystores can also be used, but are not covered here.
4+
5+
## Configuration
6+
7+
### Server
8+
9+
On a server where RTMPS will be employed, two files in `conf` must be updated: `red5.properties` and `red5-core.xml`. This is in-addition to the keystore and truststore proceedure.
10+
11+
* In `red5-core.xml` uncomment the beans named `rtmpsMinaIoHandler` and `rtmpsTransport` which may be updated as required, otherwise their values come from the `red5.properties` file. See [Advanced-configuration](#advanced-configuration) for more information.
12+
13+
* In `red5.properties`, update these properties to utilize your values; especially for store passwords and locations:
14+
15+
```properties
16+
# RTMPS
17+
rtmps.host=0.0.0.0
18+
rtmps.port=8443
19+
rtmps.ping_interval=5000
20+
rtmps.max_inactivity=60000
21+
rtmps.max_keep_alive_requests=-1
22+
rtmps.max_threads=8
23+
rtmps.acceptor_thread_count=2
24+
rtmps.processor_cache=20
25+
# RTMPS Key and Trust store parameters
26+
rtmps.keystorepass=password123
27+
rtmps.keystorefile=conf/server.p12
28+
rtmps.truststorepass=password123
29+
rtmps.truststorefile=conf/truststore.p12
30+
```
31+
32+
### Client
33+
34+
35+
```java
36+
TLSFactory.setKeystorePath("/workspace/client/conf/rtmps_server.p12");
37+
TLSFactory.setTruststorePath("/workspace/client/conf/rtmps_truststore.p12");
38+
```
39+
40+
## Testing
41+
42+
Using ffplay issue the following, update for your server IP and stream name as needed: `ffplay rtmps://localhost:8443/live/stream1`
43+
44+
45+
### Useful System Properties
46+
47+
48+
`-Djavax.net.debug=SSL,handshake,verbose,trustmanager,keymanager,record,plaintext`
49+
50+
## Advanced configuration
51+
52+
* The ciphers and protocols can be modified in the `rtmpsMinaIoHandler` bean in `red5-core.xml` to suit any special needs.
53+
54+
```xml
55+
<bean id="rtmpsMinaIoHandler" class="org.red5.server.net.rtmps.RTMPSMinaIoHandler">
56+
<property name="handler" ref="rtmpHandler" />
57+
<property name="keystorePassword" value="${rtmps.keystorepass}" />
58+
<property name="keystoreFile" value="${rtmps.keystorefile}" />
59+
<property name="truststorePassword" value="${rtmps.truststorepass}" />
60+
<property name="truststoreFile" value="${rtmps.truststorefile}" />
61+
<property name="cipherSuites">
62+
<array>
63+
<value>TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256</value>
64+
<value>TLS_RSA_WITH_AES_128_CBC_SHA256</value>
65+
</array>
66+
</property>
67+
<property name="protocols">
68+
<array>
69+
<value>TLSv1.2</value>
70+
<value>TLSv1.3</value>
71+
</array>
72+
</property>
73+
</bean>
74+
```

client/src/main/java/org/red5/client/net/rtmps/RTMPSClient.java

Lines changed: 55 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import org.apache.mina.core.session.IoSession;
1717
import org.apache.mina.filter.ssl.SslFilter;
1818
import org.apache.mina.transport.socket.nio.NioSocketConnector;
19+
import org.red5.client.net.rtmp.ClientExceptionHandler;
1920
import org.red5.client.net.rtmp.RTMPClient;
2021
import org.red5.client.net.rtmp.RTMPMinaIoHandler;
2122
import org.red5.io.tls.TLSFactory;
@@ -39,7 +40,7 @@ public class RTMPSClient extends RTMPClient {
3940

4041
private static final Logger log = LoggerFactory.getLogger(RTMPSClient.class);
4142

42-
private static String[] cipherSuites = new String[] { "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA256", "TLS_RSA_WITH_AES_128_CBC_SHA" };
43+
private static String[] cipherSuites;
4344

4445
// I/O handler
4546
private final RTMPSClientIoHandler ioHandler;
@@ -59,11 +60,37 @@ public RTMPSClient() {
5960
protocol = "rtmps";
6061
ioHandler = new RTMPSClientIoHandler();
6162
ioHandler.setHandler(this);
63+
setExceptionHandler(new ClientExceptionHandler() {
64+
@Override
65+
public void handleException(Throwable throwable) {
66+
log.error("Exception", throwable);
67+
try {
68+
ioHandler.exceptionCaught(null, throwable);
69+
} catch (Exception e) {
70+
log.debug("Exception", e);
71+
}
72+
}
73+
});
74+
}
75+
76+
/**
77+
* Creates a new RTMPSClient with the given keystore type and password.
78+
*
79+
* @param keyStoreType keystore type
80+
* @param password keystore password
81+
*/
82+
public RTMPSClient(String keyStoreType, String password) {
83+
protocol = "rtmps";
84+
this.keyStoreType = keyStoreType;
85+
this.password = password.toCharArray();
86+
ioHandler = new RTMPSClientIoHandler();
87+
ioHandler.setHandler(this);
6288
}
6389

6490
@SuppressWarnings({ "rawtypes" })
6591
@Override
6692
protected void startConnector(String server, int port) {
93+
log.debug("startConnector - server: {} port: {}", server, port);
6794
socketConnector = new NioSocketConnector();
6895
socketConnector.setHandler(ioHandler);
6996
future = socketConnector.connect(new InetSocketAddress(server, port));
@@ -73,20 +100,17 @@ public void operationComplete(IoFuture future) {
73100
try {
74101
// will throw RuntimeException after connection error
75102
future.getSession();
76-
} catch (Throwable e) {
77-
//if there isn't an ClientExceptionHandler set, a
78-
//RuntimeException may be thrown in handleException
79-
handleException(e);
103+
} catch (Throwable t) {
104+
try {
105+
ioHandler.exceptionCaught(null, t);
106+
} catch (Exception e) {
107+
// no-op
108+
}
80109
}
81110
}
82111
});
83-
// Do the close requesting that the pending messages are sent before
84-
// the session is closed
85-
//future.getSession().close(false);
86112
// Now wait for the close to be completed
87113
future.awaitUninterruptibly(CONNECTOR_WORKER_TIMEOUT);
88-
// We can now dispose the connector
89-
//socketConnector.dispose();
90114
}
91115

92116
/**
@@ -107,34 +131,47 @@ public void setKeyStoreType(String keyStoreType) {
107131
this.keyStoreType = keyStoreType;
108132
}
109133

134+
public static void setCipherSuites(String[] cipherSuites) {
135+
RTMPSClient.cipherSuites = cipherSuites;
136+
}
137+
110138
private class RTMPSClientIoHandler extends RTMPMinaIoHandler {
111139

112140
/** {@inheritDoc} */
113141
@Override
114142
public void sessionOpened(IoSession session) throws Exception {
115-
// START OF NATIVE SSL STUFF
143+
log.debug("RTMPS sessionOpened: {}", session);
144+
// do tls stuff
116145
SSLContext context = TLSFactory.getTLSContext(keyStoreType, password);
117146
SslFilter sslFilter = new SslFilter(context);
118147
if (sslFilter != null) {
119148
// we are a client
120149
sslFilter.setUseClientMode(true);
121150
// set the cipher suites
122-
//sslFilter.setEnabledCipherSuites(cipherSuites);
151+
if (cipherSuites != null) {
152+
sslFilter.setEnabledCipherSuites(cipherSuites);
153+
}
123154
session.getFilterChain().addFirst("sslFilter", sslFilter);
124155
}
125-
// END OF NATIVE SSL STUFF
126156
super.sessionOpened(session);
127157
}
128158

159+
@Override
160+
public void sessionClosed(IoSession session) throws Exception {
161+
log.debug("RTMPS sessionClosed: {}", session);
162+
super.sessionClosed(session);
163+
}
164+
129165
/** {@inheritDoc} */
130166
@Override
131167
public void exceptionCaught(IoSession session, Throwable cause) throws Exception {
132-
log.warn("Exception caught {}", cause.getMessage());
133-
if (log.isDebugEnabled()) {
134-
log.error("Exception detail", cause);
168+
log.warn("Exception caught: {}", cause.getMessage());
169+
log.debug("Exception detail", cause);
170+
// if there are any errors using ssl, kill the session
171+
if (session != null) {
172+
session.closeNow();
135173
}
136-
//if there are any errors using ssl, kill the session
137-
session.closeNow();
174+
socketConnector.dispose(false);
138175
}
139176

140177
}

io/src/main/java/org/red5/io/tls/TLSFactory.java

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ public class TLSFactory {
2222

2323
private static final Logger log = LoggerFactory.getLogger(TLSFactory.class);
2424

25-
private static final boolean isDebug = log.isDebugEnabled();
25+
private static final boolean isDebug = log.isDebugEnabled(), isTrace = log.isTraceEnabled();
2626

27+
// shared thread-safe random
2728
private static final SecureRandom RANDOM = new SecureRandom();
2829

2930
public static final int MAX_HANDSHAKE_LOOPS = 200;
@@ -44,20 +45,19 @@ public class TLSFactory {
4445
*/
4546
private static String storeType = "PKCS12"; // JKS or PKCS12
4647

47-
private static String keyStoreFile = String.format("server.%s", "PKCS12".equals(storeType) ? "p12" : "jks");
48+
private static String keyStoreFile = String.format("server.%s", "PKCS12".equals(storeType) ? "p12" : "jks"), trustStoreFile = String.format("truststore.%s", "PKCS12".equals(storeType) ? "p12" : "jks");
4849

49-
private static String trustStoreFile = String.format("truststore.%s", "PKCS12".equals(storeType) ? "p12" : "jks");
50+
private static String keystorePath = Paths.get(System.getProperty("user.dir"), "conf", keyStoreFile).toString(), truststorePath = Paths.get(System.getProperty("user.dir"), "conf", trustStoreFile).toString();
5051

5152
private static String passwd = "password123";
5253

53-
private static String keyFilename = Paths.get(System.getProperty("user.dir"), "conf", keyStoreFile).toString();
54-
55-
private static String trustFilename = Paths.get(System.getProperty("user.dir"), "conf", trustStoreFile).toString();
56-
5754
static {
5855
if (isDebug) {
59-
//System.setProperty("javax.net.debug", "all");
60-
System.setProperty("javax.net.debug", "SSL,handshake,verbose,trustmanager,keymanager,record,plaintext");
56+
if (isTrace) {
57+
System.setProperty("javax.net.debug", "SSL,handshake,verbose,trustmanager,keymanager,record,plaintext");
58+
} else {
59+
System.setProperty("javax.net.debug", "all");
60+
}
6161
}
6262
// set unlimited crypto policy
6363
Security.setProperty("crypto.policy", "unlimited");
@@ -79,27 +79,27 @@ public class TLSFactory {
7979
}
8080

8181
public static SSLContext getTLSContext() throws Exception {
82-
log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keyFilename, trustFilename, storeType);
82+
log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keystorePath, truststorePath, storeType);
8383
KeyStore ks = KeyStore.getInstance(storeType);
8484
KeyStore ts = KeyStore.getInstance(storeType);
8585
char[] passphrase = passwd.toCharArray();
86-
try (FileInputStream fis = new FileInputStream(keyFilename)) {
86+
try (FileInputStream fis = new FileInputStream(keystorePath)) {
8787
ks.load(fis, passphrase);
8888
} catch (Exception e) {
89-
log.error("Failed to load keystore: {}", keyFilename, e);
89+
log.error("Failed to load keystore: {}", keystorePath, e);
9090
throw e;
9191
}
92-
try (FileInputStream fis = new FileInputStream(trustFilename)) {
92+
try (FileInputStream fis = new FileInputStream(truststorePath)) {
9393
ts.load(fis, passphrase);
9494
} catch (Exception e) {
95-
log.error("Failed to load truststore: {}", trustFilename, e);
95+
log.error("Failed to load truststore: {}", truststorePath, e);
9696
throw e;
9797
}
9898
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
9999
try {
100100
kmf.init(ks, passphrase);
101101
} catch (UnrecoverableKeyException e) {
102-
log.error("Failed to initialize KeyManagerFactory with keystore: {}", keyFilename, e);
102+
log.error("Failed to initialize KeyManagerFactory with keystore: {}", keystorePath, e);
103103
throw e;
104104
}
105105
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
@@ -110,21 +110,21 @@ public static SSLContext getTLSContext() throws Exception {
110110
}
111111

112112
public static SSLContext getTLSContext(String storeType, char[] passphrase) throws Exception {
113-
log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keyFilename, trustFilename, storeType);
114-
log.debug("Keystore - file name: {} password: {}", keyFilename, passphrase);
115-
log.debug("Truststore - file name: {} password: {}", trustFilename, passphrase);
113+
log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keystorePath, truststorePath, storeType);
114+
log.debug("Keystore - file: {} password: {}", keystorePath, passphrase);
115+
log.debug("Truststore - file: {} password: {}", truststorePath, passphrase);
116116
KeyStore ks = KeyStore.getInstance(storeType);
117117
KeyStore ts = KeyStore.getInstance(storeType);
118-
try (FileInputStream fis = new FileInputStream(keyFilename)) {
118+
try (FileInputStream fis = new FileInputStream(keystorePath)) {
119119
ks.load(fis, passphrase);
120120
} catch (Exception e) {
121-
log.error("Failed to load keystore: {}", keyFilename, e);
121+
log.error("Failed to load keystore: {}", keystorePath, e);
122122
throw e;
123123
}
124-
try (FileInputStream fis = new FileInputStream(trustFilename)) {
124+
try (FileInputStream fis = new FileInputStream(truststorePath)) {
125125
ts.load(fis, passphrase);
126126
} catch (Exception e) {
127-
log.error("Failed to load truststore: {}", trustFilename, e);
127+
log.error("Failed to load truststore: {}", truststorePath, e);
128128
throw e;
129129
}
130130
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
@@ -133,7 +133,7 @@ public static SSLContext getTLSContext(String storeType, char[] passphrase) thro
133133
log.debug("Private key: {}", privateKey);
134134
kmf.init(ks, passphrase);
135135
} catch (UnrecoverableKeyException e) {
136-
log.error("Failed to initialize KeyManagerFactory with keystore: {}", keyFilename, e);
136+
log.error("Failed to initialize KeyManagerFactory with keystore: {}", keystorePath, e);
137137
throw e;
138138
}
139139
TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509");
@@ -143,18 +143,18 @@ public static SSLContext getTLSContext(String storeType, char[] passphrase) thro
143143
return sslCtx;
144144
}
145145

146-
public static SSLContext getTLSContext(String storeType, String keystorePassword, String keyFilename, String truststorePassword, String trustFilename) throws Exception {
147-
log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keyFilename, trustFilename, storeType);
148-
log.debug("Keystore - file name: {} password: {}", keyFilename, keystorePassword);
149-
log.debug("Truststore - file name: {} password: {}", trustFilename, truststorePassword);
146+
public static SSLContext getTLSContext(String storeType, String keystorePassword, String keystorePath, String truststorePassword, String truststorePath) throws Exception {
147+
log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keystorePath, truststorePath, storeType);
148+
log.debug("Keystore - file: {} password: {}", keystorePath, keystorePassword);
149+
log.debug("Truststore - file: {} password: {}", truststorePath, truststorePassword);
150150
KeyStore ks = KeyStore.getInstance(storeType);
151151
KeyStore ts = KeyStore.getInstance(storeType);
152152
char[] keyStrorePassphrase = keystorePassword.toCharArray();
153153
char[] trustStorePassphrase = truststorePassword.toCharArray();
154-
try (FileInputStream fis = new FileInputStream(keyFilename)) {
154+
try (FileInputStream fis = new FileInputStream(keystorePath)) {
155155
ks.load(fis, keyStrorePassphrase);
156156
}
157-
try (FileInputStream fis = new FileInputStream(trustFilename)) {
157+
try (FileInputStream fis = new FileInputStream(truststorePath)) {
158158
ts.load(fis, trustStorePassphrase);
159159
}
160160
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
@@ -208,20 +208,20 @@ public static void setPasswd(String passwd) {
208208
TLSFactory.passwd = passwd;
209209
}
210210

211-
public static String getKeyFilename() {
212-
return keyFilename;
211+
public static String getKeystorePath() {
212+
return keystorePath;
213213
}
214214

215-
public static void setKeyFilename(String keyFilename) {
216-
TLSFactory.keyFilename = keyFilename;
215+
public static void setKeystorePath(String keystorePath) {
216+
TLSFactory.keystorePath = keystorePath;
217217
}
218218

219-
public static String getTrustFilename() {
220-
return trustFilename;
219+
public static String getTruststorePath() {
220+
return truststorePath;
221221
}
222222

223-
public static void setTrustFilename(String trustFilename) {
224-
TLSFactory.trustFilename = trustFilename;
223+
public static void setTruststorePath(String truststorePath) {
224+
TLSFactory.truststorePath = truststorePath;
225225
}
226226

227227
}

0 commit comments

Comments
 (0)