Skip to content

Commit 5a65303

Browse files
committed
Improved performance of extracting siblings
1 parent 10bfb5a commit 5a65303

10 files changed

Lines changed: 99 additions & 56 deletions

File tree

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232

3333
<properties>
3434
<version.java>21</version.java>
35-
<version.ayza>10.0.3</version.ayza>
35+
<version.ayza>10.0.4</version.ayza>
3636
<version.sude>2.0.2</version.sude>
3737
<version.slf4j>2.0.17</version.slf4j>
3838
<version.picocli>4.7.7</version.picocli>

src/main/java/nl/altindag/crip/client/CertificateRipperClient.java

Lines changed: 58 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -38,16 +38,16 @@
3838
import java.time.Duration;
3939
import java.time.temporal.ChronoUnit;
4040
import java.util.ArrayList;
41+
import java.util.Collections;
4142
import java.util.HashMap;
42-
import java.util.LinkedHashMap;
4343
import java.util.List;
4444
import java.util.Map;
4545
import java.util.Objects;
46-
import java.util.Optional;
4746
import java.util.function.UnaryOperator;
4847
import java.util.stream.Collectors;
4948

5049
import nl.altindag.ssl.util.CertificateUtils;
50+
import nl.altindag.ssl.util.ClientRunnable;
5151
import nl.altindag.sude.Logger;
5252
import nl.altindag.sude.LoggerFactory;
5353

@@ -66,48 +66,45 @@ public CertificateRipperClient(ClientConfig clientConfig) {
6666

6767
public CertificateHolder getCertificateHolder() {
6868
List<String> resolvedUrls = getUniqueUrls(clientConfig.getUrls());
69-
Map<String, List<X509Certificate>> urlsToCertificates = getCertificates(resolvedUrls);
69+
Map<String, List<X509Certificate>> certificates = getCertificates(resolvedUrls);
70+
Map<String, List<X509Certificate>> siblings = getSiblings(certificates);
71+
Map<String, List<X509Certificate>> systemCertificates = getSystemCertificates();
7072

71-
addSiblingsIfNeeded(urlsToCertificates);
72-
urlsToCertificates = filterCertificatesIfNeeded(urlsToCertificates, clientConfig.getCertificateType());
73-
addSystemCertificatesIfNeeded(urlsToCertificates);
73+
Map<String, List<X509Certificate>> urlsToCertificates = mergeMaps(certificates, siblings, systemCertificates);
74+
urlsToCertificates = filterCertificates(urlsToCertificates, clientConfig.getCertificateType());
7475

7576
return new CertificateHolder(urlsToCertificates);
7677
}
7778

7879
private Map<String, List<X509Certificate>> getCertificates(List<String> urls) {
79-
return urls.stream().distinct().parallel()
80-
.map(this::getCertificates)
81-
.filter(Optional::isPresent)
82-
.map(Optional::get)
83-
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (key1, key2) -> key1, LinkedHashMap::new), HashMap::new));
84-
}
85-
86-
private Optional<Map.Entry<String, List<X509Certificate>>> getCertificates(String url) {
87-
try {
88-
CertificateExtractingClient client = createClient(url).build();
89-
List<X509Certificate> certificates = client.get(url);
90-
return Optional.of(Map.entry(url, certificates));
91-
} catch (Exception e) {
92-
LOGGER.debug(String.format("Could not extract from %s", url), e);
93-
return Optional.empty();
94-
}
80+
return urls.stream().parallel()
81+
.map(url -> {
82+
try {
83+
var client = createClient(url);
84+
List<X509Certificate> certificates = client.get(url);
85+
return Map.entry(url, certificates);
86+
} catch (Exception e) {
87+
LOGGER.debug(String.format("Could not extract from %s", url), e);
88+
return null;
89+
}})
90+
.filter(Objects::nonNull)
91+
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue), HashMap::new));
9592
}
9693

