Skip to content

Commit d398204

Browse files
authored
Merge pull request #376 from Red5/bug/rtmpclient
Bug/rtmpclient
2 parents 86956d9 + 459d4a1 commit d398204

File tree

66 files changed

+819
-203
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

66 files changed

+819
-203
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,6 @@ log/
1515
temp/
1616
ci/Red5-release-steps.md
1717
/.metadata/
18+
19+
*.crt
20+
*.pem

client/pom.xml

Lines changed: 0 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -53,22 +53,5 @@
5353
<version>${mina.version}</version>
5454
<type>bundle</type>
5555
</dependency>
56-
<dependency>
57-
<groupId>junit</groupId>
58-
<artifactId>junit</artifactId>
59-
<scope>test</scope>
60-
</dependency>
61-
<dependency>
62-
<groupId>net.sf.ehcache</groupId>
63-
<artifactId>ehcache</artifactId>
64-
<version>${ehcache.version}</version>
65-
<scope>runtime</scope>
66-
<exclusions>
67-
<exclusion>
68-
<groupId>*</groupId>
69-
<artifactId>*</artifactId>
70-
</exclusion>
71-
</exclusions>
72-
</dependency>
7356
</dependencies>
7457
</project>

client/src/main/java/org/red5/client/net/rtmp/BaseRTMPClientHandler.java

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1037,17 +1037,17 @@ public void resultReceived(IPendingServiceCall call) {
10371037
} else if (callResult instanceof Map) {
10381038
Map<?, ?> map = (Map<?, ?>) callResult;
10391039
// XXX(paul) log out the map contents
1040-
log.warn("CreateStreamCallBack resultReceived - map: {}", map);
1040+
log.warn("{} resultReceived - map: {}", call.getClass().getSimpleName(), map);
10411041
if (map.containsKey("streamId")) {
10421042
Object tmpStreamId = map.get("streamId");
10431043
if (tmpStreamId instanceof Number) {
10441044
streamId = ((Number) tmpStreamId).intValue();
10451045
} else {
1046-
log.warn("CreateStreamCallBack resultReceived - stream id is not a number: {}", tmpStreamId);
1046+
log.warn("{} resultReceived - stream id is not a number: {}", call.getClass().getName(), tmpStreamId);
10471047
}
10481048
}
10491049
}
1050-
log.debug("CreateStreamCallBack resultReceived - stream id: {} call: {} connection: {}", streamId, call, conn);
1050+
log.debug("{} resultReceived - stream id: {} call: {} connection: {}", call.getClass().getName(), streamId, call, conn);
10511051
if (conn != null && streamId != -1) {
10521052
log.debug("Setting new net stream");
10531053
NetStream stream = new NetStream(streamEventDispatcher);
@@ -1062,7 +1062,7 @@ public void resultReceived(IPendingServiceCall call) {
10621062
}
10631063
wrapped.resultReceived(call);
10641064
} else {
1065-
log.warn("CreateStreamCallBack resultReceived - call result is null");
1065+
log.warn("{} resultReceived - call result is null", call.getClass().getSimpleName());
10661066
}
10671067
}
10681068
}
@@ -1078,6 +1078,7 @@ public ReleaseStreamCallBack(IPendingServiceCallback wrapped) {
10781078

10791079
@Override
10801080
public void resultReceived(IPendingServiceCall call) {
1081+
log.debug("ReleaseStreamCallBack resultReceived - call: {}", call);
10811082
wrapped.resultReceived(call);
10821083
}
10831084
}
@@ -1093,6 +1094,7 @@ public DeleteStreamCallBack(IPendingServiceCallback wrapped) {
10931094

10941095
@Override
10951096
public void resultReceived(IPendingServiceCall call) {
1097+
log.debug("DeleteStreamCallBack resultReceived - call: {}", call);
10961098
// get the result as base object
10971099
Object callResult = call.getResult();
10981100
if (callResult != null) {

client/src/main/java/org/red5/client/net/rtmp/RTMPClient.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,4 +134,23 @@ public void setProtocol(String protocol) throws Exception {
134134
throw new Exception("Unsupported protocol specified, please use the correct client for the intended protocol");
135135
}
136136
}
137+
138+
/**
139+
* Logs the contents of a map at debug level.
140+
* This is useful for debugging purposes, especially when dealing with connection parameters or other
141+
* configuration settings.
142+
*
143+
* @param map
144+
* @param name
145+
*/
146+
protected void logMap(Map<String, Object> map, String name) {
147+
if (isDebug) {
148+
log.debug("logMap------ {} -------", name);
149+
for (String key : map.keySet()) {
150+
Object obj = map.get(key);
151+
log.debug("{} : {} ({})", key, obj, obj == null ? "--" : obj.getClass().getName());
152+
}
153+
}
154+
}
155+
137156
}

client/src/main/java/org/red5/client/net/rtmp/RTMPClientConnManager.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,8 +74,7 @@ public BaseConnection createConnection(Class<?> connCls) {
7474
conn = createConnectionInstance(connCls);
7575
// add to local map
7676
connMap.put(conn.getSessionId(), conn);
77-
log.trace("Connections: {}", conns.incrementAndGet());
78-
log.trace("Connection created: {}", conn);
77+
log.trace("Connections: {} created: {}", conns.incrementAndGet(), conn);
7978
} catch (Exception ex) {
8079
log.warn("Exception creating connection", ex);
8180
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package org.red5.client.net.rtmps;
2+
3+
import java.io.FileWriter;
4+
import java.io.PrintWriter;
5+
import java.security.cert.Certificate;
6+
import java.security.cert.X509Certificate;
7+
8+
import javax.net.ssl.SSLContext;
9+
import javax.net.ssl.SSLSession;
10+
import javax.net.ssl.SSLSocket;
11+
import javax.net.ssl.SSLSocketFactory;
12+
import javax.net.ssl.TrustManager;
13+
import javax.net.ssl.X509TrustManager;
14+
15+
import org.apache.commons.codec.binary.Base64;
16+
import org.slf4j.Logger;
17+
import org.slf4j.LoggerFactory;
18+
19+
/**
20+
* Utility class to grab and save server certificates from a given host and port; useful for TLS connections to ensure
21+
* a server's certificate is trusted.
22+
*
23+
* Usage:
24+
* Call the `retrieveCertificate` method with the desired host and port to save the server's certificate in PEM format.
25+
*
26+
* @author Paul Gregoire
27+
*/
28+
public class CertificateGrabber {
29+
30+
private static Logger log = LoggerFactory.getLogger(CertificateGrabber.class);
31+
32+
/**
33+
* Retrieves the server certificate from the specified host and port.
34+
*
35+
* @param host
36+
* @param port
37+
* @throws Exception
38+
*/
39+
public static void retrieveCertificate(String host, int port) throws Exception {
40+
// Create a trust manager that captures certificates
41+
final X509Certificate[] serverCert = new X509Certificate[1];
42+
TrustManager[] trustManagers = new TrustManager[] { new X509TrustManager() {
43+
public void checkClientTrusted(X509Certificate[] chain, String authType) {
44+
}
45+
46+
public void checkServerTrusted(X509Certificate[] chain, String authType) {
47+
// Capture the server certificate
48+
if (chain != null && chain.length > 0) {
49+
serverCert[0] = chain[0];
50+
}
51+
}
52+
53+
public X509Certificate[] getAcceptedIssuers() {
54+
return new X509Certificate[0];
55+
}
56+
} };
57+
// Create SSL context
58+
SSLContext sslContext = SSLContext.getInstance("TLS");
59+
sslContext.init(null, trustManagers, null);
60+
// Connect to the server
61+
SSLSocketFactory factory = sslContext.getSocketFactory();
62+
try (SSLSocket socket = (SSLSocket) factory.createSocket(host, port)) {
63+
socket.startHandshake();
64+
// Get the certificate chain
65+
SSLSession session = socket.getSession();
66+
Certificate[] certs = session.getPeerCertificates();
67+
// Save the certificate
68+
if (certs != null && certs.length > 0) {
69+
X509Certificate cert = (X509Certificate) certs[0];
70+
saveCertificate(cert, String.format("%s.pem", host));
71+
log.debug("Certificate subject: {} issuer: {}\n serial number: {}\n valid from: {} to: {}", cert.getSubjectX500Principal(), cert.getIssuerX500Principal(), cert.getSerialNumber(), cert.getNotBefore(), cert.getNotAfter());
72+
}
73+
}
74+
}
75+
76+
/**
77+
* Saves the given X509 certificate to a file in PEM format.
78+
*
79+
* @param cert the X509 certificate to save
80+
* @param fileName name of the file to save the certificate to
81+
* @throws Exception if an error occurs while saving the certificate
82+
*/
83+
private static void saveCertificate(X509Certificate cert, String fileName) throws Exception {
84+
// Save as PEM format
85+
try (FileWriter fw = new FileWriter(fileName); PrintWriter pw = new PrintWriter(fw)) {
86+
pw.println("-----BEGIN CERTIFICATE-----");
87+
pw.println(Base64.encodeBase64String(cert.getEncoded()));
88+
pw.println("-----END CERTIFICATE-----");
89+
}
90+
log.debug("Certificate saved to: {}", fileName);
91+
}
92+
93+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package org.red5.client.net.rtmps;
2+
3+
import java.io.FileInputStream;
4+
import java.io.FileNotFoundException;
5+
import java.io.FileOutputStream;
6+
import java.io.InputStream;
7+
import java.security.KeyStore;
8+
import java.security.cert.CertificateFactory;
9+
import java.security.cert.X509Certificate;
10+
import java.util.Collection;
11+
12+
import org.slf4j.Logger;
13+
import org.slf4j.LoggerFactory;
14+
15+
/**
16+
* Manages a PKCS#12 (P12) truststore for TLS connections.
17+
*/
18+
public class P12StoreManager {
19+
20+
private static Logger log = LoggerFactory.getLogger(P12StoreManager.class);
21+
22+
@SuppressWarnings("unchecked")
23+
public static KeyStore buildTrustStore(String truststorePath, char[] truststorePassword, String certPath) {
24+
KeyStore trustStore = null;
25+
try {
26+
// load / create the store (P12 format)
27+
trustStore = KeyStore.getInstance("PKCS12");
28+
try (InputStream is = new FileInputStream(truststorePath)) {
29+
trustStore.load(is, truststorePassword);
30+
log.info("Truststore loaded successfully from {}", truststorePath);
31+
} catch (FileNotFoundException e) {
32+
// store doesn't exist, create a new one
33+
trustStore.load(null, truststorePassword);
34+
log.info("New truststore created successfully at {}", truststorePath);
35+
}
36+
// load the certs
37+
CertificateFactory certFactory = CertificateFactory.getInstance("X.509");
38+
// if we have a single PEM file with the full chain
39+
try (InputStream certIs = new FileInputStream(certPath)) {
40+
// read all certificates from the stream (useful for chain)
41+
for (X509Certificate cert : (Collection<X509Certificate>) certFactory.generateCertificates(certIs)) {
42+
// generate an alias based on the certificate's subject name
43+
// using hashCode to ensure uniqueness
44+
String alias = "cert_" + Integer.toHexString(cert.getSubjectX500Principal().getName().hashCode());
45+
log.info("Processing certificate with alias: {}", alias);
46+
if (trustStore.containsAlias(alias)) {
47+
log.warn("Certificate with alias '{}' already exists, skipping", alias);
48+
} else {
49+
trustStore.setCertificateEntry(alias, cert);
50+
log.info("Imported certificate: {}", cert.getSubjectX500Principal().getName());
51+
}
52+
}
53+
}
54+
// save the KeyStore
55+
try (FileOutputStream fos = new FileOutputStream(truststorePath)) {
56+
trustStore.store(fos, truststorePassword);
57+
log.info("Truststore saved successfully to {}", truststorePath);
58+
}
59+
} catch (Exception e) {
60+
log.error("Error managing truststore", e);
61+
}
62+
return trustStore;
63+
}
64+
}

0 commit comments

Comments
 (0)