mirror of
https://github.com/MarginaliaSearch/MarginaliaSearch.git
synced 2025-10-06 07:32:38 +02:00
Compare commits
10 Commits
deploy-022
...
deploy-023
Author | SHA1 | Date | |
---|---|---|---|
|
13fb1efce4 | ||
|
c1225165b7 | ||
|
67ad7a3bbc | ||
|
ed62ec8a35 | ||
|
42b24cfa34 | ||
|
1ffaab2da6 | ||
|
5f93c7f767 | ||
|
4001c68c82 | ||
|
6b811489c5 | ||
|
e9d317c65d |
@@ -0,0 +1,6 @@
|
|||||||
|
-- Add additional summary columns to DOMAIN_SECURITY_EVENTS table
|
||||||
|
-- to make it easier to make sense of certificate changes
|
||||||
|
|
||||||
|
ALTER TABLE DOMAIN_SECURITY_EVENTS ADD COLUMN CHANGE_CERTIFICATE_SERIAL_NUMBER BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
ALTER TABLE DOMAIN_SECURITY_EVENTS ADD COLUMN CHANGE_CERTIFICATE_ISSUER BOOLEAN NOT NULL DEFAULT FALSE;
|
||||||
|
OPTIMIZE TABLE DOMAIN_SECURITY_EVENTS;
|
@@ -12,7 +12,7 @@ public enum ExecutorActor {
|
|||||||
RECRAWL(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
RECRAWL(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
||||||
RECRAWL_SINGLE_DOMAIN(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
RECRAWL_SINGLE_DOMAIN(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
||||||
PROC_CRAWLER_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
PROC_CRAWLER_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
||||||
PROC_PING_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED, NodeProfile.SIDELOAD),
|
PROC_PING_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED, NodeProfile.REALTIME),
|
||||||
PROC_EXPORT_TASKS_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
PROC_EXPORT_TASKS_SPAWNER(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
||||||
ADJACENCY_CALCULATION(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
ADJACENCY_CALCULATION(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
||||||
EXPORT_DATA(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
EXPORT_DATA(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED),
|
||||||
|
@@ -3,24 +3,184 @@ package nu.marginalia.actor.proc;
|
|||||||
import com.google.gson.Gson;
|
import com.google.gson.Gson;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
import nu.marginalia.actor.monitor.AbstractProcessSpawnerActor;
|
import nu.marginalia.actor.prototype.RecordActorPrototype;
|
||||||
|
import nu.marginalia.actor.state.ActorResumeBehavior;
|
||||||
|
import nu.marginalia.actor.state.ActorStep;
|
||||||
|
import nu.marginalia.actor.state.Resume;
|
||||||
|
import nu.marginalia.actor.state.Terminal;
|
||||||
|
import nu.marginalia.mq.MqMessageState;
|
||||||
|
import nu.marginalia.mq.persistence.MqMessageHandlerRegistry;
|
||||||
import nu.marginalia.mq.persistence.MqPersistence;
|
import nu.marginalia.mq.persistence.MqPersistence;
|
||||||
import nu.marginalia.mqapi.ProcessInboxNames;
|
import nu.marginalia.mqapi.ProcessInboxNames;
|
||||||
|
import nu.marginalia.mqapi.ping.PingRequest;
|
||||||
|
import nu.marginalia.nodecfg.NodeConfigurationService;
|
||||||
|
import nu.marginalia.nodecfg.model.NodeProfile;
|
||||||
import nu.marginalia.process.ProcessService;
|
import nu.marginalia.process.ProcessService;
|
||||||
import nu.marginalia.service.module.ServiceConfiguration;
|
import nu.marginalia.service.module.ServiceConfiguration;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import java.sql.SQLException;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.ExecutionException;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class PingMonitorActor extends AbstractProcessSpawnerActor {
|
public class PingMonitorActor extends RecordActorPrototype {
|
||||||
|
|
||||||
@Inject
|
private final MqPersistence persistence;
|
||||||
public PingMonitorActor(Gson gson, ServiceConfiguration configuration, MqPersistence persistence, ProcessService processService) {
|
private final ProcessService processService;
|
||||||
super(gson,
|
|
||||||
configuration,
|
private final Logger logger = LoggerFactory.getLogger(getClass());
|
||||||
persistence,
|
|
||||||
processService,
|
public static final int MAX_ATTEMPTS = 3;
|
||||||
ProcessInboxNames.PING_INBOX,
|
private final String inboxName;
|
||||||
ProcessService.ProcessId.PING);
|
private final ProcessService.ProcessId processId;
|
||||||
|
private final ExecutorService executorService = Executors.newSingleThreadExecutor();
|
||||||
|
private final int node;
|
||||||
|
private final boolean isPrimaryNode;
|
||||||
|
private final Gson gson;
|
||||||
|
|
||||||
|
public record Initial() implements ActorStep {}
|
||||||
|
@Resume(behavior = ActorResumeBehavior.RETRY)
|
||||||
|
public record Monitor(int errorAttempts) implements ActorStep {}
|
||||||
|
@Resume(behavior = ActorResumeBehavior.RESTART)
|
||||||
|
public record Run(int attempts) implements ActorStep {}
|
||||||
|
@Terminal
|
||||||
|
public record Aborted() implements ActorStep {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ActorStep transition(ActorStep self) throws Exception {
|
||||||
|
return switch (self) {
|
||||||
|
case Initial i -> {
|
||||||
|
PingRequest request = new PingRequest(isPrimaryNode ? "primary": "secondary");
|
||||||
|
|
||||||
|
persistence.sendNewMessage(inboxName, null, null,
|
||||||
|
"PingRequest",
|
||||||
|
gson.toJson(request),
|
||||||
|
null);
|
||||||
|
|
||||||
|
yield new Monitor(0);
|
||||||
|
}
|
||||||
|
case Monitor(int errorAttempts) -> {
|
||||||
|
for (;;) {
|
||||||
|
var messages = persistence.eavesdrop(inboxName, 1);
|
||||||
|
|
||||||
|
if (messages.isEmpty() && !processService.isRunning(processId)) {
|
||||||
|
synchronized (processId) {
|
||||||
|
processId.wait(5000);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errorAttempts > 0) { // Reset the error counter if there is silence in the inbox
|
||||||
|
yield new Monitor(0);
|
||||||
|
}
|
||||||
|
// else continue
|
||||||
|
} else {
|
||||||
|
// Special: Associate this thread with the message so that we can get tracking
|
||||||
|
MqMessageHandlerRegistry.register(messages.getFirst().msgId());
|
||||||
|
|
||||||
|
yield new Run(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case Run(int attempts) -> {
|
||||||
|
try {
|
||||||
|
long startTime = System.currentTimeMillis();
|
||||||
|
var exec = new TaskExecution();
|
||||||
|
long endTime = System.currentTimeMillis();
|
||||||
|
|
||||||
|
if (exec.isError()) {
|
||||||
|
if (attempts < MAX_ATTEMPTS)
|
||||||
|
yield new Run(attempts + 1);
|
||||||
|
else
|
||||||
|
yield new Error();
|
||||||
|
}
|
||||||
|
else if (endTime - startTime < TimeUnit.SECONDS.toMillis(1)) {
|
||||||
|
// To avoid boot loops, we transition to error if the process
|
||||||
|
// didn't run for longer than 1 seconds. This might happen if
|
||||||
|
// the process crashes before it can reach the heartbeat and inbox
|
||||||
|
// stages of execution. In this case it would not report having acted
|
||||||
|
// on its message, and the process would be restarted forever without
|
||||||
|
// the attempts counter incrementing.
|
||||||
|
yield new Error("Process terminated within 1 seconds of starting");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InterruptedException ex) {
|
||||||
|
// We get this exception when the process is cancelled by the user
|
||||||
|
|
||||||
|
processService.kill(processId);
|
||||||
|
setCurrentMessageToDead();
|
||||||
|
|
||||||
|
yield new Aborted();
|
||||||
|
}
|
||||||
|
|
||||||
|
yield new Monitor(attempts);
|
||||||
|
}
|
||||||
|
default -> new Error();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String describe() {
|
||||||
|
return "Spawns a(n) " + processId + " process and monitors its inbox for messages";
|
||||||
|
}
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
public PingMonitorActor(Gson gson,
|
||||||
|
NodeConfigurationService nodeConfigurationService,
|
||||||
|
ServiceConfiguration configuration,
|
||||||
|
MqPersistence persistence,
|
||||||
|
ProcessService processService) throws SQLException {
|
||||||
|
super(gson);
|
||||||
|
this.gson = gson;
|
||||||
|
this.node = configuration.node();
|
||||||
|
this.persistence = persistence;
|
||||||
|
this.processService = processService;
|
||||||
|
this.inboxName = ProcessInboxNames.PING_INBOX + ":" + node;
|
||||||
|
this.processId = ProcessService.ProcessId.PING;
|
||||||
|
|
||||||
|
this.isPrimaryNode = Set.of(NodeProfile.BATCH_CRAWL, NodeProfile.MIXED)
|
||||||
|
.contains(nodeConfigurationService.get(node).profile());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets the message to dead in the database to avoid
|
||||||
|
* the service respawning on the same task when we
|
||||||
|
* re-enable this actor */
|
||||||
|
private void setCurrentMessageToDead() {
|
||||||
|
try {
|
||||||
|
var messages = persistence.eavesdrop(inboxName, 1);
|
||||||
|
|
||||||
|
if (messages.isEmpty()) // Possibly a race condition where the task is already finished
|
||||||
|
return;
|
||||||
|
|
||||||
|
var theMessage = messages.iterator().next();
|
||||||
|
persistence.updateMessageState(theMessage.msgId(), MqMessageState.DEAD);
|
||||||
|
}
|
||||||
|
catch (SQLException ex) {
|
||||||
|
logger.error("Tried but failed to set the message for " + processId + " to dead", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Encapsulates the execution of the process in a separate thread so that
|
||||||
|
* we can interrupt the thread if the process is cancelled */
|
||||||
|
private class TaskExecution {
|
||||||
|
private final AtomicBoolean error = new AtomicBoolean(false);
|
||||||
|
public TaskExecution() throws ExecutionException, InterruptedException {
|
||||||
|
// Run this call in a separate thread so that this thread can be interrupted waiting for it
|
||||||
|
executorService.submit(() -> {
|
||||||
|
try {
|
||||||
|
processService.trigger(processId);
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.warn("Error in triggering process", e);
|
||||||
|
error.set(true);
|
||||||
|
}
|
||||||
|
}).get(); // Wait for the process to start
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isError() {
|
||||||
|
return error.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -27,10 +27,12 @@ public class DbBrowseDomainsRandom {
|
|||||||
public List<BrowseResult> getRandomDomains(int count, DomainBlacklist blacklist, int set) {
|
public List<BrowseResult> getRandomDomains(int count, DomainBlacklist blacklist, int set) {
|
||||||
|
|
||||||
final String q = """
|
final String q = """
|
||||||
SELECT DOMAIN_ID, DOMAIN_NAME, INDEXED
|
SELECT EC_RANDOM_DOMAINS.DOMAIN_ID, DOMAIN_NAME, INDEXED
|
||||||
FROM EC_RANDOM_DOMAINS
|
FROM EC_RANDOM_DOMAINS
|
||||||
INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID
|
INNER JOIN EC_DOMAIN ON EC_DOMAIN.ID=DOMAIN_ID
|
||||||
|
LEFT JOIN DOMAIN_AVAILABILITY_INFORMATION DAI ON DAI.DOMAIN_ID=EC_RANDOM_DOMAINS.DOMAIN_ID
|
||||||
WHERE STATE<2
|
WHERE STATE<2
|
||||||
|
AND SERVER_AVAILABLE
|
||||||
AND DOMAIN_SET=?
|
AND DOMAIN_SET=?
|
||||||
AND DOMAIN_ALIAS IS NULL
|
AND DOMAIN_ALIAS IS NULL
|
||||||
ORDER BY RAND()
|
ORDER BY RAND()
|
||||||
|
@@ -137,7 +137,7 @@ public class PingDao {
|
|||||||
|
|
||||||
public HistoricalAvailabilityData getHistoricalAvailabilityData(long domainId) throws SQLException {
|
public HistoricalAvailabilityData getHistoricalAvailabilityData(long domainId) throws SQLException {
|
||||||
var query = """
|
var query = """
|
||||||
SELECT EC_DOMAIN.ID, EC_DOMAIN.DOMAIN_NAME, DOMAIN_AVAILABILITY_INFORMATION.*, DOMAIN_SECURITY_INFORMATION.*
|
SELECT EC_DOMAIN.ID, EC_DOMAIN.DOMAIN_NAME, EC_DOMAIN.NODE_AFFINITY, DOMAIN_AVAILABILITY_INFORMATION.*, DOMAIN_SECURITY_INFORMATION.*
|
||||||
FROM EC_DOMAIN
|
FROM EC_DOMAIN
|
||||||
LEFT JOIN DOMAIN_SECURITY_INFORMATION ON DOMAIN_SECURITY_INFORMATION.DOMAIN_ID = EC_DOMAIN.ID
|
LEFT JOIN DOMAIN_SECURITY_INFORMATION ON DOMAIN_SECURITY_INFORMATION.DOMAIN_ID = EC_DOMAIN.ID
|
||||||
LEFT JOIN DOMAIN_AVAILABILITY_INFORMATION ON DOMAIN_AVAILABILITY_INFORMATION.DOMAIN_ID = EC_DOMAIN.ID
|
LEFT JOIN DOMAIN_AVAILABILITY_INFORMATION ON DOMAIN_AVAILABILITY_INFORMATION.DOMAIN_ID = EC_DOMAIN.ID
|
||||||
@@ -168,7 +168,7 @@ public class PingDao {
|
|||||||
if (dar == null) {
|
if (dar == null) {
|
||||||
return new HistoricalAvailabilityData.JustDomainReference(new DomainReference(
|
return new HistoricalAvailabilityData.JustDomainReference(new DomainReference(
|
||||||
rs.getInt("EC_DOMAIN.ID"),
|
rs.getInt("EC_DOMAIN.ID"),
|
||||||
rs.getInt("EC_DOMAIN.NODE_ID"),
|
rs.getInt("EC_DOMAIN.NODE_AFFINITY"),
|
||||||
domainName.toLowerCase()
|
domainName.toLowerCase()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,7 @@ import nu.marginalia.ping.svc.HttpPingService;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -60,11 +61,13 @@ public class PingJobScheduler {
|
|||||||
allThreads.add(Thread.ofPlatform().daemon().name("sync-dns").start(this::syncAvailabilityJobs));
|
allThreads.add(Thread.ofPlatform().daemon().name("sync-dns").start(this::syncAvailabilityJobs));
|
||||||
allThreads.add(Thread.ofPlatform().daemon().name("sync-availability").start(this::syncDnsRecords));
|
allThreads.add(Thread.ofPlatform().daemon().name("sync-availability").start(this::syncDnsRecords));
|
||||||
|
|
||||||
|
int availabilityThreads = Integer.getInteger("ping.availabilityThreads", 8);
|
||||||
|
int pingThreads = Integer.getInteger("ping.dnsThreads", 2);
|
||||||
|
|
||||||
for (int i = 0; i < 8; i++) {
|
for (int i = 0; i < availabilityThreads; i++) {
|
||||||
allThreads.add(Thread.ofPlatform().daemon().name("availability-job-consumer-" + i).start(this::availabilityJobConsumer));
|
allThreads.add(Thread.ofPlatform().daemon().name("availability-job-consumer-" + i).start(this::availabilityJobConsumer));
|
||||||
}
|
}
|
||||||
for (int i = 0; i < 1; i++) {
|
for (int i = 0; i < pingThreads; i++) {
|
||||||
allThreads.add(Thread.ofPlatform().daemon().name("dns-job-consumer-" + i).start(this::dnsJobConsumer));
|
allThreads.add(Thread.ofPlatform().daemon().name("dns-job-consumer-" + i).start(this::dnsJobConsumer));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,6 +86,8 @@ public class PingJobScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void pause(int nodeId) {
|
public void pause(int nodeId) {
|
||||||
|
logger.info("Pausing PingJobScheduler for nodeId: {}", nodeId);
|
||||||
|
|
||||||
if (this.nodeId != null && this.nodeId != nodeId) {
|
if (this.nodeId != null && this.nodeId != nodeId) {
|
||||||
logger.warn("Attempted to pause PingJobScheduler with mismatched nodeId: expected {}, got {}", this.nodeId, nodeId);
|
logger.warn("Attempted to pause PingJobScheduler with mismatched nodeId: expected {}, got {}", this.nodeId, nodeId);
|
||||||
return;
|
return;
|
||||||
@@ -96,6 +101,7 @@ public class PingJobScheduler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void resume(int nodeId) {
|
public synchronized void resume(int nodeId) {
|
||||||
|
logger.info("Resuming PingJobScheduler for nodeId: {}", nodeId);
|
||||||
if (this.nodeId != null) {
|
if (this.nodeId != null) {
|
||||||
logger.warn("Attempted to resume PingJobScheduler with mismatched nodeId: expected {}, got {}", this.nodeId, nodeId);
|
logger.warn("Attempted to resume PingJobScheduler with mismatched nodeId: expected {}, got {}", this.nodeId, nodeId);
|
||||||
return;
|
return;
|
||||||
@@ -137,24 +143,14 @@ public class PingJobScheduler {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
List<WritableModel> objects = switch (data) {
|
List<WritableModel> objects = switch (data) {
|
||||||
case HistoricalAvailabilityData.JustDomainReference(DomainReference reference) -> {
|
case HistoricalAvailabilityData.JustDomainReference(DomainReference reference)
|
||||||
logger.info("Processing availability job for domain: {}", reference.domainName());
|
-> httpPingService.pingDomain(reference, null, null);
|
||||||
yield httpPingService.pingDomain(reference, null, null);
|
case HistoricalAvailabilityData.JustAvailability(String domain, DomainAvailabilityRecord record)
|
||||||
}
|
-> httpPingService.pingDomain(
|
||||||
case HistoricalAvailabilityData.JustAvailability(String domain, DomainAvailabilityRecord record) -> {
|
new DomainReference(record.domainId(), record.nodeId(), domain), record, null);
|
||||||
logger.info("Availability check with no security info: {}", domain);
|
case HistoricalAvailabilityData.AvailabilityAndSecurity(String domain, DomainAvailabilityRecord availability, DomainSecurityRecord security)
|
||||||
yield httpPingService.pingDomain(
|
-> httpPingService.pingDomain(
|
||||||
new DomainReference(record.domainId(), record.nodeId(), domain),
|
new DomainReference(availability.domainId(), availability.nodeId(), domain), availability, security);
|
||||||
record,
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
case HistoricalAvailabilityData.AvailabilityAndSecurity(String domain, DomainAvailabilityRecord availability, DomainSecurityRecord security) -> {
|
|
||||||
logger.info("Availability check with full historical data: {}", domain);
|
|
||||||
yield httpPingService.pingDomain(
|
|
||||||
new DomainReference(availability.domainId(), availability.nodeId(), domain),
|
|
||||||
availability,
|
|
||||||
security);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pingDao.write(objects);
|
pingDao.write(objects);
|
||||||
@@ -199,8 +195,8 @@ public class PingJobScheduler {
|
|||||||
yield dnsPingService.pingDomain(oldRecord.rootDomainName(), oldRecord);
|
yield dnsPingService.pingDomain(oldRecord.rootDomainName(), oldRecord);
|
||||||
}
|
}
|
||||||
case RootDomainReference.ByName(String name) -> {
|
case RootDomainReference.ByName(String name) -> {
|
||||||
var oldRecord = pingDao.getDomainDnsRecord(name);
|
@Nullable var oldRecord = pingDao.getDomainDnsRecord(name);
|
||||||
yield dnsPingService.pingDomain(oldRecord.rootDomainName(), oldRecord);
|
yield dnsPingService.pingDomain(name, oldRecord);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -83,7 +83,7 @@ public class PingHttpFetcher {
|
|||||||
} catch (SocketTimeoutException ex) {
|
} catch (SocketTimeoutException ex) {
|
||||||
return new TimeoutResponse(ex.getMessage());
|
return new TimeoutResponse(ex.getMessage());
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
return new ConnectionError(e.getMessage());
|
return new ConnectionError(e.getClass().getSimpleName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -16,6 +16,8 @@ public record DomainSecurityEvent(
|
|||||||
boolean certificateProfileChanged,
|
boolean certificateProfileChanged,
|
||||||
boolean certificateSanChanged,
|
boolean certificateSanChanged,
|
||||||
boolean certificatePublicKeyChanged,
|
boolean certificatePublicKeyChanged,
|
||||||
|
boolean certificateSerialNumberChanged,
|
||||||
|
boolean certificateIssuerChanged,
|
||||||
Duration oldCertificateTimeToExpiry,
|
Duration oldCertificateTimeToExpiry,
|
||||||
boolean securityHeadersChanged,
|
boolean securityHeadersChanged,
|
||||||
boolean ipChanged,
|
boolean ipChanged,
|
||||||
@@ -41,8 +43,10 @@ public record DomainSecurityEvent(
|
|||||||
change_software,
|
change_software,
|
||||||
old_cert_time_to_expiry,
|
old_cert_time_to_expiry,
|
||||||
security_signature_before,
|
security_signature_before,
|
||||||
security_signature_after
|
security_signature_after,
|
||||||
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
change_certificate_serial_number,
|
||||||
|
change_certificate_issuer
|
||||||
|
) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||||
"""))
|
"""))
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -75,6 +79,9 @@ public record DomainSecurityEvent(
|
|||||||
ps.setBytes(14, securitySignatureAfter().compressed());
|
ps.setBytes(14, securitySignatureAfter().compressed());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ps.setBoolean(15, certificateSerialNumberChanged());
|
||||||
|
ps.setBoolean(16, certificateIssuerChanged());
|
||||||
|
|
||||||
ps.executeUpdate();
|
ps.executeUpdate();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,8 @@ public record SecurityInformationChange(
|
|||||||
boolean isCertificateProfileChanged,
|
boolean isCertificateProfileChanged,
|
||||||
boolean isCertificateSanChanged,
|
boolean isCertificateSanChanged,
|
||||||
boolean isCertificatePublicKeyChanged,
|
boolean isCertificatePublicKeyChanged,
|
||||||
|
boolean isCertificateSerialNumberChanged,
|
||||||
|
boolean isCertificateIssuerChanged,
|
||||||
Duration oldCertificateTimeToExpiry,
|
Duration oldCertificateTimeToExpiry,
|
||||||
boolean isSecurityHeadersChanged,
|
boolean isSecurityHeadersChanged,
|
||||||
boolean isIpAddressChanged,
|
boolean isIpAddressChanged,
|
||||||
@@ -30,8 +32,10 @@ public record SecurityInformationChange(
|
|||||||
|
|
||||||
boolean certificateFingerprintChanged = 0 != Arrays.compare(before.sslCertFingerprintSha256(), after.sslCertFingerprintSha256());
|
boolean certificateFingerprintChanged = 0 != Arrays.compare(before.sslCertFingerprintSha256(), after.sslCertFingerprintSha256());
|
||||||
boolean certificateProfileChanged = before.certificateProfileHash() != after.certificateProfileHash();
|
boolean certificateProfileChanged = before.certificateProfileHash() != after.certificateProfileHash();
|
||||||
|
boolean certificateSerialNumberChanged = !Objects.equals(before.sslCertSerialNumber(), after.sslCertSerialNumber());
|
||||||
boolean certificatePublicKeyChanged = 0 != Arrays.compare(before.sslCertPublicKeyHash(), after.sslCertPublicKeyHash());
|
boolean certificatePublicKeyChanged = 0 != Arrays.compare(before.sslCertPublicKeyHash(), after.sslCertPublicKeyHash());
|
||||||
boolean certificateSanChanged = !Objects.equals(before.sslCertSan(), after.sslCertSan());
|
boolean certificateSanChanged = !Objects.equals(before.sslCertSan(), after.sslCertSan());
|
||||||
|
boolean certificateIssuerChanged = !Objects.equals(before.sslCertIssuer(), after.sslCertIssuer());
|
||||||
|
|
||||||
Duration oldCertificateTimeToExpiry = before.sslCertNotAfter() == null ? null : Duration.between(
|
Duration oldCertificateTimeToExpiry = before.sslCertNotAfter() == null ? null : Duration.between(
|
||||||
Instant.now(),
|
Instant.now(),
|
||||||
@@ -50,6 +54,7 @@ public record SecurityInformationChange(
|
|||||||
boolean isChanged = asnChanged
|
boolean isChanged = asnChanged
|
||||||
|| certificateFingerprintChanged
|
|| certificateFingerprintChanged
|
||||||
|| securityHeadersChanged
|
|| securityHeadersChanged
|
||||||
|
|| certificateProfileChanged
|
||||||
|| softwareChanged;
|
|| softwareChanged;
|
||||||
|
|
||||||
return new SecurityInformationChange(
|
return new SecurityInformationChange(
|
||||||
@@ -59,6 +64,8 @@ public record SecurityInformationChange(
|
|||||||
certificateProfileChanged,
|
certificateProfileChanged,
|
||||||
certificateSanChanged,
|
certificateSanChanged,
|
||||||
certificatePublicKeyChanged,
|
certificatePublicKeyChanged,
|
||||||
|
certificateSerialNumberChanged,
|
||||||
|
certificateIssuerChanged,
|
||||||
oldCertificateTimeToExpiry,
|
oldCertificateTimeToExpiry,
|
||||||
securityHeadersChanged,
|
securityHeadersChanged,
|
||||||
ipChanged,
|
ipChanged,
|
||||||
|
@@ -48,7 +48,6 @@ public class DnsPingService {
|
|||||||
switch (changes) {
|
switch (changes) {
|
||||||
case DnsRecordChange.None _ -> {}
|
case DnsRecordChange.None _ -> {}
|
||||||
case DnsRecordChange.Changed changed -> {
|
case DnsRecordChange.Changed changed -> {
|
||||||
logger.info("DNS record for {} changed: {}", newRecord.dnsRootDomainId(), changed);
|
|
||||||
generatedRecords.add(DomainDnsEvent.builder()
|
generatedRecords.add(DomainDnsEvent.builder()
|
||||||
.rootDomainId(newRecord.dnsRootDomainId())
|
.rootDomainId(newRecord.dnsRootDomainId())
|
||||||
.nodeId(newRecord.nodeAffinity())
|
.nodeId(newRecord.nodeAffinity())
|
||||||
|
@@ -8,6 +8,7 @@ import nu.marginalia.ping.ssl.PKIXValidationResult;
|
|||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
import javax.annotation.Nullable;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
import java.security.NoSuchAlgorithmException;
|
import java.security.NoSuchAlgorithmException;
|
||||||
import java.security.cert.CertificateEncodingException;
|
import java.security.cert.CertificateEncodingException;
|
||||||
@@ -21,13 +22,17 @@ public class DomainSecurityInformationFactory {
|
|||||||
private static final Logger logger = LoggerFactory.getLogger(DomainSecurityInformationFactory.class);
|
private static final Logger logger = LoggerFactory.getLogger(DomainSecurityInformationFactory.class);
|
||||||
|
|
||||||
// Vanilla HTTP (not HTTPS) response does not have SSL session information, so we return null
|
// Vanilla HTTP (not HTTPS) response does not have SSL session information, so we return null
|
||||||
public DomainSecurityRecord createHttpSecurityInformation(HttpResponse httpResponse, int domainId, int nodeId) {
|
public DomainSecurityRecord createHttpSecurityInformation(HttpResponse httpResponse,
|
||||||
|
int domainId, int nodeId,
|
||||||
|
@Nullable Integer asn
|
||||||
|
) {
|
||||||
|
|
||||||
var headers = httpResponse.headers();
|
var headers = httpResponse.headers();
|
||||||
|
|
||||||
return DomainSecurityRecord.builder()
|
return DomainSecurityRecord.builder()
|
||||||
.domainId(domainId)
|
.domainId(domainId)
|
||||||
.nodeId(nodeId)
|
.nodeId(nodeId)
|
||||||
|
.asn(asn)
|
||||||
.httpSchema(HttpSchema.HTTP)
|
.httpSchema(HttpSchema.HTTP)
|
||||||
.httpVersion(httpResponse.version())
|
.httpVersion(httpResponse.version())
|
||||||
.headerServer(headers.getFirst("Server"))
|
.headerServer(headers.getFirst("Server"))
|
||||||
@@ -47,7 +52,13 @@ public class DomainSecurityInformationFactory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HTTPS response
|
// HTTPS response
|
||||||
public DomainSecurityRecord createHttpsSecurityInformation(HttpsResponse httpResponse, PKIXValidationResult validationResult, int domainId, int nodeId) {
|
public DomainSecurityRecord createHttpsSecurityInformation(
|
||||||
|
HttpsResponse httpResponse,
|
||||||
|
PKIXValidationResult validationResult,
|
||||||
|
int domainId,
|
||||||
|
int nodeId,
|
||||||
|
@Nullable Integer asn
|
||||||
|
) {
|
||||||
|
|
||||||
|
|
||||||
var headers = httpResponse.headers();
|
var headers = httpResponse.headers();
|
||||||
@@ -86,6 +97,7 @@ public class DomainSecurityInformationFactory {
|
|||||||
return DomainSecurityRecord.builder()
|
return DomainSecurityRecord.builder()
|
||||||
.domainId(domainId)
|
.domainId(domainId)
|
||||||
.nodeId(nodeId)
|
.nodeId(nodeId)
|
||||||
|
.asn(asn)
|
||||||
.httpSchema(HttpSchema.HTTPS)
|
.httpSchema(HttpSchema.HTTPS)
|
||||||
.headerServer(headers.getFirst("Server"))
|
.headerServer(headers.getFirst("Server"))
|
||||||
.headerCorsAllowOrigin(headers.getFirst("Access-Control-Allow-Origin"))
|
.headerCorsAllowOrigin(headers.getFirst("Access-Control-Allow-Origin"))
|
||||||
|
@@ -18,6 +18,7 @@ import java.net.InetAddress;
|
|||||||
import java.net.UnknownHostException;
|
import java.net.UnknownHostException;
|
||||||
import java.security.cert.X509Certificate;
|
import java.security.cert.X509Certificate;
|
||||||
import java.sql.SQLException;
|
import java.sql.SQLException;
|
||||||
|
import java.time.Duration;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -75,8 +76,8 @@ public class HttpPingService {
|
|||||||
|
|
||||||
result = pingHttpFetcher.fetchUrl(url, Method.HEAD, null, null);
|
result = pingHttpFetcher.fetchUrl(url, Method.HEAD, null, null);
|
||||||
|
|
||||||
if (result instanceof HttpsResponse response && response.httpStatus() == 405) {
|
if (result instanceof HttpsResponse response && shouldTryGET(response.httpStatus())) {
|
||||||
// If we get a 405, we try the GET method instead as not all servers support HEAD requests
|
sleep(Duration.ofSeconds(2));
|
||||||
result = pingHttpFetcher.fetchUrl(url, Method.GET, null, null);
|
result = pingHttpFetcher.fetchUrl(url, Method.GET, null, null);
|
||||||
}
|
}
|
||||||
else if (result instanceof ConnectionError) {
|
else if (result instanceof ConnectionError) {
|
||||||
@@ -84,8 +85,8 @@ public class HttpPingService {
|
|||||||
if (!(result2 instanceof ConnectionError)) {
|
if (!(result2 instanceof ConnectionError)) {
|
||||||
result = result2;
|
result = result2;
|
||||||
}
|
}
|
||||||
if (result instanceof HttpResponse response && response.httpStatus() == 405) {
|
if (result instanceof HttpResponse response && shouldTryGET(response.httpStatus())) {
|
||||||
// If we get a 405, we try the GET method instead as not all servers support HEAD requests
|
sleep(Duration.ofSeconds(2));
|
||||||
result = pingHttpFetcher.fetchUrl(alternateUrl, Method.GET, null, null);
|
result = pingHttpFetcher.fetchUrl(alternateUrl, Method.GET, null, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ public class HttpPingService {
|
|||||||
domainReference.nodeId(),
|
domainReference.nodeId(),
|
||||||
oldPingStatus,
|
oldPingStatus,
|
||||||
ErrorClassification.CONNECTION_ERROR,
|
ErrorClassification.CONNECTION_ERROR,
|
||||||
null);
|
rsp.errorMessage());
|
||||||
newSecurityInformation = null;
|
newSecurityInformation = null;
|
||||||
}
|
}
|
||||||
case TimeoutResponse rsp -> {
|
case TimeoutResponse rsp -> {
|
||||||
@@ -148,7 +149,8 @@ public class HttpPingService {
|
|||||||
newSecurityInformation = domainSecurityInformationFactory.createHttpSecurityInformation(
|
newSecurityInformation = domainSecurityInformationFactory.createHttpSecurityInformation(
|
||||||
httpResponse,
|
httpResponse,
|
||||||
domainReference.domainId(),
|
domainReference.domainId(),
|
||||||
domainReference.nodeId()
|
domainReference.nodeId(),
|
||||||
|
newPingStatus.asn()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
case HttpsResponse httpsResponse -> {
|
case HttpsResponse httpsResponse -> {
|
||||||
@@ -166,7 +168,8 @@ public class HttpPingService {
|
|||||||
httpsResponse,
|
httpsResponse,
|
||||||
validationResult,
|
validationResult,
|
||||||
domainReference.domainId(),
|
domainReference.domainId(),
|
||||||
domainReference.nodeId()
|
domainReference.nodeId(),
|
||||||
|
newPingStatus.asn()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,6 +193,29 @@ public class HttpPingService {
|
|||||||
return generatedRecords;
|
return generatedRecords;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean shouldTryGET(int statusCode) {
|
||||||
|
if (statusCode < 400) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (statusCode == 429) { // Too many requests, we should not retry with GET
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all other status codes, we can try a GET request, as many severs do not
|
||||||
|
// cope with HEAD requests properly.
|
||||||
|
|
||||||
|
return statusCode < 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sleep(Duration duration) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(duration.toMillis());
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt(); // Restore the interrupted status
|
||||||
|
logger.warn("Sleep interrupted", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void comparePingStatuses(List<WritableModel> generatedRecords,
|
private void comparePingStatuses(List<WritableModel> generatedRecords,
|
||||||
DomainAvailabilityRecord oldPingStatus,
|
DomainAvailabilityRecord oldPingStatus,
|
||||||
DomainAvailabilityRecord newPingStatus) {
|
DomainAvailabilityRecord newPingStatus) {
|
||||||
@@ -258,6 +284,8 @@ public class HttpPingService {
|
|||||||
change.isCertificateProfileChanged(),
|
change.isCertificateProfileChanged(),
|
||||||
change.isCertificateSanChanged(),
|
change.isCertificateSanChanged(),
|
||||||
change.isCertificatePublicKeyChanged(),
|
change.isCertificatePublicKeyChanged(),
|
||||||
|
change.isCertificateSerialNumberChanged(),
|
||||||
|
change.isCertificateIssuerChanged(),
|
||||||
change.oldCertificateTimeToExpiry(),
|
change.oldCertificateTimeToExpiry(),
|
||||||
change.isSecurityHeadersChanged(),
|
change.isSecurityHeadersChanged(),
|
||||||
change.isIpAddressChanged(),
|
change.isIpAddressChanged(),
|
||||||
|
@@ -318,6 +318,8 @@ class PingDaoTest {
|
|||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
Duration.ofDays(30),
|
Duration.ofDays(30),
|
||||||
false,
|
false,
|
||||||
false,
|
false,
|
||||||
|
Reference in New Issue
Block a user