97-
private CertificateExtractingClient.Builder createClient(String url) {
98-
CertificateExtractingClient.Builder clientBuilder = createClient();
99-
URI uri = URI.create(url);
100-
switch (uri.getScheme()) {
101-
case "wss" -> clientBuilder.withClientRunnable(new WebSocketClientRunnable());
102-
case "ftps" -> clientBuilder.withClientRunnable(new FtpsClientRunnable());
103-
case "smtps" -> clientBuilder.withClientRunnable(new SmtpClientRunnable());
104-
case "imaps" -> clientBuilder.withClientRunnable(new ImapClientRunnable());
105-
case "postgresql" -> clientBuilder.withClientRunnable(new PostgresClientRunnable());
106-
case "mysql" -> clientBuilder.withClientRunnable(new MySQLClientRunnable());
107-
default -> {}
108-
}
94+
private CertificateExtractingClient createClient(String url) {
95+
ClientRunnable clientRunnable = switch (URI.create(url).getScheme()) {
96+
case "wss" -> WebSocketClientRunnable.getInstance();
97+
case "ftps" -> FtpsClientRunnable.getInstance();
98+
case "smtps" -> SmtpClientRunnable.getInstance();
99+
case "imaps" -> ImapClientRunnable.getInstance();
100+
case "postgresql" -> PostgresClientRunnable.getInstance();
101+
case "mysql" -> MySQLClientRunnable.getInstance();
102+
default -> null;
103+
};
109104

110-
return clientBuilder;
105+
return createClient()
106+
.withClientRunnable(clientRunnable)
107+
.build();
111108
}
112109

113110
private CertificateExtractingClient.Builder createClient() {
@@ -156,9 +153,9 @@ private List<String> getUniqueUrls(List<String> urls) {
156153
return uniqueUrls;
157154
}
158155

159-
private void addSiblingsIfNeeded(Map<String, List<X509Certificate>> urlsToCertificates) {
156+
private Map<String, List<X509Certificate>> getSiblings(Map<String, List<X509Certificate>> urlsToCertificates) {
160157
if (!clientConfig.getResolveSiblings()) {
161-
return;
158+
return Collections.emptyMap();
162159
}
163160

164161
ProgressBarBuilder pbb = new ProgressBarBuilder()
@@ -167,38 +164,40 @@ private void addSiblingsIfNeeded(Map<String, List<X509Certificate>> urlsToCertif
167164
.setStyle(ProgressBarStyle.COLORFUL_UNICODE_BAR)
168165
.setTaskName("Resolving sibling certificates").showSpeed();
169166

170-
List<String> urls = urlsToCertificates.values().stream().parallel()
171-
.flatMap(certificates -> UriUtils.getDnsNames(certificates).stream())
167+
List<String> urls = urlsToCertificates.values().stream()
168+
.flatMap(certificates -> UriUtils.extractHostsFromSAN(certificates).stream())
172169
.distinct()
173170
.toList();
174171

175172
CertificateExtractingClient client = createClient().build();
176-
Map<String, List<X509Certificate>> siblings = ProgressBar.wrap(urls.stream(), pbb)
177-
.map(url -> {
173+
ProgressBar.wrap(urls.stream(), pbb)
174+
.parallel()
175+
.forEach(url -> {
178176
try {
179-
return Map.entry(url, client.get(url));
177+
client.call(url);
180178
} catch (Exception e) {
181-
return null;
179+
LOGGER.debug(String.format("Could not extract sibling certificate from %s", url), e);
182180
}
183-
})
184-
.filter(Objects::nonNull)
185-
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (key1, key2) -> key1, LinkedHashMap::new), HashMap::new));
181+
});
186182

187-
urlsToCertificates.putAll(siblings);
183+
return client.getCertificatesCollector().entrySet().stream()
184+
.map(entry -> Map.entry("https://" + entry.getKey(), entry.getValue()))
185+
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
188186
}
189187

190-
private void addSystemCertificatesIfNeeded(Map<String, List<X509Certificate>> urlsToCertificates) {
188+
private Map<String, List<X509Certificate>> getSystemCertificates() {
191189
if (clientConfig.getUrls().contains(SYSTEM)) {
192190
try {
193191
List<X509Certificate> systemTrustedCertificates = CertificateUtils.getSystemTrustedCertificates();
194-
urlsToCertificates.put(SYSTEM, systemTrustedCertificates);
192+
return Map.of(SYSTEM, systemTrustedCertificates);
195193
} catch (UnsatisfiedLinkError error) {
196194
LOGGER.debug(String.format("Unable to extract system certificates for %s", System.getProperty("os.name")));
197195
}
198196
}
197+
return Collections.emptyMap();
199198
}
200199

201-
Map<String, List<X509Certificate>> filterCertificatesIfNeeded(Map<String, List<X509Certificate>> urlsToCertificates, CertificateType type) {
200+
Map<String, List<X509Certificate>> filterCertificates(Map<String, List<X509Certificate>> urlsToCertificates, CertificateType type) {
202201
return switch (type) {
203202
case ALL -> urlsToCertificates;
204203
case LEAF -> filterCertificates(urlsToCertificates, certificates -> List.of(certificates.getFirst()));
@@ -221,4 +220,13 @@ private Map<String, List<X509Certificate>> filterCertificates(Map<String, List<X
221220
.collect(Collectors.collectingAndThen(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue), HashMap::new));
222221
}
223222

223+
@SafeVarargs
224+
private static <T, U> Map<T, U> mergeMaps(Map<T, U>... maps) {
225+
Map<T, U> mergedMap = new HashMap<>();
226+
for (Map<T, U> map : maps) {
227+
mergedMap.putAll(map);
228+
}
229+
return mergedMap;
230+
}
231+
224232
}

src/main/java/nl/altindag/crip/client/ftp/FtpsClientRunnable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
public final class FtpsClientRunnable implements ClientRunnable {
3030

3131
private static final Logger LOGGER = LoggerFactory.getLogger(FtpsClientRunnable.class);
32+
private static final FtpsClientRunnable INSTANCE = new FtpsClientRunnable();
3233

3334
@Override
3435
public void run(ClientConfig clientConfig, URI uri) {
@@ -59,4 +60,8 @@ public void run(ClientConfig clientConfig, URI uri) {
5960
}
6061
}
6162

63+
public static FtpsClientRunnable getInstance() {
64+
return INSTANCE;
65+
}
66+
6267
}

src/main/java/nl/altindag/crip/client/imap/ImapClientRunnable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
public final class ImapClientRunnable implements ClientRunnable {
2929

3030
private static final Logger LOGGER = LoggerFactory.getLogger(ImapClientRunnable.class);
31+
private static final ImapClientRunnable INSTANCE = new ImapClientRunnable();
3132

3233
@Override
3334
public void run(ClientConfig clientConfig, URI uri) {
@@ -45,4 +46,8 @@ public void run(ClientConfig clientConfig, URI uri) {
4546
}
4647
}
4748

49+
public static ImapClientRunnable getInstance() {
50+
return INSTANCE;
51+
}
52+
4853
}

src/main/java/nl/altindag/crip/client/mysql/MySQLClientRunnable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
public final class MySQLClientRunnable implements ClientRunnable {
3636

3737
private static final Logger LOGGER = LoggerFactory.getLogger(MySQLClientRunnable.class);
38+
private static final MySQLClientRunnable INSTANCE = new MySQLClientRunnable();
3839

3940
private static final int SSL_FLAG = 0x800;
4041
private static final byte[] SSL_REQUEST = {
@@ -122,4 +123,8 @@ private static int getCapabilityFlags(int bytesRead, byte[] buffer) throws IOExc
122123
return (buffer[pos] & 0xFF) + ((buffer[pos + 1] & 0xFF) << 8);
123124
}
124125

126+
public static MySQLClientRunnable getInstance() {
127+
return INSTANCE;
128+
}
129+
125130
}

src/main/java/nl/altindag/crip/client/postgres/PostgresClientRunnable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
public final class PostgresClientRunnable implements ClientRunnable {
3737

3838
private static final Logger LOGGER = LoggerFactory.getLogger(PostgresClientRunnable.class);
39+
private static final PostgresClientRunnable INSTANCE = new PostgresClientRunnable();
3940

4041
private static final int SSL_REQUEST_MESSAGE_LENGTH = 8;
4142
private static final int SSL_REQUEST_CODE = 80877103;
@@ -73,4 +74,8 @@ public void run(ClientConfig clientConfig, URI uri) {
7374
}
7475
}
7576

77+
public static PostgresClientRunnable getInstance() {
78+
return INSTANCE;
79+
}
80+
7681
}

src/main/java/nl/altindag/crip/client/smtp/SmtpClientRunnable.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
public final class SmtpClientRunnable implements ClientRunnable {
2929

3030
private static final Logger LOGGER = LoggerFactory.getLogger(SmtpClientRunnable.class);
31+
private static final SmtpClientRunnable INSTANCE = new SmtpClientRunnable();
3132

3233
@Override
3334
public void run(ClientConfig clientConfig, URI uri) {
@@ -60,4 +61,8 @@ public void run(ClientConfig clientConfig, URI uri) {
6061
}
6162
}
6263

64+
public static SmtpClientRunnable getInstance() {
65+
return INSTANCE;
66+
}
67+
6368
}

src/main/java/nl/altindag/crip/client/websocket/WebSocketClientRunnable.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232

3333
public final class WebSocketClientRunnable implements ClientRunnable {
3434

35+
private static final WebSocketClientRunnable INSTANCE = new WebSocketClientRunnable();
36+
3537
@Override
3638
public void run(ClientConfig clientConfig, URI uri) {
3739
HttpClient.Builder clientBuilder = HttpClient.newBuilder()
@@ -69,4 +71,8 @@ public void connectFailed(URI uri, SocketAddress sa, IOException ioe) {
6971
};
7072
}
7173

74+
public static WebSocketClientRunnable getInstance() {
75+
return INSTANCE;
76+
}
77+
7278
}

src/main/java/nl/altindag/crip/util/UriUtils.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ public static int extractPort(String value) {
5454
}
5555
}
5656

57-
public static List<String> getDnsNames(List<X509Certificate> certificates) {
57+
/**
58+
* Extracts the DNS Names from the Subject Alternative Name extension of the provided certificates.
59+
* And appends "https://" prefix to each DNS name.
60+
*/
61+
public static List<String> extractHostsFromSAN(List<X509Certificate> certificates) {
5862
List<String> dnsNames = new ArrayList<>();
5963
for (X509Certificate certificate : certificates) {
6064
try {

src/test/java/nl/altindag/crip/client/CertificateRipperClientShould.java

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,14 @@ static void loadCertificates() {
5454

5555
@Test
5656
void doNotFilterCertificatesWhenAllCertificateTypeIsSpecified() {
57-
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificatesIfNeeded(urlsToCertificates, CertificateType.ALL);
57+
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificates(urlsToCertificates, CertificateType.ALL);
5858
assertThat(filteredUrlsToCertificates).hasSize(1).hasKeySatisfying(new Condition<>("https://google.com"::equals, "Key is https://google.com"));
5959
assertThat(filteredUrlsToCertificates.get("https://google.com")).hasSize(4);
6060
}
6161

6262
@Test
6363
void filterCertificatesWhenRootCertificateTypeIsSpecified() {
64-
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificatesIfNeeded(urlsToCertificates, CertificateType.ROOT);
64+
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificates(urlsToCertificates, CertificateType.ROOT);
6565
assertThat(filteredUrlsToCertificates).hasSize(1).hasKeySatisfying(new Condition<>("https://google.com"::equals, "Key is https://google.com"));
6666
assertThat(filteredUrlsToCertificates.get("https://google.com")).hasSize(1);
6767

@@ -71,7 +71,7 @@ void filterCertificatesWhenRootCertificateTypeIsSpecified() {
7171

7272
@Test
7373
void filterCertificatesWhenLeafCertificateTypeIsSpecified() {
74-
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificatesIfNeeded(urlsToCertificates, CertificateType.LEAF);
74+
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificates(urlsToCertificates, CertificateType.LEAF);
7575
assertThat(filteredUrlsToCertificates).hasSize(1).hasKeySatisfying(new Condition<>("https://google.com"::equals, "Key is https://google.com"));
7676
assertThat(filteredUrlsToCertificates.get("https://google.com")).hasSize(1);
7777

@@ -81,7 +81,7 @@ void filterCertificatesWhenLeafCertificateTypeIsSpecified() {
8181

8282
@Test
8383
void filterCertificatesWhenInterCertificateTypeIsSpecified() {
84-
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificatesIfNeeded(urlsToCertificates, CertificateType.INTER);
84+
Map<String, List<X509Certificate>> filteredUrlsToCertificates = victim.filterCertificates(urlsToCertificates, CertificateType.INTER);
8585
assertThat(filteredUrlsToCertificates).hasSize(1).hasKeySatisfying(new Condition<>("https://google.com"::equals, "Key is https://google.com"));
8686
assertThat(filteredUrlsToCertificates.get("https://google.com")).hasSize(2);
8787

0 commit comments

Comments
 (0)