From 2e8dd875777c52897890d31ee469a3b790729c31 Mon Sep 17 00:00:00 2001 From: Paul Gregoire Date: Tue, 24 Sep 2024 07:30:47 -0700 Subject: [PATCH] Pull updates from master; move tests --- RTMPS.md | 273 ++++++ .../java/org/red5/client/StreamRelay.java | 2 +- .../red5/client/net/rtmps/RTMPSClient.java | 127 ++- .../red5/client/net/rtmps/RTMPTSClient.java | 4 + .../net/ssl/BogusSslContextFactory.java | 113 --- .../net/ssl/BogusTrustManagerFactory.java | 55 -- .../net/ssl/SslServerSocketFactory.java | 68 -- .../red5/client/net/ssl/SslSocketFactory.java | 89 -- client/src/test/resources/bogus.cert | Bin 979 -> 0 bytes client/src/test/resources/logback-test.xml | 32 - .../red5/logging/LoggingContextSelector.java | 2 - .../scheduling/JDKSchedulingService.java | 4 +- .../java/org/red5/server/scope/Scope.java | 25 +- .../main/java/org/red5/io/tls/TLSFactory.java | 281 ++++++ .../main/java/org/red5/io/utils/TlsUtils.java | 885 ++++++++++++++++++ .../server/net/rtmps/RTMPSMinaIoHandler.java | 116 +-- .../red5/server/tomcat/rtmps/RTMPSLoader.java | 10 +- server/src/main/server/conf/red5-core.xml | 26 +- server/src/main/server/conf/red5.properties | 23 - .../test/java/org/red5/client/ClientTest.java | 0 .../client}/PublisherConnectLoadTest.java | 0 .../java/org/red5/client/RTMPTClientTest.java | 0 .../java/org/red5/client/RemoteSOTest.java | 0 .../org/red5/client/SharedObjectClient.java | 0 .../client/net/rtmp/ClientMethodHander.java | 0 .../client/net/rtmp/FBLiveConnectTest.java | 0 .../red5/client/net/rtmp/RTMPClientTest.java | 0 .../client/net/rtmp/RTMPHandshakeTest.java | 0 .../client/net/rtmp/YouTubeConnectTest.java | 0 .../codec/RTMPClientProtocolDecoderTest.java | 0 .../client/net/rtmpe/RTMPEClientTest.java | 0 .../client/net/rtmps/RTMPSClientTest.java | 0 .../client/net/rtmps/RTMPTSClientTest.java | 0 .../red5/client/util/PropertiesReader.java | 0 .../red5/io/utils/ConversionUtilsTest.java | 0 .../net/websocket/WebSocketServerTest.java | 0 .../net/rtmp/ServerRTMPHandshakeTest.java | 0 .../rtmp/codec/TestRTMPProtocolDecoder.java | 0 .../java/org/red5/server/scope/ScopeTest.java | 0 .../server/service/CuePointInjectionTest.java | 0 .../server/service/MetaDataInjectionTest.java | 0 .../org/red5/server/service/MetaDataTest.java | 0 .../red5/server/service/MetaServiceTest.java | 0 .../server/service/ReflectionUtilsTest.java | 0 .../test/java/org/red5/server/so/Complex.java | 0 .../org/red5/server/so/SOApplication.java | 0 .../java/org/red5/server/so/SOListener.java | 0 .../org/red5/server/so/SharedObjectTest.java | 0 .../stream/provider/FileProviderTest.java | 0 .../red5/test/selftest/EchoApplication.java | 0 .../test/selftest/ExternalizableClass.java | 0 .../org/red5/test/selftest/RemoteClass.java | 0 .../src/main}/test/resources/RTMP-C0C1.dat | Bin .../src/main}/test/resources/RTMP-C2.dat | Bin .../main}/test/resources/RTMP-S0S1S2-01.dat | Bin .../main}/test/resources/RTMP-S0S1S2-02.dat | Bin .../src/main}/test/resources/Readme.md | 0 .../test/resources/fixtures/rotations.flv | Bin .../main}/test/resources/fixtures/test.flv | Bin .../src/main}/test/resources/logback-test.xml | 0 .../org/red5/server/scope/ScopeTest.xml | 0 .../org/red5/server/so/SharedObjectTest.xml | 0 .../stream/provider/FileProviderTest.xml | 0 .../src/main}/test/resources/test.properties | 0 64 files changed, 1615 insertions(+), 520 deletions(-) create mode 100644 RTMPS.md delete mode 100644 client/src/main/java/org/red5/client/net/ssl/BogusSslContextFactory.java delete mode 100644 client/src/main/java/org/red5/client/net/ssl/BogusTrustManagerFactory.java delete mode 100644 client/src/main/java/org/red5/client/net/ssl/SslServerSocketFactory.java delete mode 100644 client/src/main/java/org/red5/client/net/ssl/SslSocketFactory.java delete mode 100644 client/src/test/resources/bogus.cert delete mode 100644 client/src/test/resources/logback-test.xml create mode 100644 io/src/main/java/org/red5/io/tls/TLSFactory.java create mode 100644 io/src/main/java/org/red5/io/utils/TlsUtils.java rename {client/src => tests/src/main}/test/java/org/red5/client/ClientTest.java (100%) rename {client/src/main/java/org/red5/client/test => tests/src/main/test/java/org/red5/client}/PublisherConnectLoadTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/RTMPTClientTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/RemoteSOTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/SharedObjectClient.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmp/ClientMethodHander.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmp/FBLiveConnectTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmp/RTMPClientTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmp/RTMPHandshakeTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmp/YouTubeConnectTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmp/codec/RTMPClientProtocolDecoderTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmpe/RTMPEClientTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmps/RTMPSClientTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/net/rtmps/RTMPTSClientTest.java (100%) rename {client/src => tests/src/main}/test/java/org/red5/client/util/PropertiesReader.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/io/utils/ConversionUtilsTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/net/websocket/WebSocketServerTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/net/rtmp/ServerRTMPHandshakeTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/net/rtmp/codec/TestRTMPProtocolDecoder.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/scope/ScopeTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/service/CuePointInjectionTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/service/MetaDataInjectionTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/service/MetaDataTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/service/MetaServiceTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/service/ReflectionUtilsTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/so/Complex.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/so/SOApplication.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/so/SOListener.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/so/SharedObjectTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/server/stream/provider/FileProviderTest.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/test/selftest/EchoApplication.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/test/selftest/ExternalizableClass.java (100%) rename {server/src => tests/src/main}/test/java/org/red5/test/selftest/RemoteClass.java (100%) rename {client/src => tests/src/main}/test/resources/RTMP-C0C1.dat (100%) rename {client/src => tests/src/main}/test/resources/RTMP-C2.dat (100%) rename {client/src => tests/src/main}/test/resources/RTMP-S0S1S2-01.dat (100%) rename {client/src => tests/src/main}/test/resources/RTMP-S0S1S2-02.dat (100%) rename {client/src => tests/src/main}/test/resources/Readme.md (100%) rename {client/src => tests/src/main}/test/resources/fixtures/rotations.flv (100%) rename {server/src => tests/src/main}/test/resources/fixtures/test.flv (100%) rename {server/src => tests/src/main}/test/resources/logback-test.xml (100%) rename {server/src => tests/src/main}/test/resources/org/red5/server/scope/ScopeTest.xml (100%) rename {server/src => tests/src/main}/test/resources/org/red5/server/so/SharedObjectTest.xml (100%) rename {server/src => tests/src/main}/test/resources/org/red5/server/stream/provider/FileProviderTest.xml (100%) rename {client/src => tests/src/main}/test/resources/test.properties (100%) diff --git a/RTMPS.md b/RTMPS.md new file mode 100644 index 000000000..c17149fc5 --- /dev/null +++ b/RTMPS.md @@ -0,0 +1,273 @@ +# RTMPS + +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. + +## Keystore and Truststore Creation + +The following commands will create the necessary files for the RTMPS feature. The keystore will contain the server certificate and private key, while the truststore will contain the CA certificate. The client will use the truststore to verify the server certificate. Self-signed certificates are used in this example and are not expected to prevent the client from connecting to the server; in testing, the `ffplay` worked without issue. Examples show sample input for the certificate creation process. + +* Create our CA key and certificate for self-signing: + +```bash +openssl ecparam -name prime256v1 -genkeopenssl ecparam -name prime256v1 -genkey -noout -out ca.key + +openssl req -new -x509 -sha256 -key ca.key -out ca.crt -days 3650 + +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:US +State or Province Name (full name) [Some-State]:Nevada +Locality Name (eg, city) []:Henderson +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Red5 +Organizational Unit Name (eg, section) []:dev +Common Name (e.g. server FQDN or YOUR name) []:Paul Gregoire +Email Address []:mondain@gmail.com +``` + +* Create the server key and certificate request: + +```bash +openssl ecparam -name prime256v1 -genkey -noout -out server.key + +openssl req -new -sha256 -key server.key -out server.csr + +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:US +State or Province Name (full name) [Some-State]:Nevada +Locality Name (eg, city) []:Henderson +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Red5 +Organizational Unit Name (eg, section) []:dev +Common Name (e.g. server FQDN or YOUR name) []:mondain-XPS-8930 +Email Address []:mondain@gmail.com + +Please enter the following 'extra' attributes +to be sent with your certificate request +A challenge password []: +An optional company name []: +``` + +* CA sign the server certificate request: + +```bash +openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 + +Certificate request self-signature ok +subject=C = US, ST = Nevada, L = Henderson, O = Red5, OU = dev, CN = mondain-XPS-8930, emailAddress = mondain@gmail.com +``` + +* Create the client key and certificate request: + +```bash +openssl ecparam -name prime256v1 -genkey -noout -out client.key + +openssl req -new -sha256 -key client.key -out client.csr + +You are about to be asked to enter information that will be incorporated +into your certificate request. +What you are about to enter is what is called a Distinguished Name or a DN. +There are quite a few fields but you can leave some blank +For some fields there will be a default value, +If you enter '.', the field will be left blank. +----- +Country Name (2 letter code) [AU]:US +State or Province Name (full name) [Some-State]:Nevada +Locality Name (eg, city) []:Henderson +Organization Name (eg, company) [Internet Widgits Pty Ltd]:Red5 +Organizational Unit Name (eg, section) []:dev +Common Name (e.g. server FQDN or YOUR name) []:mondain-XPS-8930 +Email Address []:mondain@gmail.com + +Please enter the following 'extra' attributes +to be sent with your certificate request +A challenge password []: +An optional company name []: +``` + +* CA sign the client certificate request: + +```bash +openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 3650 + +Certificate request self-signature ok +subject=C = US, ST = Nevada, L = Henderson, O = Red5, OU = dev, CN = mondain-XPS-8930, emailAddress = mondain@gmail.com +``` + +* Add the server certificate to the keystore (_Make sure to use the same password for the key and store_): + +```bash +keytool -genkey -dname "CN=mondain-XPS-8930, OU=dev, O=Red5, L=Henderson, S=Nevada, C=US" -keystore rtmps_keystore.jks -storepass password123 -keypass password123 -alias server -keyalg RSA -file server.crt + +Generating 2,048 bit RSA key pair and self-signed certificate (SHA256withRSA) with a validity of 90 days + for: CN=mondain-XPS-8930, OU=dev, O=Red5, L=Henderson, ST=Nevada, C=US +``` + +* Add the self-signed CA root certificate to the truststore (_Make sure to use the same password for the store_): + +```bash +keytool -import -trustcacerts -file ca.crt -alias CARoot -keystore rtmps_truststore.jks -storepass password123 + +Owner: EMAILADDRESS=mondain@gmail.com, CN=Paul Gregoire, OU=dev, O=Red5, L=Henderson, ST=Nevada, C=US +Issuer: EMAILADDRESS=mondain@gmail.com, CN=Paul Gregoire, OU=dev, O=Red5, L=Henderson, ST=Nevada, C=US +Serial number: 7139dce6b44a5e3d50ace573849cf88e63366153 +Valid from: Mon Mar 04 18:10:14 PST 2024 until: Thu Mar 02 18:10:14 PST 2034 +Certificate fingerprints: + SHA1: 48:CC:8A:65:5B:96:5B:7B:39:6C:55:27:30:84:24:B8:67:B0:91:6A + SHA256: C0:41:37:4C:DB:49:12:6B:14:C5:B4:8E:4A:28:1C:33:A0:C2:38:C7:76:44:97:6B:5E:A0:7B:20:01:0F:C9:2C +Signature algorithm name: SHA256withECDSA +Subject Public Key Algorithm: 256-bit EC (secp256r1) key +Version: 3 + +Extensions: + +#1: ObjectId: 2.5.29.35 Criticality=false +AuthorityKeyIdentifier [ +KeyIdentifier [ +0000: FF 05 5E DA 39 EB B5 40 E2 0D 5F 6A 90 DC C3 0B ..^.9..@.._j.... +0010: 12 B2 6D F6 ..m. +] +] + +#2: ObjectId: 2.5.29.19 Criticality=true +BasicConstraints:[ + CA:true + PathLen: no limit +] + +#3: ObjectId: 2.5.29.14 Criticality=false +SubjectKeyIdentifier [ +KeyIdentifier [ +0000: FF 05 5E DA 39 EB B5 40 E2 0D 5F 6A 90 DC C3 0B ..^.9..@.._j.... +0010: 12 B2 6D F6 ..m. +] +] + +Trust this certificate? [no]: yes +Certificate was added to keystore +``` + +* Last step is to convert the keystore and truststore to PKCS12 format (_Make sure to use the same passwords_): + +```bash + +keytool -importkeystore -srckeystore rtmps_keystore.jks -destkeystore rtmps_keystore.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass password123 -deststorepass password123 -srcalias server -destalias server -noprompt + +keytool -importkeystore -srckeystore rtmps_truststore.jks -destkeystore rtmps_truststore.p12 -srcstoretype JKS -deststoretype PKCS12 -srcstorepass password123 -deststorepass password123 +``` + +## Configuration + +The following configuration changes are required to enable RTMPS in Red5. + +### Server + +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. + +* 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. Note that the previous property names `keyStoreFile` and `trustStoreFile` have been replaced with `keystorePath` and `truststorePath`. + +```xml + + + + + + + +``` + +To modify the ciphers and / or protocols in the `rtmpsMinaIoHandler` bean in `red5-core.xml`, see the example below: + +```xml + + + + + + + + + TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 + TLS_RSA_WITH_AES_128_CBC_SHA256 + + + + + TLSv1.2 + TLSv1.3 + + + +``` + +* The `rtmpsTransport` is not expected to need modification, but can be updated as required. The `rtmps.host` and `rtmps.port` properties are required to be set in `red5.properties` and are used in the `rtmpsTransport` bean: + +```xml + + + + + ${rtmps.host}:${rtmps.port} + + + + + +``` + +* In `red5.properties`, update these properties to utilize your values; especially for store passwords and locations: + +```properties +# RTMPS +rtmps.host=0.0.0.0 +rtmps.port=8443 +rtmps.ping_interval=5000 +rtmps.max_inactivity=60000 +rtmps.max_keep_alive_requests=-1 +rtmps.max_threads=8 +rtmps.acceptor_thread_count=2 +rtmps.processor_cache=20 +# RTMPS Key and Trust store parameters +rtmps.keystorepass=password123 +rtmps.keystorefile=conf/rtmps_keystore.p12 +rtmps.truststorepass=password123 +rtmps.truststorefile=conf/rtmps_truststore.p12 +``` + +### Client + +When connecting to a server that uses RTMPS, the client must have the server's certificate in its truststore. The following example demonstrates how to use the truststore with the Red5 client. Before connecting to the server, the client must set the keystore and truststore paths with password. + +* Using full paths to the keystore and truststore files: + +```java +TLSFactory.setKeystorePath("/workspace/client/conf/rtmps_keystore.p12"); +TLSFactory.setTruststorePath("/workspace/client/conf/rtmps_truststore.p12"); +``` + +* When the keystore and truststore are contained within a jar file, use the following format: `jar:file:/path/to/your.jar!/path/to/file/in/jar` for the keystore and truststore paths. This example assumes the jar file which is named `my_rtmps_client.jar` file is contained in a `lib` sub-directory of the application client launch location and the keystore and truststore are in the root: + +```java +String jarKeystorePath = String.format("jar:file:%s/lib/my_rtmps_client.jar!/rtmps_%s.p12", Paths.get(System.getProperty("user.dir"), "keystore"); +TLSFactory.setKeystorePath(jarKeystorePath); +String jarTruststorePath = String.format("jar:file:%s/lib/my_rtmps_client.jar!/rtmps_%s.p12", Paths.get(System.getProperty("user.dir"), "truststore"); +TLSFactory.setTruststorePath(jarTruststorePath); +``` + +## Testing + +Using ffplay to test playback, issue the following, but make sure to update the command for your server IP and stream name: `ffplay rtmps://localhost:8443/live/stream1` (this assumes a stream named `stream1` is being published already). + +### Useful System Properties + +* To enable SSL debugging, add the following system property to the JVM: `-Djavax.net.debug=SSL` +* To enable more detailed SSL debugging, add the following system property to the JVM: `-Djavax.net.debug=SSL,handshake,verbose,trustmanager,keymanager,record,plaintext` diff --git a/client/src/main/java/org/red5/client/StreamRelay.java b/client/src/main/java/org/red5/client/StreamRelay.java index 8aad0e497..7e50a6478 100644 --- a/client/src/main/java/org/red5/client/StreamRelay.java +++ b/client/src/main/java/org/red5/client/StreamRelay.java @@ -107,7 +107,7 @@ public void handleException(Throwable throwable) { e.printStackTrace(); } } while (!proxy.isPublished()); - System.out.println("Publishing.."); + System.out.println("Publishing..."); // create the consumer client = new RTMPClient(); diff --git a/client/src/main/java/org/red5/client/net/rtmps/RTMPSClient.java b/client/src/main/java/org/red5/client/net/rtmps/RTMPSClient.java index 06e3d002d..c2acaf235 100644 --- a/client/src/main/java/org/red5/client/net/rtmps/RTMPSClient.java +++ b/client/src/main/java/org/red5/client/net/rtmps/RTMPSClient.java @@ -7,7 +7,12 @@ package org.red5.client.net.rtmps; +import java.io.IOException; +import java.io.InputStream; import java.net.InetSocketAddress; +import java.net.URI; +import java.nio.file.Files; +import java.nio.file.Paths; import javax.net.ssl.SSLContext; @@ -16,9 +21,12 @@ import org.apache.mina.core.session.IoSession; import org.apache.mina.filter.ssl.SslFilter; import org.apache.mina.transport.socket.nio.NioSocketConnector; +import org.red5.client.net.rtmp.ClientExceptionHandler; import org.red5.client.net.rtmp.RTMPClient; import org.red5.client.net.rtmp.RTMPMinaIoHandler; -import org.red5.client.net.ssl.BogusSslContextFactory; +import org.red5.io.tls.TLSFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * RTMPS client object (RTMPS Native) @@ -35,31 +43,84 @@ */ public class RTMPSClient extends RTMPClient { + private static final Logger log = LoggerFactory.getLogger(RTMPSClient.class); + + private static String[] cipherSuites; + // I/O handler private final RTMPSClientIoHandler ioHandler; /** * Password for accessing the keystore. */ - @SuppressWarnings("unused") - private char[] password; + private char[] password = "password123".toCharArray(); + + /** + * Path to the keystore and truststore files. + */ + private InputStream keystoreStream, truststoreStream; /** * The keystore type, valid options are JKS and PKCS12 */ - @SuppressWarnings("unused") - private String keyStoreType = "JKS"; + private String keyStoreType = "PKCS12"; /** Constructs a new RTMPClient. */ public RTMPSClient() { protocol = "rtmps"; ioHandler = new RTMPSClientIoHandler(); ioHandler.setHandler(this); + setExceptionHandler(new ClientExceptionHandler() { + @Override + public void handleException(Throwable throwable) { + log.error("Exception", throwable); + try { + ioHandler.exceptionCaught(null, throwable); + } catch (Exception e) { + log.debug("Exception", e); + } + } + }); + } + + /** + * Creates a new RTMPSClient with the given keystore type and password. + * + * @param keyStoreType keystore type + * @param password keystore password + */ + public RTMPSClient(String keyStoreType, String password) { + protocol = "rtmps"; + this.keyStoreType = keyStoreType; + this.password = password.toCharArray(); + ioHandler = new RTMPSClientIoHandler(); + ioHandler.setHandler(this); + } + + /** + * Creates a new RTMPSClient with the given keystore type, password, and paths to store files. If the stores + * are inside a jar file, use the following format: jar:file:/path/to/your.jar!/path/to/file/in/jar + * + * @param keyStoreType keystore type + * @param password keystore password + * @param keystorePath path to keystore file + * @param truststorePath path to truststore file + * @throws IOException + */ + public RTMPSClient(String keyStoreType, String password, String keystorePath, String truststorePath) throws IOException { + protocol = "rtmps"; + this.keyStoreType = keyStoreType; + this.password = password.toCharArray(); + this.keystoreStream = Files.newInputStream(Paths.get(URI.create(keystorePath))); + this.truststoreStream = Files.newInputStream(Paths.get(URI.create(truststorePath))); + ioHandler = new RTMPSClientIoHandler(); + ioHandler.setHandler(this); } @SuppressWarnings({ "rawtypes" }) @Override protected void startConnector(String server, int port) { + log.debug("startConnector - server: {} port: {}", server, port); socketConnector = new NioSocketConnector(); socketConnector.setHandler(ioHandler); future = socketConnector.connect(new InetSocketAddress(server, port)); @@ -69,20 +130,17 @@ public void operationComplete(IoFuture future) { try { // will throw RuntimeException after connection error future.getSession(); - } catch (Throwable e) { - //if there isn't an ClientExceptionHandler set, a - //RuntimeException may be thrown in handleException - handleException(e); + } catch (Throwable t) { + try { + ioHandler.exceptionCaught(null, t); + } catch (Exception e) { + // no-op + } } } }); - // Do the close requesting that the pending messages are sent before - // the session is closed - //future.getSession().close(false); // Now wait for the close to be completed future.awaitUninterruptibly(CONNECTOR_WORKER_TIMEOUT); - // We can now dispose the connector - //socketConnector.dispose(); } /** @@ -103,31 +161,52 @@ public void setKeyStoreType(String keyStoreType) { this.keyStoreType = keyStoreType; } + public static void setCipherSuites(String[] cipherSuites) { + RTMPSClient.cipherSuites = cipherSuites; + } + private class RTMPSClientIoHandler extends RTMPMinaIoHandler { /** {@inheritDoc} */ @Override public void sessionOpened(IoSession session) throws Exception { - // START OF NATIVE SSL STUFF - SSLContext sslContext = BogusSslContextFactory.getInstance(false); - SslFilter sslFilter = new SslFilter(sslContext); - sslFilter.setUseClientMode(true); + log.debug("RTMPS sessionOpened: {}", session); + // if we're using a input streams, pass them to the ctor + SSLContext context = null; + if (keystoreStream != null && truststoreStream != null) { + context = TLSFactory.getTLSContext(keyStoreType, password, keystoreStream, password, truststoreStream); + } else { + context = TLSFactory.getTLSContext(keyStoreType, password); + } + SslFilter sslFilter = new SslFilter(context); if (sslFilter != null) { + // we are a client + sslFilter.setUseClientMode(true); + // set the cipher suites + if (cipherSuites != null) { + sslFilter.setEnabledCipherSuites(cipherSuites); + } session.getFilterChain().addFirst("sslFilter", sslFilter); } - // END OF NATIVE SSL STUFF super.sessionOpened(session); } + @Override + public void sessionClosed(IoSession session) throws Exception { + log.debug("RTMPS sessionClosed: {}", session); + super.sessionClosed(session); + } + /** {@inheritDoc} */ @Override public void exceptionCaught(IoSession session, Throwable cause) throws Exception { - log.warn("Exception caught {}", cause.getMessage()); - if (log.isDebugEnabled()) { - log.error("Exception detail", cause); + log.warn("Exception caught: {}", cause.getMessage()); + log.debug("Exception detail", cause); + // if there are any errors using ssl, kill the session + if (session != null) { + session.closeNow(); } - //if there are any errors using ssl, kill the session - session.closeNow(); + socketConnector.dispose(false); } } diff --git a/client/src/main/java/org/red5/client/net/rtmps/RTMPTSClient.java b/client/src/main/java/org/red5/client/net/rtmps/RTMPTSClient.java index c059cb04c..037ce0cbe 100644 --- a/client/src/main/java/org/red5/client/net/rtmps/RTMPTSClient.java +++ b/client/src/main/java/org/red5/client/net/rtmps/RTMPTSClient.java @@ -9,6 +9,8 @@ import org.red5.client.net.rtmpt.RTMPTClient; import org.red5.server.net.rtmpt.codec.RTMPTCodecFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * RTMPT/S client object @@ -17,6 +19,8 @@ */ public class RTMPTSClient extends RTMPTClient { + private static final Logger log = LoggerFactory.getLogger(RTMPTSClient.class); + public RTMPTSClient() { protocol = "rtmps"; codecFactory = new RTMPTCodecFactory(); diff --git a/client/src/main/java/org/red5/client/net/ssl/BogusSslContextFactory.java b/client/src/main/java/org/red5/client/net/ssl/BogusSslContextFactory.java deleted file mode 100644 index 321db222f..000000000 --- a/client/src/main/java/org/red5/client/net/ssl/BogusSslContextFactory.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.red5.client.net.ssl; - -import java.io.IOException; -import java.io.InputStream; -import java.security.GeneralSecurityException; -import java.security.KeyStore; -import java.security.Security; -import javax.net.ssl.KeyManagerFactory; -import javax.net.ssl.SSLContext; - -/** - * Factory to create a bougus SSLContext. - * - * @author The Apache MINA Project (dev@mina.apache.org) - */ -public class BogusSslContextFactory { - - /** - * Protocol to use. - */ - private static final String PROTOCOL = "TLS"; - - private static final String KEY_MANAGER_FACTORY_ALGORITHM; - - static { - String algorithm = Security.getProperty("ssl.KeyManagerFactory.algorithm"); - if (algorithm == null) { - algorithm = "SunX509"; - } - KEY_MANAGER_FACTORY_ALGORITHM = algorithm; - } - - /** - * Bougus Server certificate keystore file name. - */ - private static final String BOGUS_KEYSTORE = "bogus.cert"; - - // NOTE: The keystore was generated using keytool: - // keytool -genkey -alias bogus -keysize 512 -validity 3650 -keyalg RSA -dname "CN=bogus.com, OU=XXX CA, O=Bogus Inc, L=Stockholm, S=Stockholm, C=SE" -keypass boguspw -storepass boguspw -keystore bogus.cert - /** - * Bougus keystore password. - */ - private static final char[] BOGUS_PW = { 'b', 'o', 'g', 'u', 's', 'p', 'w' }; - - private static SSLContext serverInstance = null; - - private static SSLContext clientInstance = null; - - /** - * Get SSLContext singleton. - * - * @param server whether server or client - * @return SSLContext - * @throws java.security.GeneralSecurityException thrown - */ - public static SSLContext getInstance(boolean server) throws GeneralSecurityException { - SSLContext retInstance = null; - if (server) { - if (serverInstance == null) { - synchronized (BogusSslContextFactory.class) { - if (serverInstance == null) { - try { - serverInstance = createBougusServerSslContext(); - } catch (Exception ioe) { - throw new GeneralSecurityException("Can't create Server SSLContext:" + ioe); - } - } - } - } - retInstance = serverInstance; - } else { - if (clientInstance == null) { - synchronized (BogusSslContextFactory.class) { - if (clientInstance == null) { - clientInstance = createBougusClientSslContext(); - } - } - } - retInstance = clientInstance; - } - return retInstance; - } - - private static SSLContext createBougusServerSslContext() throws GeneralSecurityException, IOException { - // Create keystore - KeyStore ks = KeyStore.getInstance("JKS"); - InputStream in = null; - try { - in = BogusSslContextFactory.class.getResourceAsStream(BOGUS_KEYSTORE); - ks.load(in, BOGUS_PW); - } finally { - if (in != null) { - try { - in.close(); - } catch (IOException ignored) { - } - } - } - // Set up key manager factory to use our key store - KeyManagerFactory kmf = KeyManagerFactory.getInstance(KEY_MANAGER_FACTORY_ALGORITHM); - kmf.init(ks, BOGUS_PW); - // Initialize the SSLContext to work with our key managers. - SSLContext sslContext = SSLContext.getInstance(PROTOCOL); - sslContext.init(kmf.getKeyManagers(), BogusTrustManagerFactory.X509_MANAGERS, null); - return sslContext; - } - - private static SSLContext createBougusClientSslContext() throws GeneralSecurityException { - SSLContext context = SSLContext.getInstance(PROTOCOL); - context.init(null, BogusTrustManagerFactory.X509_MANAGERS, null); - return context; - } -} \ No newline at end of file diff --git a/client/src/main/java/org/red5/client/net/ssl/BogusTrustManagerFactory.java b/client/src/main/java/org/red5/client/net/ssl/BogusTrustManagerFactory.java deleted file mode 100644 index 5bd351365..000000000 --- a/client/src/main/java/org/red5/client/net/ssl/BogusTrustManagerFactory.java +++ /dev/null @@ -1,55 +0,0 @@ -package org.red5.client.net.ssl; - -import java.security.InvalidAlgorithmParameterException; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import javax.net.ssl.ManagerFactoryParameters; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactorySpi; -import javax.net.ssl.X509TrustManager; - -/** - * Bogus trust manager factory. Creates BogusX509TrustManager - * - * @author The Apache MINA Project (dev@mina.apache.org) - */ -class BogusTrustManagerFactory extends TrustManagerFactorySpi { - - static final X509TrustManager X509 = new X509TrustManager() { - @Override - public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - } - - @Override - public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException { - } - - @Override - public X509Certificate[] getAcceptedIssuers() { - return new X509Certificate[0]; - } - }; - - static final TrustManager[] X509_MANAGERS = new TrustManager[] { X509 }; - - public BogusTrustManagerFactory() { - } - - @Override - protected TrustManager[] engineGetTrustManagers() { - return X509_MANAGERS; - } - - @Override - protected void engineInit(KeyStore keystore) throws KeyStoreException { - // noop - } - - @Override - protected void engineInit(ManagerFactoryParameters managerFactoryParameters) throws InvalidAlgorithmParameterException { - // noop - } - -} \ No newline at end of file diff --git a/client/src/main/java/org/red5/client/net/ssl/SslServerSocketFactory.java b/client/src/main/java/org/red5/client/net/ssl/SslServerSocketFactory.java deleted file mode 100644 index c889c1af2..000000000 --- a/client/src/main/java/org/red5/client/net/ssl/SslServerSocketFactory.java +++ /dev/null @@ -1,68 +0,0 @@ -package org.red5.client.net.ssl; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.ServerSocket; -import java.security.GeneralSecurityException; -import javax.net.ServerSocketFactory; - -/** - * Simple Server Socket factory to create sockets with or without SSL enabled. If SSL enabled a "bogus" SSL Context is used (suitable for - * test purposes) - * - * @author The Apache MINA Project (dev@mina.apache.org) - */ -public class SslServerSocketFactory extends javax.net.ServerSocketFactory { - private static boolean sslEnabled = false; - - private static javax.net.ServerSocketFactory sslFactory = null; - - private static ServerSocketFactory factory = null; - - public SslServerSocketFactory() { - super(); - } - - @Override - public ServerSocket createServerSocket(int port) throws IOException { - return new ServerSocket(port); - } - - @Override - public ServerSocket createServerSocket(int port, int backlog) throws IOException { - return new ServerSocket(port, backlog); - } - - @Override - public ServerSocket createServerSocket(int port, int backlog, InetAddress ifAddress) throws IOException { - return new ServerSocket(port, backlog, ifAddress); - } - - public static javax.net.ServerSocketFactory getServerSocketFactory() throws IOException { - if (isSslEnabled()) { - if (sslFactory == null) { - try { - sslFactory = BogusSslContextFactory.getInstance(true).getServerSocketFactory(); - } catch (GeneralSecurityException e) { - IOException ioe = new IOException("could not create SSL socket"); - ioe.initCause(e); - throw ioe; - } - } - return sslFactory; - } else { - if (factory == null) { - factory = new SslServerSocketFactory(); - } - return factory; - } - } - - public static boolean isSslEnabled() { - return sslEnabled; - } - - public static void setSslEnabled(boolean newSslEnabled) { - sslEnabled = newSslEnabled; - } -} diff --git a/client/src/main/java/org/red5/client/net/ssl/SslSocketFactory.java b/client/src/main/java/org/red5/client/net/ssl/SslSocketFactory.java deleted file mode 100644 index cf54aa823..000000000 --- a/client/src/main/java/org/red5/client/net/ssl/SslSocketFactory.java +++ /dev/null @@ -1,89 +0,0 @@ -package org.red5.client.net.ssl; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.Socket; -import java.net.UnknownHostException; -import java.security.GeneralSecurityException; -import javax.net.SocketFactory; - -/** - * Simple Socket factory to create sockets with or without SSL enabled. If SSL enabled a "bogus" SSL Context is used (suitable for test - * purposes) - * - * @author The Apache MINA Project (dev@mina.apache.org) - */ -public class SslSocketFactory extends SocketFactory { - - private static boolean sslEnabled = false; - - private static javax.net.ssl.SSLSocketFactory sslFactory = null; - - private static javax.net.SocketFactory factory = null; - - public SslSocketFactory() { - super(); - } - - @Override - public Socket createSocket(String arg1, int arg2) throws IOException, UnknownHostException { - if (isSslEnabled()) { - return getSSLFactory().createSocket(arg1, arg2); - } else { - return new Socket(arg1, arg2); - } - } - - @Override - public Socket createSocket(String arg1, int arg2, InetAddress arg3, int arg4) throws IOException, UnknownHostException { - if (isSslEnabled()) { - return getSSLFactory().createSocket(arg1, arg2, arg3, arg4); - } else { - return new Socket(arg1, arg2, arg3, arg4); - } - } - - @Override - public Socket createSocket(InetAddress arg1, int arg2) throws IOException { - if (isSslEnabled()) { - return getSSLFactory().createSocket(arg1, arg2); - } else { - return new Socket(arg1, arg2); - } - } - - @Override - public Socket createSocket(InetAddress arg1, int arg2, InetAddress arg3, int arg4) throws IOException { - if (isSslEnabled()) { - return getSSLFactory().createSocket(arg1, arg2, arg3, arg4); - } else { - return new Socket(arg1, arg2, arg3, arg4); - } - } - - public static javax.net.SocketFactory getSocketFactory() { - if (factory == null) { - factory = new SslSocketFactory(); - } - return factory; - } - - private static javax.net.ssl.SSLSocketFactory getSSLFactory() { - if (sslFactory == null) { - try { - sslFactory = BogusSslContextFactory.getInstance(false).getSocketFactory(); - } catch (GeneralSecurityException e) { - throw new RuntimeException("could not create SSL socket", e); - } - } - return sslFactory; - } - - public static boolean isSslEnabled() { - return sslEnabled; - } - - public static void setSslEnabled(boolean newSslEnabled) { - sslEnabled = newSslEnabled; - } -} diff --git a/client/src/test/resources/bogus.cert b/client/src/test/resources/bogus.cert deleted file mode 100644 index 4a7927f55ee10183c79f3debf56840b565b1061b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 979 zcmezO_TO6u1_mY|W&~r_r2O>KVjwS+``;^lAU(sNiE)Yn9~+l88zT#&7Ly<&BP#<- z6JtZbJm$cROS@M_1u)-kD9d(IwXl!zb=B53dbD8m?{iO1Rn61v+7h-X?aQ;gXP+7^ zkN-KoWYde)Pd3@w>4ru>H;UGOkgy_a^7RY;Uu&MJ2}T~7(WmkG(DJEU<}!Z#Z(F`; zijwApo@4%YDP~n_3IDjxTsie?#>^$(U-0Q>-+guN>dEpWElS=mwtau`XY<`ibDoua zF+TPFev;t_HS@VIF1uMIVkqh%ZaGIj?mc__zm@H*E#W_0qLdo{lwNa@dUnx`^Xbjw zmqPf2t4oe@mt9mgU0z`^Lu|i**d(johgGZA?%tAGTrT|ox>4e)ujL&c&&v)k_nyo0 zGG4b~Yrp2JFRDV)Or|MoSAXpO>fs;9B^WHKKD#mhuCQUf*Mq4Xp+8GE`#s{C{5Rml zeeNJliR7|37L1S1ajpAWT+*eaW)*+4qHvMoX331Qa}xQi(Y@z2qdxO8slK^6X+nvJ z*4B{6?y|Z%^%MNpd^Kv;3*(UQTikK&S#8mdUyu5Pmf8h#G<$ALu6zxNt_VF-1502u zeFa9-MNgzUN%mxHjlRNyo`+8tPBQuhTI06Y|No7Y{E>zu7*Me z0w4~DFlTT{esXq3eoih-j2(*@moTRjC~YWs<|P~Q1I-5Ndc7$nU7Ul#e zAid=LTmw0AUPChjGXq0IBNI~t<0x@nBQpb2V*@Cc?(T^}4iI1}X7*+<@L+Oe*t1U| zV7_tm^o{+RLJfC+7?pmO;|OxScUdiD2G^$KGeyD%Oq#Rawaxe&srq-v%q`HspvdyEh_Q&YPVTy|+Hi7H_}|^0f)(u>D)?Ls zk*x>D2eTtXWR>{Io%*ZhWS8ANvhhz?lV`8Us`p$6vx90ww(obJd+$@%k}W#R#7~?% xw4Xh&y0T`&GLMz2Pq$^QRXfEcv#f2`2dnQgZ8-s5nojLpUSfZncYB@=1OOX@VlMyy diff --git a/client/src/test/resources/logback-test.xml b/client/src/test/resources/logback-test.xml deleted file mode 100644 index 31b10fe8e..000000000 --- a/client/src/test/resources/logback-test.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - - %d{ISO8601} [%p] %logger - %msg%n - - - - target/red5-client.log - false - - %d{ISO8601} [%thread] %-5level %logger{16} - %msg%n - - - - - - - - - - - - - - - - diff --git a/common/src/main/java/org/red5/logging/LoggingContextSelector.java b/common/src/main/java/org/red5/logging/LoggingContextSelector.java index 7b2beca62..26434afc8 100644 --- a/common/src/main/java/org/red5/logging/LoggingContextSelector.java +++ b/common/src/main/java/org/red5/logging/LoggingContextSelector.java @@ -125,7 +125,6 @@ public LoggerContext getLoggerContext(String contextName) { configurator.doConfigure(url); context.start(); } catch (JoranException e) { - // } } else { if (Red5LoggerFactory.DEBUG) { @@ -164,7 +163,6 @@ public LoggerContext getLoggerContext(String contextName, URL url) { configurator.doConfigure(url); context.start(); } catch (JoranException e) { - // } } } catch (InterruptedException e) { diff --git a/common/src/main/java/org/red5/server/scheduling/JDKSchedulingService.java b/common/src/main/java/org/red5/server/scheduling/JDKSchedulingService.java index 4b9c7703d..c15a1f061 100644 --- a/common/src/main/java/org/red5/server/scheduling/JDKSchedulingService.java +++ b/common/src/main/java/org/red5/server/scheduling/JDKSchedulingService.java @@ -59,7 +59,7 @@ public class JDKSchedulingService implements ISchedulingService, JDKSchedulingSe /** Constructs a new JDKSchedulingService. */ public void afterPropertiesSet() throws Exception { - log.debug("Initializing.."); + log.debug("Initializing..."); scheduler = Executors.newScheduledThreadPool(threadCount); } @@ -191,7 +191,7 @@ public void removeScheduledJob(String name) { public void destroy() throws Exception { if (scheduler != null) { - log.debug("Destroying.."); + log.debug("Destroying..."); scheduler.shutdownNow(); } keyMap.clear(); diff --git a/common/src/main/java/org/red5/server/scope/Scope.java b/common/src/main/java/org/red5/server/scope/Scope.java index 3a8dd27be..03954b874 100644 --- a/common/src/main/java/org/red5/server/scope/Scope.java +++ b/common/src/main/java/org/red5/server/scope/Scope.java @@ -1252,8 +1252,7 @@ public void dump() { } names = null; } - log.trace("Handler: {}", handler); - log.trace("Child count: {}", children.size()); + log.trace("Handler: {} child count: {}", handler, children.size()); children.forEach(child -> { log.trace("Child: {}", child); }); @@ -1373,23 +1372,25 @@ public boolean add(IBasicScope scope) { added = super.add(scope); if (added) { subscopeStats.increment(); - if (scope instanceof Scope) { - // start the scope - if (((Scope) scope).start()) { - log.debug("Child scope started"); - } else { - log.trace("Failed to start child scope: {} in {}", scope, this); - } - } } else { - log.debug("Scope was not added"); + log.debug("Subscope was not added"); } } else { - log.debug("Scope already exists"); + log.debug("Subscope already exists"); } } catch (Exception e) { log.warn("Exception on add", e); } + if (added && scope instanceof Scope) { + // cast it + Scope scp = (Scope) scope; + // start the scope + if (scp.start()) { + log.debug("Child scope started"); + } else { + log.debug("Failed to start child scope: {} in {}", scope, this); + } + } } return added; } diff --git a/io/src/main/java/org/red5/io/tls/TLSFactory.java b/io/src/main/java/org/red5/io/tls/TLSFactory.java new file mode 100644 index 000000000..6ce2fb2a8 --- /dev/null +++ b/io/src/main/java/org/red5/io/tls/TLSFactory.java @@ -0,0 +1,281 @@ +package org.red5.io.tls; + +import java.io.FileInputStream; +import java.io.InputStream; +import java.nio.file.Paths; +import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.SecureRandom; +import java.security.Security; +import java.security.UnrecoverableKeyException; + +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.TrustManagerFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class TLSFactory { + + private static final Logger log = LoggerFactory.getLogger(TLSFactory.class); + + private static final boolean isDebug = log.isDebugEnabled(), isTrace = log.isTraceEnabled(); + + // shared thread-safe random + private static final SecureRandom RANDOM = new SecureRandom(); + + public static final int MAX_HANDSHAKE_LOOPS = 200; + + public static final int MAX_APP_READ_LOOPS = 60; + + public static final int SOCKET_TIMEOUT = Integer.getInteger("socket.timeout", 3 * 1000); // in millis + + public static final int BUFFER_SIZE = 1024 * 4; // 4KB enough? + + public static final int MAXIMUM_PACKET_SIZE = 1180; // use this for PMTU for now + + // protocol version + public static final String PROTOCOL_VERSION = "TLSv1.2"; + + /* + * The following is to set up the keystores. + */ + private static String storeType = "PKCS12"; // JKS or PKCS12 + + private static String keyStoreFile = String.format("server.%s", "PKCS12".equals(storeType) ? "p12" : "jks"), trustStoreFile = String.format("truststore.%s", "PKCS12".equals(storeType) ? "p12" : "jks"); + + private static String keystorePath = Paths.get(System.getProperty("user.dir"), "conf", keyStoreFile).toString(), truststorePath = Paths.get(System.getProperty("user.dir"), "conf", trustStoreFile).toString(); + + private static String passwd = "password123"; + + static { + if (isDebug) { + if (isTrace) { + System.setProperty("javax.net.debug", "SSL,handshake,verbose,trustmanager,keymanager,record,plaintext"); + } else { + System.setProperty("javax.net.debug", "all"); + } + } + // set unlimited crypto policy + Security.setProperty("crypto.policy", "unlimited"); + // set extensions + System.setProperty("jdk.tls.useExtendedMasterSecret", "true"); // https://bugs.openjdk.org/browse/JDK-8192045 not for DTLS 1.3 + // allows rejection if session hash and master secret are not supported + System.setProperty("jdk.tls.allowLegacyMasterSecret", "false"); + System.setProperty("jdk.tls.acknowledgeCloseNotify", "true"); + // https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/JSSERefGuide.html + // https://docs.oracle.com/javase/8/docs/technotes/guides/security/jsse/ReadDebug.html + // check max key size + int maxKeySize; + try { + maxKeySize = javax.crypto.Cipher.getMaxAllowedKeyLength("AES"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to get max key size for AES", e); + maxKeySize = 128; + } + log.info("Max key size for AES: {}", (maxKeySize == Integer.MAX_VALUE ? "unlimited" : maxKeySize)); + } + + /** + * Returns an SSLContext for the configured keystore and truststore with the default password. + * + * @return SSLContext + * @throws Exception + */ + public static SSLContext getTLSContext() throws Exception { + log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keystorePath, truststorePath, storeType); + KeyStore ks = KeyStore.getInstance(storeType); + KeyStore ts = KeyStore.getInstance(storeType); + char[] passphrase = passwd.toCharArray(); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + ks.load(fis, passphrase); + } catch (Exception e) { + log.error("Failed to load keystore: {}", keystorePath, e); + throw e; + } + try (FileInputStream fis = new FileInputStream(truststorePath)) { + ts.load(fis, passphrase); + } catch (Exception e) { + log.error("Failed to load truststore: {}", truststorePath, e); + throw e; + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + try { + kmf.init(ks, passphrase); + } catch (UnrecoverableKeyException e) { + log.error("Failed to initialize KeyManagerFactory with keystore: {}", keystorePath, e); + throw e; + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ts); + SSLContext sslCtx = SSLContext.getInstance(PROTOCOL_VERSION); + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), RANDOM); + return sslCtx; + } + + /** + * Returns an SSLContext for the configured keystore and truststore with the provided password. + * + * @param passphrase + * @return SSLContext + * @throws Exception + */ + public static SSLContext getTLSContext(String storeType, char[] passphrase) throws Exception { + log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keystorePath, truststorePath, storeType); + log.debug("Keystore - file: {} password: {}", keystorePath, passphrase); + log.debug("Truststore - file: {} password: {}", truststorePath, passphrase); + KeyStore ks = KeyStore.getInstance(storeType); + KeyStore ts = KeyStore.getInstance(storeType); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + ks.load(fis, passphrase); + } catch (Exception e) { + log.error("Failed to load keystore: {}", keystorePath, e); + throw e; + } + try (FileInputStream fis = new FileInputStream(truststorePath)) { + ts.load(fis, passphrase); + } catch (Exception e) { + log.error("Failed to load truststore: {}", truststorePath, e); + throw e; + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + try { + PrivateKey privateKey = (PrivateKey) ks.getKey("privatekey", passphrase); + log.debug("Private key: {}", privateKey); + kmf.init(ks, passphrase); + } catch (UnrecoverableKeyException e) { + log.error("Failed to initialize KeyManagerFactory with keystore: {}", keystorePath, e); + throw e; + } + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ts); + SSLContext sslCtx = SSLContext.getInstance(PROTOCOL_VERSION); + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), RANDOM); + return sslCtx; + } + + /** + * Returns an SSLContext for the provided keystore, truststore, and password. + * + * @param storeType + * @param keystorePassword + * @param keystorePath + * @param truststorePassword + * @param truststorePath + * @return SSLContext + * @throws Exception + */ + public static SSLContext getTLSContext(String storeType, String keystorePassword, String keystorePath, String truststorePassword, String truststorePath) throws Exception { + log.info("Creating SSL context with keystore: {} and truststore: {} using {}", keystorePath, truststorePath, storeType); + log.debug("Keystore - file: {} password: {}", keystorePath, keystorePassword); + log.debug("Truststore - file: {} password: {}", truststorePath, truststorePassword); + KeyStore ks = KeyStore.getInstance(storeType); + KeyStore ts = KeyStore.getInstance(storeType); + char[] keyStrorePassphrase = keystorePassword.toCharArray(); + char[] trustStorePassphrase = truststorePassword.toCharArray(); + try (FileInputStream fis = new FileInputStream(keystorePath)) { + ks.load(fis, keyStrorePassphrase); + } + try (FileInputStream fis = new FileInputStream(truststorePath)) { + ts.load(fis, trustStorePassphrase); + } + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, keyStrorePassphrase); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ts); + SSLContext sslCtx = SSLContext.getInstance(PROTOCOL_VERSION); + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), RANDOM); + return sslCtx; + } + + /** + * Returns an SSLContext for the provided keystore and truststore input streams. + * + * @param storeType + * @param keyStrorePassphrase + * @param keystoreInput + * @param trustStorePassphrase + * @param truststoreInput + * @return SSLContext + * @throws Exception + */ + public static SSLContext getTLSContext(String storeType, char[] keyStrorePassphrase, InputStream keystoreInput, char[] trustStorePassphrase, InputStream truststoreInput) throws Exception { + log.info("Creating SSL context with keystore and truststore input streams, using {}", storeType); + log.debug("Keystore - passphrase: {}", keyStrorePassphrase); + log.debug("Truststore - passphrase: {}", trustStorePassphrase); + KeyStore ks = KeyStore.getInstance(storeType); + KeyStore ts = KeyStore.getInstance(storeType); + ks.load(keystoreInput, keyStrorePassphrase); + ts.load(truststoreInput, trustStorePassphrase); + KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509"); + kmf.init(ks, keyStrorePassphrase); + TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); + tmf.init(ts); + SSLContext sslCtx = SSLContext.getInstance(PROTOCOL_VERSION); + sslCtx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), RANDOM); + return sslCtx; + } + + public static SSLEngine createSSLEngine(boolean isClient) throws Exception { + SSLContext context = getTLSContext(); + SSLEngine engine = context.createSSLEngine(); + SSLParameters paras = engine.getSSLParameters(); + paras.setMaximumPacketSize(MAXIMUM_PACKET_SIZE); + engine.setUseClientMode(isClient); + engine.setSSLParameters(paras); + return engine; + } + + public static String getStoreType() { + return storeType; + } + + public static void setStoreType(String storeType) { + TLSFactory.storeType = storeType; + } + + public static String getKeyStoreFile() { + return keyStoreFile; + } + + public static void setKeyStoreFile(String keyStoreFile) { + TLSFactory.keyStoreFile = keyStoreFile; + } + + public static String getTrustStoreFile() { + return trustStoreFile; + } + + public static void setTrustStoreFile(String trustStoreFile) { + TLSFactory.trustStoreFile = trustStoreFile; + } + + public static String getPasswd() { + return passwd; + } + + public static void setPasswd(String passwd) { + TLSFactory.passwd = passwd; + } + + public static String getKeystorePath() { + return keystorePath; + } + + public static void setKeystorePath(String keystorePath) { + TLSFactory.keystorePath = keystorePath; + } + + public static String getTruststorePath() { + return truststorePath; + } + + public static void setTruststorePath(String truststorePath) { + TLSFactory.truststorePath = truststorePath; + } + +} \ No newline at end of file diff --git a/io/src/main/java/org/red5/io/utils/TlsUtils.java b/io/src/main/java/org/red5/io/utils/TlsUtils.java new file mode 100644 index 000000000..ab7c81875 --- /dev/null +++ b/io/src/main/java/org/red5/io/utils/TlsUtils.java @@ -0,0 +1,885 @@ +package org.red5.io.utils; + +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Hashtable; +import java.util.Vector; + +import org.bouncycastle.asn1.ASN1Encoding; +import org.bouncycastle.asn1.ASN1InputStream; +import org.bouncycastle.asn1.ASN1Object; +import org.bouncycastle.asn1.ASN1Primitive; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.Integers; +import org.bouncycastle.util.encoders.Hex; +import org.bouncycastle.util.io.Streams; + +/** + * Some helper functions for the TLS API. + */ +@SuppressWarnings({ "rawtypes", "unchecked" }) +public class TlsUtils { + + @SuppressWarnings("unused") + private static byte[] DOWNGRADE_TLS11 = Hex.decodeStrict("444F574E47524400"); + + @SuppressWarnings("unused") + private static byte[] DOWNGRADE_TLS12 = Hex.decodeStrict("444F574E47524401"); + + public static final byte[] EMPTY_BYTES = new byte[0]; + + public static final short[] EMPTY_SHORTS = new short[0]; + + public static final int[] EMPTY_INTS = new int[0]; + + public static final long[] EMPTY_LONGS = new long[0]; + + public static final String[] EMPTY_STRINGS = new String[0]; + + public static boolean isValidUint8(short i) { + return (i & 0xFF) == i; + } + + public static boolean isValidUint8(int i) { + return (i & 0xFF) == i; + } + + public static boolean isValidUint8(long i) { + return (i & 0xFFL) == i; + } + + public static boolean isValidUint16(int i) { + return (i & 0xFFFF) == i; + } + + public static boolean isValidUint16(long i) { + return (i & 0xFFFFL) == i; + } + + public static boolean isValidUint24(int i) { + return (i & 0xFFFFFF) == i; + } + + public static boolean isValidUint24(long i) { + return (i & 0xFFFFFFL) == i; + } + + public static boolean isValidUint32(long i) { + return (i & 0xFFFFFFFFL) == i; + } + + public static boolean isValidUint48(long i) { + return (i & 0xFFFFFFFFFFFFL) == i; + } + + public static boolean isValidUint64(long i) { + return true; + } + + public static void writeUint8(short i, OutputStream output) throws IOException { + output.write(i); + } + + public static void writeUint8(int i, OutputStream output) throws IOException { + output.write(i); + } + + public static void writeUint8(short i, byte[] buf, int offset) { + buf[offset] = (byte) i; + } + + public static void writeUint8(int i, byte[] buf, int offset) { + buf[offset] = (byte) i; + } + + public static void writeUint16(int i, OutputStream output) throws IOException { + output.write(i >>> 8); + output.write(i); + } + + public static void writeUint16(int i, byte[] buf, int offset) { + buf[offset] = (byte) (i >>> 8); + buf[offset + 1] = (byte) i; + } + + public static void writeUint24(int i, OutputStream output) throws IOException { + output.write((byte) (i >>> 16)); + output.write((byte) (i >>> 8)); + output.write((byte) i); + } + + public static void writeUint24(int i, byte[] buf, int offset) { + buf[offset] = (byte) (i >>> 16); + buf[offset + 1] = (byte) (i >>> 8); + buf[offset + 2] = (byte) i; + } + + public static void writeUint32(long i, OutputStream output) throws IOException { + output.write((byte) (i >>> 24)); + output.write((byte) (i >>> 16)); + output.write((byte) (i >>> 8)); + output.write((byte) i); + } + + public static void writeUint32(long i, byte[] buf, int offset) { + buf[offset] = (byte) (i >>> 24); + buf[offset + 1] = (byte) (i >>> 16); + buf[offset + 2] = (byte) (i >>> 8); + buf[offset + 3] = (byte) i; + } + + public static void writeUint48(long i, OutputStream output) throws IOException { + output.write((byte) (i >>> 40)); + output.write((byte) (i >>> 32)); + output.write((byte) (i >>> 24)); + output.write((byte) (i >>> 16)); + output.write((byte) (i >>> 8)); + output.write((byte) i); + } + + public static void writeUint48(long i, byte[] buf, int offset) { + buf[offset] = (byte) (i >>> 40); + buf[offset + 1] = (byte) (i >>> 32); + buf[offset + 2] = (byte) (i >>> 24); + buf[offset + 3] = (byte) (i >>> 16); + buf[offset + 4] = (byte) (i >>> 8); + buf[offset + 5] = (byte) i; + } + + public static void writeUint64(long i, OutputStream output) throws IOException { + output.write((byte) (i >>> 56)); + output.write((byte) (i >>> 48)); + output.write((byte) (i >>> 40)); + output.write((byte) (i >>> 32)); + output.write((byte) (i >>> 24)); + output.write((byte) (i >>> 16)); + output.write((byte) (i >>> 8)); + output.write((byte) i); + } + + public static void writeUint64(long i, byte[] buf, int offset) { + buf[offset] = (byte) (i >>> 56); + buf[offset + 1] = (byte) (i >>> 48); + buf[offset + 2] = (byte) (i >>> 40); + buf[offset + 3] = (byte) (i >>> 32); + buf[offset + 4] = (byte) (i >>> 24); + buf[offset + 5] = (byte) (i >>> 16); + buf[offset + 6] = (byte) (i >>> 8); + buf[offset + 7] = (byte) i; + } + + public static void writeOpaque8(byte[] buf, OutputStream output) throws IOException { + writeUint8(buf.length, output); + output.write(buf); + } + + public static void writeOpaque8(byte[] data, byte[] buf, int off) throws IOException { + writeUint8(data.length, buf, off); + System.arraycopy(data, 0, buf, off + 1, data.length); + } + + public static void writeOpaque16(byte[] buf, OutputStream output) throws IOException { + writeUint16(buf.length, output); + output.write(buf); + } + + public static void writeOpaque16(byte[] data, byte[] buf, int off) throws IOException { + writeUint16(data.length, buf, off); + System.arraycopy(data, 0, buf, off + 2, data.length); + } + + public static void writeOpaque24(byte[] buf, OutputStream output) throws IOException { + writeUint24(buf.length, output); + output.write(buf); + } + + public static void writeOpaque24(byte[] data, byte[] buf, int off) throws IOException { + writeUint24(data.length, buf, off); + System.arraycopy(data, 0, buf, off + 3, data.length); + } + + public static void writeUint8Array(short[] uints, OutputStream output) throws IOException { + for (int i = 0; i < uints.length; ++i) { + writeUint8(uints[i], output); + } + } + + public static void writeUint8Array(short[] uints, byte[] buf, int offset) throws IOException { + for (int i = 0; i < uints.length; ++i) { + writeUint8(uints[i], buf, offset); + ++offset; + } + } + + public static void writeUint8ArrayWithUint8Length(short[] uints, OutputStream output) throws IOException { + writeUint8(uints.length, output); + writeUint8Array(uints, output); + } + + public static void writeUint8ArrayWithUint8Length(short[] uints, byte[] buf, int offset) throws IOException { + writeUint8(uints.length, buf, offset); + writeUint8Array(uints, buf, offset + 1); + } + + public static void writeUint16Array(int[] uints, OutputStream output) throws IOException { + for (int i = 0; i < uints.length; ++i) { + writeUint16(uints[i], output); + } + } + + public static void writeUint16Array(int[] uints, byte[] buf, int offset) throws IOException { + for (int i = 0; i < uints.length; ++i) { + writeUint16(uints[i], buf, offset); + offset += 2; + } + } + + public static void writeUint16ArrayWithUint8Length(int[] uints, byte[] buf, int offset) throws IOException { + int length = 2 * uints.length; + writeUint8(length, buf, offset); + writeUint16Array(uints, buf, offset + 1); + } + + public static void writeUint16ArrayWithUint16Length(int[] uints, OutputStream output) throws IOException { + int length = 2 * uints.length; + writeUint16(length, output); + writeUint16Array(uints, output); + } + + public static void writeUint16ArrayWithUint16Length(int[] uints, byte[] buf, int offset) throws IOException { + int length = 2 * uints.length; + writeUint16(length, buf, offset); + writeUint16Array(uints, buf, offset + 2); + } + + public static byte[] decodeOpaque8(byte[] buf) throws IOException { + return decodeOpaque8(buf, 0); + } + + public static byte[] decodeOpaque8(byte[] buf, int minLength) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + if (buf.length < 1) { + throw new IOException("AlertDescription.decode_error"); + } + short length = readUint8(buf, 0); + if (buf.length != (length + 1) || length < minLength) { + throw new IOException("AlertDescription.decode_error"); + } + return copyOfRangeExact(buf, 1, buf.length); + } + + public static byte[] decodeOpaque16(byte[] buf) throws IOException { + return decodeOpaque16(buf, 0); + } + + public static byte[] decodeOpaque16(byte[] buf, int minLength) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + if (buf.length < 2) { + throw new IOException("AlertDescription.decode_error"); + } + int length = readUint16(buf, 0); + if (buf.length != (length + 2) || length < minLength) { + throw new IOException("AlertDescription.decode_error"); + } + return copyOfRangeExact(buf, 2, buf.length); + } + + public static short decodeUint8(byte[] buf) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + if (buf.length != 1) { + throw new IOException("AlertDescription.decode_error"); + } + return readUint8(buf, 0); + } + + public static short[] decodeUint8ArrayWithUint8Length(byte[] buf) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + if (buf.length < 1) { + throw new IOException("AlertDescription.decode_error"); + } + + int count = readUint8(buf, 0); + if (buf.length != (count + 1)) { + throw new IOException("AlertDescription.decode_error"); + } + + short[] uints = new short[count]; + for (int i = 0; i < count; ++i) { + uints[i] = readUint8(buf, i + 1); + } + return uints; + } + + public static int decodeUint16(byte[] buf) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + if (buf.length != 2) { + throw new IOException("AlertDescription.decode_error"); + } + return readUint16(buf, 0); + } + + public static int[] decodeUint16ArrayWithUint8Length(byte[] buf) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + + int length = readUint8(buf, 0); + if (buf.length != (length + 1) || (length & 1) != 0) { + throw new IOException("AlertDescription.decode_error"); + } + + int count = length / 2, pos = 1; + int[] uints = new int[count]; + for (int i = 0; i < count; ++i) { + uints[i] = readUint16(buf, pos); + pos += 2; + } + return uints; + } + + public static long decodeUint32(byte[] buf) throws IOException { + if (buf == null) { + throw new IllegalArgumentException("'buf' cannot be null"); + } + if (buf.length != 4) { + throw new IOException("AlertDescription.decode_error"); + } + return readUint32(buf, 0); + } + + public static byte[] encodeOpaque8(byte[] buf) throws IOException { + return Arrays.prepend(buf, (byte) buf.length); + } + + public static byte[] encodeOpaque16(byte[] buf) throws IOException { + byte[] r = new byte[2 + buf.length]; + writeUint16(buf.length, r, 0); + System.arraycopy(buf, 0, r, 2, buf.length); + return r; + } + + public static byte[] encodeOpaque24(byte[] buf) throws IOException { + byte[] r = new byte[3 + buf.length]; + writeUint24(buf.length, r, 0); + System.arraycopy(buf, 0, r, 3, buf.length); + return r; + } + + public static byte[] encodeUint8(short uint) throws IOException { + byte[] encoding = new byte[1]; + writeUint8(uint, encoding, 0); + return encoding; + } + + public static byte[] encodeUint8ArrayWithUint8Length(short[] uints) throws IOException { + byte[] result = new byte[1 + uints.length]; + writeUint8ArrayWithUint8Length(uints, result, 0); + return result; + } + + public static byte[] encodeUint16(int uint) throws IOException { + byte[] encoding = new byte[2]; + writeUint16(uint, encoding, 0); + return encoding; + } + + public static byte[] encodeUint16ArrayWithUint8Length(int[] uints) throws IOException { + int length = 2 * uints.length; + byte[] result = new byte[1 + length]; + writeUint16ArrayWithUint8Length(uints, result, 0); + return result; + } + + public static byte[] encodeUint16ArrayWithUint16Length(int[] uints) throws IOException { + int length = 2 * uints.length; + byte[] result = new byte[2 + length]; + writeUint16ArrayWithUint16Length(uints, result, 0); + return result; + } + + public static byte[] encodeUint24(int uint) throws IOException { + byte[] encoding = new byte[3]; + writeUint24(uint, encoding, 0); + return encoding; + } + + public static byte[] encodeUint32(long uint) throws IOException { + byte[] encoding = new byte[4]; + writeUint32(uint, encoding, 0); + return encoding; + } + + public static int readInt32(byte[] buf, int offset) { + int n = buf[offset] << 24; + n |= (buf[++offset] & 0xff) << 16; + n |= (buf[++offset] & 0xff) << 8; + n |= (buf[++offset] & 0xff); + return n; + } + + public static short readUint8(InputStream input) throws IOException { + int i = input.read(); + if (i < 0) { + throw new EOFException(); + } + return (short) i; + } + + public static short readUint8(byte[] buf, int offset) { + return (short) (buf[offset] & 0xff); + } + + public static int readUint16(InputStream input) throws IOException { + int i1 = input.read(); + int i2 = input.read(); + if (i2 < 0) { + throw new EOFException(); + } + return (i1 << 8) | i2; + } + + public static int readUint16(byte[] buf, int offset) { + int n = (buf[offset] & 0xff) << 8; + n |= (buf[++offset] & 0xff); + return n; + } + + public static int readUint24(InputStream input) throws IOException { + int i1 = input.read(); + int i2 = input.read(); + int i3 = input.read(); + if (i3 < 0) { + throw new EOFException(); + } + return (i1 << 16) | (i2 << 8) | i3; + } + + public static int readUint24(byte[] buf, int offset) { + int n = (buf[offset] & 0xff) << 16; + n |= (buf[++offset] & 0xff) << 8; + n |= (buf[++offset] & 0xff); + return n; + } + + public static long readUint32(InputStream input) throws IOException { + int i1 = input.read(); + int i2 = input.read(); + int i3 = input.read(); + int i4 = input.read(); + if (i4 < 0) { + throw new EOFException(); + } + return ((i1 << 24) | (i2 << 16) | (i3 << 8) | i4) & 0xFFFFFFFFL; + } + + public static long readUint32(byte[] buf, int offset) { + int n = (buf[offset] & 0xff) << 24; + n |= (buf[++offset] & 0xff) << 16; + n |= (buf[++offset] & 0xff) << 8; + n |= (buf[++offset] & 0xff); + return n & 0xFFFFFFFFL; + } + + public static long readUint48(InputStream input) throws IOException { + int hi = readUint24(input); + int lo = readUint24(input); + return ((long) (hi & 0xffffffffL) << 24) | (long) (lo & 0xffffffffL); + } + + public static long readUint48(byte[] buf, int offset) { + int hi = readUint24(buf, offset); + int lo = readUint24(buf, offset + 3); + return ((long) (hi & 0xffffffffL) << 24) | (long) (lo & 0xffffffffL); + } + + public static byte[] readAllOrNothing(int length, InputStream input) throws IOException { + if (length < 1) { + return EMPTY_BYTES; + } + byte[] buf = new byte[length]; + int read = Streams.readFully(input, buf); + if (read == 0) { + return null; + } + if (read != length) { + throw new EOFException(); + } + return buf; + } + + public static byte[] readFully(int length, InputStream input) throws IOException { + if (length < 1) { + return EMPTY_BYTES; + } + byte[] buf = new byte[length]; + if (length != Streams.readFully(input, buf)) { + throw new EOFException(); + } + return buf; + } + + public static void readFully(byte[] buf, InputStream input) throws IOException { + int length = buf.length; + if (length > 0 && length != Streams.readFully(input, buf)) { + throw new EOFException(); + } + } + + public static byte[] readOpaque8(InputStream input) throws IOException { + short length = readUint8(input); + return readFully(length, input); + } + + public static byte[] readOpaque8(InputStream input, int minLength) throws IOException { + short length = readUint8(input); + if (length < minLength) { + throw new IOException("AlertDescription.decode_error"); + } + return readFully(length, input); + } + + public static byte[] readOpaque8(InputStream input, int minLength, int maxLength) throws IOException { + short length = readUint8(input); + if (length < minLength || maxLength < length) { + throw new IOException("AlertDescription.decode_error"); + } + return readFully(length, input); + } + + public static byte[] readOpaque16(InputStream input) throws IOException { + int length = readUint16(input); + return readFully(length, input); + } + + public static byte[] readOpaque16(InputStream input, int minLength) throws IOException { + int length = readUint16(input); + if (length < minLength) { + throw new IOException("AlertDescription.decode_error"); + } + return readFully(length, input); + } + + public static byte[] readOpaque24(InputStream input) throws IOException { + int length = readUint24(input); + return readFully(length, input); + } + + public static byte[] readOpaque24(InputStream input, int minLength) throws IOException { + int length = readUint24(input); + if (length < minLength) { + throw new IOException("AlertDescription.decode_error"); + } + return readFully(length, input); + } + + public static short[] readUint8Array(int count, InputStream input) throws IOException { + short[] uints = new short[count]; + for (int i = 0; i < count; ++i) { + uints[i] = readUint8(input); + } + return uints; + } + + public static short[] readUint8ArrayWithUint8Length(InputStream input, int minLength) throws IOException { + int length = readUint8(input); + if (length < minLength) { + throw new IOException("AlertDescription.decode_error"); + } + + return readUint8Array(length, input); + } + + public static int[] readUint16Array(int count, InputStream input) throws IOException { + int[] uints = new int[count]; + for (int i = 0; i < count; ++i) { + uints[i] = readUint16(input); + } + return uints; + } + + public static ASN1Primitive readASN1Object(byte[] encoding) throws IOException { + ASN1InputStream asn1 = null; + try { + asn1 = new ASN1InputStream(encoding); + ASN1Primitive result = asn1.readObject(); + if (null == result) { + throw new IOException("AlertDescription.decode_error"); + } + if (null != asn1.readObject()) { + throw new IOException("AlertDescription.decode_error"); + } + return result; + } finally { + if (asn1 != null) { + asn1.close(); + } + } + } + + /** @deprecated Will be removed. Use readASN1Object in combination with requireDEREncoding instead */ + public static ASN1Primitive readDERObject(byte[] encoding) throws IOException { + /* + * NOTE: The current ASN.1 parsing code can't enforce DER-only parsing, but since DER is + * canonical, we can check it by re-encoding the result and comparing to the original. + */ + ASN1Primitive result = readASN1Object(encoding); + requireDEREncoding(result, encoding); + return result; + } + + public static void requireDEREncoding(ASN1Object asn1, byte[] encoding) throws IOException { + /* + * NOTE: The current ASN.1 parsing code can't enforce DER-only parsing, but since DER is + * canonical, we can check it by re-encoding the result and comparing to the original. + */ + byte[] check = asn1.getEncoded(ASN1Encoding.DER); + if (!Arrays.areEqual(check, encoding)) { + throw new IOException("AlertDescription.decode_error"); + } + } + + public static void writeGMTUnixTime(byte[] buf, int offset) { + int t = (int) (System.currentTimeMillis() / 1000L); + buf[offset] = (byte) (t >>> 24); + buf[offset + 1] = (byte) (t >>> 16); + buf[offset + 2] = (byte) (t >>> 8); + buf[offset + 3] = (byte) t; + } + + public static boolean addToSet(Vector s, int i) { + boolean result = !s.contains(Integers.valueOf(i)); + if (result) { + s.add(Integers.valueOf(i)); + } + return result; + } + + public static byte[] getExtensionData(Hashtable extensions, Integer extensionType) { + return extensions == null ? null : (byte[]) extensions.get(extensionType); + } + + public static boolean hasExpectedEmptyExtensionData(Hashtable extensions, Integer extensionType, short alertDescription) throws IOException { + byte[] extension_data = getExtensionData(extensions, extensionType); + if (extension_data == null) { + return false; + } + if (extension_data.length != 0) { + throw new IOException("" + alertDescription); + } + return true; + } + + public static boolean isNullOrContainsNull(Object[] array) { + if (null == array) { + return true; + } + int count = array.length; + for (int i = 0; i < count; ++i) { + if (null == array[i]) { + return true; + } + } + return false; + } + + public static boolean isNullOrEmpty(byte[] array) { + return null == array || array.length < 1; + } + + public static boolean isNullOrEmpty(short[] array) { + return null == array || array.length < 1; + } + + public static boolean isNullOrEmpty(int[] array) { + return null == array || array.length < 1; + } + + public static boolean isNullOrEmpty(Object[] array) { + return null == array || array.length < 1; + } + + public static boolean isNullOrEmpty(String s) { + return null == s || s.length() < 1; + } + + public static boolean isNullOrEmpty(Vector v) { + return null == v || v.isEmpty(); + } + + public static byte[] clone(byte[] data) { + return null == data ? (byte[]) null : data.length == 0 ? EMPTY_BYTES : (byte[]) data.clone(); + } + + public static String[] clone(String[] s) { + return null == s ? (String[]) null : s.length < 1 ? EMPTY_STRINGS : (String[]) s.clone(); + } + + public static boolean constantTimeAreEqual(int len, byte[] a, int aOff, byte[] b, int bOff) { + int d = 0; + for (int i = 0; i < len; ++i) { + d |= (a[aOff + i] ^ b[bOff + i]); + } + return 0 == d; + } + + public static byte[] copyOfRangeExact(byte[] original, int from, int to) { + int newLength = to - from; + byte[] copy = new byte[newLength]; + System.arraycopy(original, from, copy, 0, newLength); + return copy; + } + + static byte[] concat(byte[] a, byte[] b) { + byte[] c = new byte[a.length + b.length]; + System.arraycopy(a, 0, c, 0, a.length); + System.arraycopy(b, 0, c, a.length, b.length); + return c; + } + + @SuppressWarnings("unused") + private static byte[] getCertificateVerifyHeader(String contextString) { + int count = contextString.length(); + byte[] header = new byte[64 + count + 1]; + for (int i = 0; i < 64; ++i) { + header[i] = 0x20; + } + for (int i = 0; i < count; ++i) { + char c = contextString.charAt(i); + header[64 + i] = (byte) c; + } + header[64 + count] = 0x00; + return header; + } + + public static Vector vectorOfOne(Object obj) { + Vector v = new Vector(1); + v.addElement(obj); + return v; + } + + public static int[] getCommonCipherSuites(int[] peerCipherSuites, int[] localCipherSuites, boolean useLocalOrder) { + int[] ordered = peerCipherSuites, unordered = localCipherSuites; + if (useLocalOrder) { + ordered = localCipherSuites; + unordered = peerCipherSuites; + } + + int count = 0, limit = Math.min(ordered.length, unordered.length); + int[] candidates = new int[limit]; + for (int i = 0; i < ordered.length; ++i) { + int candidate = ordered[i]; + if (!contains(candidates, 0, count, candidate) && Arrays.contains(unordered, candidate)) { + candidates[count++] = candidate; + } + } + + if (count < limit) { + candidates = Arrays.copyOf(candidates, count); + } + + return candidates; + } + + static boolean contains(short[] buf, int off, int len, short value) { + for (int i = 0; i < len; ++i) { + if (value == buf[off + i]) { + return true; + } + } + return false; + } + + static boolean contains(int[] buf, int off, int len, int value) { + for (int i = 0; i < len; ++i) { + if (value == buf[off + i]) { + return true; + } + } + return false; + } + + static boolean containsAll(short[] container, short[] elements) { + for (int i = 0; i < elements.length; ++i) { + if (!Arrays.contains(container, elements[i])) { + return false; + } + } + return true; + } + + static boolean containsNot(short[] buf, int off, int len, short value) { + for (int i = 0; i < len; ++i) { + if (value != buf[off + i]) { + return true; + } + } + return false; + } + + static short[] retainAll(short[] retainer, short[] elements) { + short[] retained = new short[Math.min(retainer.length, elements.length)]; + + int count = 0; + for (int i = 0; i < elements.length; ++i) { + if (Arrays.contains(retainer, elements[i])) { + retained[count++] = elements[i]; + } + } + + return truncate(retained, count); + } + + static short[] truncate(short[] a, int n) { + if (n >= a.length) { + return a; + } + + short[] t = new short[n]; + System.arraycopy(a, 0, t, 0, n); + return t; + } + + static int[] truncate(int[] a, int n) { + if (n >= a.length) { + return a; + } + + int[] t = new int[n]; + System.arraycopy(a, 0, t, 0, n); + return t; + } + + public static boolean containsNonAscii(byte[] bs) { + for (int i = 0; i < bs.length; ++i) { + int c = bs[i] & 0xFF; + ; + if (c >= 0x80) { + return true; + } + } + return false; + } + + public static boolean containsNonAscii(String s) { + for (int i = 0; i < s.length(); ++i) { + int c = s.charAt(i); + if (c >= 0x80) { + return true; + } + } + return false; + } + +} diff --git a/server/src/main/java/org/red5/server/net/rtmps/RTMPSMinaIoHandler.java b/server/src/main/java/org/red5/server/net/rtmps/RTMPSMinaIoHandler.java index 08824535c..34f703665 100644 --- a/server/src/main/java/org/red5/server/net/rtmps/RTMPSMinaIoHandler.java +++ b/server/src/main/java/org/red5/server/net/rtmps/RTMPSMinaIoHandler.java @@ -7,9 +7,9 @@ package org.red5.server.net.rtmps; -import java.io.File; import java.io.NotActiveException; -import java.security.KeyStore; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.Provider; import java.security.Security; import java.util.Arrays; @@ -19,10 +19,8 @@ import org.apache.mina.core.filterchain.IoFilterChain; import org.apache.mina.core.session.IoSession; -import org.apache.mina.filter.ssl.KeyStoreFactory; -import org.apache.mina.filter.ssl.SslContextFactory; import org.apache.mina.filter.ssl.SslFilter; -import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.red5.io.tls.TLSFactory; import org.red5.server.net.rtmp.InboundHandshake; import org.red5.server.net.rtmp.RTMPConnection; import org.red5.server.net.rtmp.RTMPHandler; @@ -55,24 +53,14 @@ public class RTMPSMinaIoHandler extends RTMPMinaIoHandler { private static Logger log = LoggerFactory.getLogger(RTMPSMinaIoHandler.class); /** - * Password for accessing the keystore. + * Password for accessing the keystore and / or truststore. */ - private String keystorePassword; + private String keystorePassword, truststorePassword; /** - * Password for accessing the truststore. + * Stores the keystore and truststore paths. */ - private String truststorePassword; - - /** - * Stores the keystore path. - */ - private String keystoreFile; - - /** - * Stores the truststore path. - */ - private String truststoreFile; + private String keystorePath, truststorePath; /** * Names of the SSL cipher suites which are currently enabled for use. @@ -80,9 +68,9 @@ public class RTMPSMinaIoHandler extends RTMPMinaIoHandler { private String[] cipherSuites; /** - * Names of the protocol versions which are currently enabled for use. + * Names of the protocol versions which are currently enabled for use. Defaults to TLSv1.2. */ - private String[] protocols; + private String[] protocols = new String[] { "TLSv1.2" }; /** * Use client (or server) mode when handshaking. @@ -101,8 +89,6 @@ public class RTMPSMinaIoHandler extends RTMPMinaIoHandler { private boolean wantClientAuth; static { - // add bouncycastle security provider - Security.addProvider(new BouncyCastleProvider()); if (log.isTraceEnabled()) { Provider[] providers = Security.getProviders(); for (Provider provider : providers) { @@ -115,53 +101,34 @@ public class RTMPSMinaIoHandler extends RTMPMinaIoHandler { @Override public void sessionCreated(IoSession session) throws Exception { log.debug("Session created: RTMPS"); - if (keystoreFile == null || truststoreFile == null) { + if (keystorePath == null || truststorePath == null) { throw new NotActiveException("Keystore or truststore are null"); } + // determine the keystore type by the file extension + String keyStoreType = keystorePath.lastIndexOf(".p12") > 0 ? "PKCS12" : "JKS"; // create the ssl context SSLContext sslContext = null; try { - log.debug("Keystore: {}", keystoreFile); - File keyStore = new File(keystoreFile); - log.trace("Keystore - read: {} path: {}", keyStore.canRead(), keyStore.getCanonicalPath()); - log.debug("Truststore: {}", truststoreFile); - File trustStore = new File(truststoreFile); - log.trace("Truststore - read: {} path: {}", trustStore.canRead(), trustStore.getCanonicalPath()); - if (keyStore.exists() && trustStore.exists()) { - // keystore - final KeyStoreFactory keyStoreFactory = new KeyStoreFactory(); - keyStoreFactory.setDataFile(keyStore); - keyStoreFactory.setPassword(keystorePassword); - // truststore - final KeyStoreFactory trustStoreFactory = new KeyStoreFactory(); - trustStoreFactory.setDataFile(trustStore); - trustStoreFactory.setPassword(truststorePassword); - // ssl context factory - final SslContextFactory sslContextFactory = new SslContextFactory(); - //sslContextFactory.setProtocol("TLS"); - // get keystore - final KeyStore ks = keyStoreFactory.newInstance(); - sslContextFactory.setKeyManagerFactoryKeyStore(ks); - // get truststore - final KeyStore ts = trustStoreFactory.newInstance(); - sslContextFactory.setTrustManagerFactoryKeyStore(ts); - sslContextFactory.setKeyManagerFactoryKeyStorePassword(keystorePassword); - // get ssl context - sslContext = sslContextFactory.newInstance(); - log.debug("SSL provider is: {}", sslContext.getProvider()); - // get ssl context parameters - SSLParameters params = sslContext.getDefaultSSLParameters(); - if (log.isDebugEnabled()) { - log.debug("SSL context params - need client auth: {} want client auth: {} endpoint id algorithm: {}", params.getNeedClientAuth(), params.getWantClientAuth(), params.getEndpointIdentificationAlgorithm()); - String[] supportedProtocols = params.getProtocols(); - for (String protocol : supportedProtocols) { - log.debug("SSL context supported protocol: {}", protocol); - } + sslContext = TLSFactory.getTLSContext(keyStoreType, keystorePassword, keystorePath, truststorePassword, truststorePath); + log.debug("SSL provider is: {}", sslContext.getProvider()); + // get ssl context parameters + SSLParameters params = sslContext.getDefaultSSLParameters(); + //params.setApplicationProtocols(protocols); + if (log.isDebugEnabled()) { + Arrays.asList(params.getCipherSuites()).forEach(cipher -> log.debug("Supported cipher suite: {}", cipher)); + } + params.setCipherSuites(cipherSuites); + // set the endpoint identification algorithm + //params.setEndpointIdentificationAlgorithm("RTMPS"); + //params.setProtocols(protocols); + // choose to honor the client's preference rather than its own preference + params.setUseCipherSuitesOrder(false); + if (log.isDebugEnabled()) { + log.debug("SSL context params - need client auth: {} want client auth: {} endpoint id algorithm: {}", params.getNeedClientAuth(), params.getWantClientAuth(), params.getEndpointIdentificationAlgorithm()); + String[] supportedProtocols = params.getProtocols(); + for (String protocol : supportedProtocols) { + log.debug("SSL context supported protocol: {}", protocol); } - // compatibility: remove the SSLv2Hello message in the available protocols - some systems will fail - // to handshake if TSLv1 messages are enwrapped with SSLv2 messages, Java 6 tries to send TSLv1 embedded in SSLv2 - } else { - log.warn("Keystore or Truststore file does not exist"); } } catch (Exception ex) { log.error("Exception getting SSL context", ex); @@ -187,9 +154,6 @@ public void sessionCreated(IoSession session) throws Exception { // use notification messages session.setAttribute(SslFilter.USE_NOTIFICATION, Boolean.TRUE); log.debug("isSslStarted: {}", sslFilter.isSslStarted(session)); - //if (log.isTraceEnabled()) { - // chain.addLast("logger", new LoggingFilter()); - //} // add rtmps filter session.getFilterChain().addAfter("sslFilter", "rtmpsFilter", new RTMPSIoFilter()); // create a connection @@ -234,8 +198,13 @@ public void setTruststorePassword(String password) { * @param path * contains keystore */ - public void setKeystoreFile(String path) { - this.keystoreFile = path; + public void setKeystorePath(String path) { + if (Path.of(path).isAbsolute()) { + this.keystorePath = path; + } else { + this.keystorePath = Paths.get(System.getProperty("user.dir"), path).toString(); + } + this.keystorePath = path; } /** @@ -244,8 +213,13 @@ public void setKeystoreFile(String path) { * @param path * contains truststore */ - public void setTruststoreFile(String path) { - this.truststoreFile = path; + public void setTruststorePath(String path) { + if (Path.of(path).isAbsolute()) { + this.truststorePath = path; + } else { + this.truststorePath = Paths.get(System.getProperty("user.dir"), path).toString(); + } + this.truststorePath = path; } public String[] getCipherSuites() { diff --git a/server/src/main/java/org/red5/server/tomcat/rtmps/RTMPSLoader.java b/server/src/main/java/org/red5/server/tomcat/rtmps/RTMPSLoader.java index 9f8609b84..f5a70e4ef 100644 --- a/server/src/main/java/org/red5/server/tomcat/rtmps/RTMPSLoader.java +++ b/server/src/main/java/org/red5/server/tomcat/rtmps/RTMPSLoader.java @@ -102,14 +102,14 @@ public void start() throws ServletException { // add servlet wrapper StandardWrapper wrapper = new StandardWrapper(); wrapper.setServletName("RTMPTServlet"); - wrapper.setServletClass("org.red5.server.net.rtmpt.RTMPTServlet"); + wrapper.setServletClass("org.red5.server.net.rtmps.RTMPSLoader"); ctx.addChild(wrapper); // add servlet mappings - ctx.addServletMappingDecoded("/open/*", "RTMPTServlet"); - ctx.addServletMappingDecoded("/close/*", "RTMPTServlet"); - ctx.addServletMappingDecoded("/send/*", "RTMPTServlet"); - ctx.addServletMappingDecoded("/idle/*", "RTMPTServlet"); + ctx.addServletMappingDecoded("/open/*", "RTMPSLoader"); + ctx.addServletMappingDecoded("/close/*", "RTMPSLoader"); + ctx.addServletMappingDecoded("/send/*", "RTMPSLoader"); + ctx.addServletMappingDecoded("/idle/*", "RTMPSLoader"); // add the host rtmpsEngine.addChild(host); // add new Engine to set of Engine for embedded server diff --git a/server/src/main/server/conf/red5-core.xml b/server/src/main/server/conf/red5-core.xml index d392e9395..86bbfb7ea 100644 --- a/server/src/main/server/conf/red5-core.xml +++ b/server/src/main/server/conf/red5-core.xml @@ -123,33 +123,13 @@ -