mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-10-05 21:22:39 +02:00
Compare commits
8 Commits
deploy-025
...
deploy-025
Author | SHA1 | Date | |
---|---|---|---|
|
470b866008 | ||
|
4895a2ac7a | ||
|
fd32ae9fa7 | ||
|
470651ea4c | ||
|
8d4829e783 | ||
|
1290bc15dc | ||
|
e7fa558954 | ||
|
720685bf3f |
@@ -0,0 +1,7 @@
|
||||
-- Add additional summary columns to DOMAIN_SECURITY_INFORMATION table
|
||||
-- to make it easier to get more information about the SSL certificate's validity
|
||||
|
||||
ALTER TABLE DOMAIN_SECURITY_INFORMATION ADD COLUMN SSL_CHAIN_VALID BOOLEAN DEFAULT NULL;
|
||||
ALTER TABLE DOMAIN_SECURITY_INFORMATION ADD COLUMN SSL_HOST_VALID BOOLEAN DEFAULT NULL;
|
||||
ALTER TABLE DOMAIN_SECURITY_INFORMATION ADD COLUMN SSL_DATE_VALID BOOLEAN DEFAULT NULL;
|
||||
OPTIMIZE TABLE DOMAIN_SECURITY_INFORMATION;
|
@@ -112,7 +112,7 @@ public class HttpClientProvider implements Provider<HttpClient> {
|
||||
});
|
||||
|
||||
final RequestConfig defaultRequestConfig = RequestConfig.custom()
|
||||
.setCookieSpec(StandardCookieSpec.RELAXED)
|
||||
.setCookieSpec(StandardCookieSpec.IGNORE)
|
||||
.setResponseTimeout(10, TimeUnit.SECONDS)
|
||||
.setConnectionRequestTimeout(5, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
@@ -11,6 +11,7 @@ import org.slf4j.LoggerFactory;
|
||||
|
||||
import javax.net.ssl.SSLException;
|
||||
import java.io.IOException;
|
||||
import java.net.SocketException;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.net.UnknownHostException;
|
||||
|
||||
@@ -23,7 +24,8 @@ public class RetryStrategy implements HttpRequestRetryStrategy {
|
||||
case SocketTimeoutException ste -> false;
|
||||
case SSLException ssle -> false;
|
||||
case UnknownHostException uhe -> false;
|
||||
case HttpHostConnectException ex -> executionCount <= 2; // Only retry once for connection errors
|
||||
case HttpHostConnectException ex -> executionCount < 2;
|
||||
case SocketException ex -> executionCount < 2;
|
||||
default -> executionCount <= 3;
|
||||
};
|
||||
}
|
||||
|
@@ -43,7 +43,10 @@ public record DomainSecurityRecord(
|
||||
@Nullable String headerXXssProtection,
|
||||
@Nullable String headerServer,
|
||||
@Nullable String headerXPoweredBy,
|
||||
@Nullable Instant tsLastUpdate
|
||||
@Nullable Instant tsLastUpdate,
|
||||
@Nullable Boolean sslChainValid,
|
||||
@Nullable Boolean sslHostValid,
|
||||
@Nullable Boolean sslDateValid
|
||||
)
|
||||
implements WritableModel
|
||||
{
|
||||
@@ -103,7 +106,11 @@ public record DomainSecurityRecord(
|
||||
rs.getString("DOMAIN_SECURITY_INFORMATION.HEADER_X_XSS_PROTECTION"),
|
||||
rs.getString("DOMAIN_SECURITY_INFORMATION.HEADER_SERVER"),
|
||||
rs.getString("DOMAIN_SECURITY_INFORMATION.HEADER_X_POWERED_BY"),
|
||||
rs.getObject("DOMAIN_SECURITY_INFORMATION.TS_LAST_UPDATE", Instant.class));
|
||||
rs.getObject("DOMAIN_SECURITY_INFORMATION.TS_LAST_UPDATE", Instant.class),
|
||||
rs.getObject("SSL_CHAIN_VALID", Boolean.class),
|
||||
rs.getObject("SSL_HOST_VALID", Boolean.class),
|
||||
rs.getObject("SSL_DATE_VALID", Boolean.class)
|
||||
);
|
||||
}
|
||||
|
||||
private static HttpSchema httpSchemaFromString(@Nullable String schema) {
|
||||
@@ -150,8 +157,11 @@ public record DomainSecurityRecord(
|
||||
header_x_powered_by,
|
||||
ssl_cert_public_key_hash,
|
||||
asn,
|
||||
ts_last_update)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ts_last_update,
|
||||
ssl_chain_valid,
|
||||
ssl_host_valid,
|
||||
ssl_date_valid)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
"""))
|
||||
{
|
||||
ps.setInt(1, domainId());
|
||||
@@ -295,6 +305,25 @@ public record DomainSecurityRecord(
|
||||
} else {
|
||||
ps.setTimestamp(32, java.sql.Timestamp.from(tsLastUpdate()));
|
||||
}
|
||||
|
||||
if (sslChainValid() == null) {
|
||||
ps.setNull(33, java.sql.Types.BOOLEAN);
|
||||
} else {
|
||||
ps.setBoolean(33, sslChainValid());
|
||||
}
|
||||
|
||||
if (sslHostValid() == null) {
|
||||
ps.setNull(34, java.sql.Types.BOOLEAN);
|
||||
} else {
|
||||
ps.setBoolean(34, sslHostValid());
|
||||
}
|
||||
|
||||
if (sslDateValid() == null) {
|
||||
ps.setNull(35, java.sql.Types.BOOLEAN);
|
||||
} else {
|
||||
ps.setBoolean(35, sslDateValid());
|
||||
}
|
||||
|
||||
ps.executeUpdate();
|
||||
}
|
||||
}
|
||||
@@ -333,6 +362,11 @@ public record DomainSecurityRecord(
|
||||
private String headerXPoweredBy;
|
||||
private Instant tsLastUpdate;
|
||||
|
||||
private Boolean isCertChainValid;
|
||||
private Boolean isCertHostValid;
|
||||
private Boolean isCertDateValid;
|
||||
|
||||
|
||||
private static Instant MAX_UNIX_TIMESTAMP = Instant.ofEpochSecond(Integer.MAX_VALUE);
|
||||
|
||||
public Builder() {
|
||||
@@ -508,6 +542,21 @@ public record DomainSecurityRecord(
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sslChainValid(@Nullable Boolean isCertChainValid) {
|
||||
this.isCertChainValid = isCertChainValid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sslHostValid(@Nullable Boolean isCertHostValid) {
|
||||
this.isCertHostValid = isCertHostValid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder sslDateValid(@Nullable Boolean isCertDateValid) {
|
||||
this.isCertDateValid = isCertDateValid;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DomainSecurityRecord build() {
|
||||
return new DomainSecurityRecord(
|
||||
domainId,
|
||||
@@ -541,7 +590,10 @@ public record DomainSecurityRecord(
|
||||
headerXXssProtection,
|
||||
headerServer,
|
||||
headerXPoweredBy,
|
||||
tsLastUpdate
|
||||
tsLastUpdate,
|
||||
isCertChainValid,
|
||||
isCertHostValid,
|
||||
isCertDateValid
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -12,16 +12,24 @@ import java.util.*;
|
||||
/** Utility class for validating X.509 certificates.
|
||||
* This class provides methods to validate certificate chains, check expiration,
|
||||
* hostname validity, and revocation status.
|
||||
* <p></p>
|
||||
* This is extremely unsuitable for actual SSL/TLS validation,
|
||||
* and is only to be used in analyzing certificates for fingerprinting
|
||||
* and diagnosing servers!
|
||||
*/
|
||||
public class CertificateValidator {
|
||||
// If true, will attempt to fetch missing intermediate certificates via AIA urls.
|
||||
private static final boolean TRY_FETCH_MISSING_CERTS = false;
|
||||
|
||||
public static class ValidationResult {
|
||||
public boolean chainValid = false;
|
||||
public boolean certificateExpired = false;
|
||||
public boolean certificateRevoked = false;
|
||||
public boolean selfSigned = false;
|
||||
public boolean hostnameValid = false;
|
||||
|
||||
public boolean isValid() {
|
||||
return chainValid && !certificateExpired && !certificateRevoked && hostnameValid;
|
||||
return !selfSigned && !certificateExpired && !certificateRevoked && hostnameValid;
|
||||
}
|
||||
|
||||
public List<String> errors = new ArrayList<>();
|
||||
@@ -36,6 +44,7 @@ public class CertificateValidator {
|
||||
sb.append("Not Expired: ").append(!certificateExpired ? "✓" : "✗").append("\n");
|
||||
sb.append("Not Revoked: ").append(!certificateRevoked ? "✓" : "✗").append("\n");
|
||||
sb.append("Hostname Valid: ").append(hostnameValid ? "✓" : "✗").append("\n");
|
||||
sb.append("Self-Signed: ").append(selfSigned ? "✓" : "✗").append("\n");
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
sb.append("\nErrors:\n");
|
||||
@@ -85,11 +94,15 @@ public class CertificateValidator {
|
||||
// 2. Check hostname validity
|
||||
result.hostnameValid = checkHostname(leafCert, hostname, result);
|
||||
|
||||
// 3. Check certificate chain validity (with AIA fetching)
|
||||
// 3. Not really checking if it's self-signed, but if the chain is incomplete (and likely self-signed)
|
||||
result.selfSigned = certChain.length <= 1;
|
||||
|
||||
// 4. Check certificate chain validity (optionally with AIA fetching)
|
||||
result.chainValid = checkChainValidity(certChain, RootCerts.getTrustAnchors(), result, autoTrustFetchedRoots);
|
||||
|
||||
// 4. Check revocation status
|
||||
result.certificateRevoked = checkRevocation(leafCert, result);
|
||||
// 5. Check revocation status
|
||||
result.certificateRevoked = false; // not implemented
|
||||
// checkRevocation(leafCert, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -169,6 +182,13 @@ public class CertificateValidator {
|
||||
return true;
|
||||
}
|
||||
|
||||
else if (!TRY_FETCH_MISSING_CERTS) {
|
||||
result.errors.addAll(originalResult.issues);
|
||||
result.details.put("chainLength", originalChain.length);
|
||||
result.details.put("chainExtended", false);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
List<X509Certificate> repairedChain = CertificateFetcher.buildCompleteChain(originalChain[0]);
|
||||
|
||||
|
@@ -126,6 +126,9 @@ public class DomainSecurityInformationFactory {
|
||||
.sslCertWildcard(isWildcard)
|
||||
.sslCertificateChainLength(sslCertificates.length)
|
||||
.sslCertificateValid(validationResult.isValid())
|
||||
.sslHostValid(validationResult.hostnameValid)
|
||||
.sslChainValid(validationResult.chainValid)
|
||||
.sslDateValid(!validationResult.certificateExpired)
|
||||
.httpVersion(httpResponse.version())
|
||||
.tsLastUpdate(Instant.now())
|
||||
.build();
|
||||
|
@@ -243,6 +243,7 @@ class PingDaoTest {
|
||||
.headerServer("Apache/2.4.41 (Ubuntu)")
|
||||
.headerXPoweredBy("PHP/7.4.3")
|
||||
.tsLastUpdate(Instant.now())
|
||||
.sslHostValid(true)
|
||||
.build();
|
||||
var svc = new PingDao(dataSource);
|
||||
svc.write(foo);
|
||||
|
@@ -22,6 +22,7 @@ import nu.marginalia.search.model.NavbarModel;
|
||||
import nu.marginalia.search.model.ResultsPage;
|
||||
import nu.marginalia.search.model.UrlDetails;
|
||||
import nu.marginalia.search.svc.SearchFlagSiteService.FlagSiteFormData;
|
||||
import nu.marginalia.service.server.RateLimiter;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -47,6 +48,8 @@ public class SearchSiteInfoService {
|
||||
private final HikariDataSource dataSource;
|
||||
private final SearchSiteSubscriptionService searchSiteSubscriptions;
|
||||
|
||||
private final RateLimiter rateLimiter = RateLimiter.custom(60);
|
||||
|
||||
@Inject
|
||||
public SearchSiteInfoService(SearchOperator searchOperator,
|
||||
DomainInfoClient domainInfoClient,
|
||||
@@ -238,12 +241,19 @@ public class SearchSiteInfoService {
|
||||
boolean hasScreenshot = screenshotService.hasScreenshot(domainId);
|
||||
boolean isSubscribed = searchSiteSubscriptions.isSubscribed(context, domain);
|
||||
|
||||
boolean rateLimited = !rateLimiter.isAllowed();
|
||||
if (domainId < 0) {
|
||||
domainInfoFuture = CompletableFuture.failedFuture(new Exception("Unknown Domain ID"));
|
||||
similarSetFuture = CompletableFuture.failedFuture(new Exception("Unknown Domain ID"));
|
||||
linkingDomainsFuture = CompletableFuture.failedFuture(new Exception("Unknown Domain ID"));
|
||||
feedItemsFuture = CompletableFuture.failedFuture(new Exception("Unknown Domain ID"));
|
||||
}
|
||||
else if (rateLimited) {
|
||||
domainInfoFuture = CompletableFuture.failedFuture(new Exception("Rate limit exceeded"));
|
||||
similarSetFuture = CompletableFuture.failedFuture(new Exception("Rate limit exceeded"));
|
||||
linkingDomainsFuture = CompletableFuture.failedFuture(new Exception("Rate limit exceeded"));
|
||||
feedItemsFuture = CompletableFuture.failedFuture(new Exception("Rate limit exceeded"));
|
||||
}
|
||||
else if (!domainInfoClient.isAccepting()) {
|
||||
domainInfoFuture = CompletableFuture.failedFuture(new Exception("Assistant Service Unavailable"));
|
||||
similarSetFuture = CompletableFuture.failedFuture(new Exception("Assistant Service Unavailable"));
|
||||
@@ -257,7 +267,14 @@ public class SearchSiteInfoService {
|
||||
feedItemsFuture = feedsClient.getFeed(domainId);
|
||||
}
|
||||
|
||||
List<UrlDetails> sampleResults = searchOperator.doSiteSearch(domainName, domainId,5, 1).results;
|
||||
List<UrlDetails> sampleResults;
|
||||
if (rateLimited) {
|
||||
sampleResults = List.of();
|
||||
}
|
||||
else {
|
||||
sampleResults = searchOperator.doSiteSearch(domainName, domainId, 5, 1).results;
|
||||
}
|
||||
|
||||
if (!sampleResults.isEmpty()) {
|
||||
url = sampleResults.getFirst().url.withPathAndParam("/", null).toString();
|
||||
}
|
||||
|
@@ -0,0 +1,11 @@
|
||||
@param String message
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head><meta charset="UTF-8">
|
||||
<title>Unavailable</title></head>
|
||||
<body>
|
||||
<h1>Service Overloaded</h1>
|
||||
<p>${message}</p>
|
||||
</body>
|
||||
</html>
|
Reference in New Issue
Block a user