}
- public static final int CURRENT_SCHEMA_VERSION = 34;
+ public static final int CURRENT_SCHEMA_VERSION = 35;
public static final int CONNECTION_TIMEOUT = 24 * 60 * 60;
"configId" int NOT NULL,
"state" "pingState" NOT NULL,
"challenge" varchar(16),
- "result" varchar(255)
+ "result" varchar(255),
+ "needsAction" boolean DEFAULT false
);
CREATE INDEX ON "domainPinglog" ("configId","when");
+CREATE INDEX ON "domainPinglog" ("when", "needsAction");
DROP TABLE IF EXISTS "baddomains";
CREATE TABLE "baddomains" (
"version" smallint NOT NULL,
PRIMARY KEY ("version")
);
-INSERT INTO "schemeVersion" (version) VALUES(34);
+INSERT INTO "schemeVersion" (version) VALUES(35);
DROP TABLE IF EXISTS `passwordResetTickets`;
CREATE TABLE `passwordResetTickets` (
--- /dev/null
+ALTER TABLE "domainPinglog" ADD COLUMN "needsAction" boolean DEFAULT false;
+CREATE INDEX ON "domainPinglog" ("when", "needsAction");
private LinkedList<DomainPingConfiguration> configs = null;
- public List<DomainPingConfiguration> getConfiguredPings() throws GigiApiException {
+ public List<DomainPingConfiguration> getConfiguredPings() {
LinkedList<DomainPingConfiguration> configs = this.configs;
if (configs == null) {
configs = new LinkedList<>();
}
}
+ /**
+ * Determines current domain validity. A domain is valid, iff at least two
+ * configured pings are currently successful.
+ *
+ * @return true, iff domain is valid
+ * @throws GigiApiException
+ */
public boolean isVerified() {
- try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `domainid`=? AND `state`='success'")) {
- ps.setInt(1, id);
- GigiResultSet rs = ps.executeQuery();
- return rs.next();
+ int count = 0;
+ boolean[] used = new boolean[DomainPingType.values().length];
+ for (DomainPingConfiguration config : getConfiguredPings()) {
+ if (config.isValid() && !used[config.getType().ordinal()]) {
+ count++;
+ used[config.getType().ordinal()] = true;
+ }
+ if (count >= 2) {
+ return true;
+ }
}
+ return false;
}
public DomainPingExecution[] getPings() throws GigiApiException {
}
}
+ public Certificate[] fetchActiveCertificates() {
+ try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `certs`.`id` FROM `certs` INNER JOIN `subjectAlternativeNames` ON `subjectAlternativeNames`.`certId` = `certs`.`id` WHERE (`contents`=? OR RIGHT(`contents`,LENGTH(?)+1)=CONCAT('.',?::VARCHAR)) AND `type`='DNS' AND `revoked` IS NULL AND `expire` > CURRENT_TIMESTAMP AND `memid`=? GROUP BY `certs`.`id`", true)) {
+ ps.setString(1, suffix);
+ ps.setString(2, suffix);
+ ps.setString(3, suffix);
+ ps.setInt(4, owner.getId());
+ GigiResultSet rs = ps.executeQuery();
+ rs.last();
+ Certificate[] res = new Certificate[rs.getRow()];
+ rs.beforeFirst();
+ int i = 0;
+ while (rs.next()) {
+ res[i++] = Certificate.getById(rs.getInt(1));
+ }
+ return res;
+ }
+ }
+
}
package club.wpia.gigi.dbObjects;
+import java.sql.Timestamp;
import java.util.Date;
import club.wpia.gigi.Gigi;
}
throw new GigiApiException(SprintfCommand.createSimple("Reping is only allowed after {0} minutes, yours end at {1}.", REPING_MINIMUM_DELAY / 60 / 1000, new Date(lastExecution.getTime() + REPING_MINIMUM_DELAY)));
}
+
+ /**
+ * Return true when there was a last execution and it succeeded.
+ *
+ * @return if this ping is currently valid.
+ */
+ public boolean isValid() {
+ try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT state='success' AS bool from `domainPinglog` WHERE `configId`=? ORDER BY `when` DESC LIMIT 1")) {
+ ps.setInt(1, id);
+ GigiResultSet rs = ps.executeQuery();
+ if ( !rs.next()) {
+ return false;
+ }
+ return rs.getBoolean(1);
+ }
+ }
+
+ /**
+ * Return true when this ping has not been successful within the last 2
+ * weeks.
+ *
+ * @param time
+ * the point in time for which the determination is carried out.
+ * @return the value for this ping.
+ */
+ public boolean isStrictlyInvalid(Date time) {
+ Date lastSuccess = getLastSuccess();
+ if (lastSuccess.getTime() == 0) {
+ // never a successful ping
+ return true;
+ }
+ try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `when` AS stamp from `domainPinglog` WHERE `configId`=? AND state='failed' AND `when` > ? ORDER BY `when` ASC LIMIT 1")) {
+ ps.setInt(1, id);
+ ps.setTimestamp(2, new Timestamp(lastSuccess.getTime()));
+ GigiResultSet rs = ps.executeQuery();
+ if (rs.next()) {
+ Date turnedInvalid = new Date(rs.getTimestamp("stamp").getTime());
+ // turned invalid older than 2 weeks ago
+ return turnedInvalid.getTime() < time.getTime() - 2L * 7 * 24 * 60 * 60 * 1000;
+ }
+ return false;
+ }
+ }
}
import java.sql.Timestamp;
import java.util.Date;
+import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.database.GigiResultSet;
+import club.wpia.gigi.ping.DomainPinger.PingState;
public class DomainPingExecution {
- private String state;
+ private final PingState state;
- private String type;
+ private final String type;
- private String info;
+ private final String info;
- private String result;
+ private final String result;
- private DomainPingConfiguration config;
+ private final DomainPingConfiguration config;
- private Timestamp date;
+ private final Timestamp date;
- public DomainPingExecution(GigiResultSet rs) {
- state = rs.getString(1);
+ protected DomainPingExecution(GigiResultSet rs) {
+ state = PingState.valueOf(rs.getString(1).toUpperCase());
type = rs.getString(2);
info = rs.getString(3);
result = rs.getString(4);
date = rs.getTimestamp(6);
}
- public String getState() {
+ public DomainPingExecution(PingState state, String result, DomainPingConfiguration config, String challenge) {
+ this.state = state;
+ this.type = config.getType().getDBName();
+ this.info = config.getInfo();
+ this.result = result;
+ this.config = config;
+ this.date = new Timestamp(System.currentTimeMillis());
+ try (GigiPreparedStatement enterPingResult = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=?, `state`=?::`pingState`, `result`=?, `challenge`=?, `when`=?, `needsAction`=?")) {
+ enterPingResult.setInt(1, config.getId());
+ enterPingResult.setEnum(2, state);
+ enterPingResult.setString(3, result);
+ enterPingResult.setString(4, challenge);
+ enterPingResult.setTimestamp(5, this.date);
+ // Ping results with current state "failed" need followup action in
+ // two weeks to revoke any remaining active certificates.
+ enterPingResult.setBoolean(6, state == PingState.FAILED);
+ enterPingResult.execute();
+ }
+ }
+
+ public PingState getState() {
return state;
}
out.print(((Boolean) s) ? l.getTranslation("yes") : l.getTranslation("no"));
} else if (s instanceof Date) {
SimpleDateFormat sdfUI = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
- SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
- out.print("<time datetime=\"" + sdf.format(s) + "\">");
- out.print(sdfUI.format(s));
- out.print(" UTC</time>");
+ if (vars.containsKey(Outputable.OUT_KEY_PLAIN)) {
+ out.print(sdfUI.format(s));
+ } else {
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'");
+ out.print("<time datetime=\"" + sdf.format(s) + "\">");
+ out.print(sdfUI.format(s));
+ out.print(" UTC</time>");
+ }
} else {
out.print(s == null ? "null" : (unescaped ? s.toString() : HTMLEncoder.encodeHTML(s.toString())));
}
if (counter >= pings.length) {
return false;
}
- vars.put("state", pings[counter].getState());
+ vars.put("state", pings[counter].getState().getDBName());
vars.put("type", pings[counter].getType());
vars.put("config", pings[counter].getInfo());
vars.put("date", pings[counter].getDate());
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
import club.wpia.gigi.util.DNSUtil;
import club.wpia.gigi.util.SystemKeywords;
public class DNSPinger extends DomainPinger {
@Override
- public void ping(Domain domain, String expToken, CertificateOwner u, int confId) {
+ public DomainPingExecution ping(Domain domain, String expToken, CertificateOwner u, DomainPingConfiguration conf) {
String[] tokenParts = expToken.split(":", 2);
List<String> nameservers;
try {
nameservers = Arrays.asList(DNSUtil.getNSNames(domain.getSuffix()));
} catch (NamingException e) {
- enterPingResult(confId, "error", "No authorative nameserver found.", null);
- return;
+ return enterPingResult(conf, "error", "No authorative nameserver found.", null);
}
StringBuffer result = new StringBuffer();
result.append("failed: ");
}
if ( !failed) {
- enterPingResult(confId, PING_SUCCEDED, "", null);
+ return enterPingResult(conf, PING_SUCCEDED, "", null);
} else {
- enterPingResult(confId, "error", result.toString(), null);
+ return enterPingResult(conf, "error", result.toString(), null);
}
}
}
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
public abstract class DomainPinger {
public static final String PING_SUCCEDED = "";
- public abstract void ping(Domain domain, String configuration, CertificateOwner target, int confId);
+ public abstract DomainPingExecution ping(Domain domain, String configuration, CertificateOwner target, DomainPingConfiguration conf);
- protected static void enterPingResult(int configId, String state, String result, String token) {
+ protected static DomainPingExecution enterPingResult(DomainPingConfiguration config, String state, String result, String token) {
PingState estate = DomainPinger.PING_STILL_PENDING == state ? PingState.OPEN : DomainPinger.PING_SUCCEDED.equals(state) ? PingState.SUCCESS : PingState.FAILED;
- try (GigiPreparedStatement enterPingResult = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=?, `state`=?::`pingState`, `result`=?, `challenge`=?")) {
- enterPingResult.setInt(1, configId);
- enterPingResult.setEnum(2, estate);
- enterPingResult.setString(3, result);
- enterPingResult.setString(4, token);
- enterPingResult.execute();
- }
+ return new DomainPingExecution(estate, result, config, token);
}
- protected static void updatePingResult(int configId, String state, String result, String token) {
+ protected static void updatePingResult(DomainPingConfiguration config, String state, String result, String token) {
try (GigiPreparedStatement updatePingResult = new GigiPreparedStatement("UPDATE `domainPinglog` SET `state`=?::`pingState`, `result`=? WHERE `configId`=? AND `challenge`=?")) {
updatePingResult.setString(1, DomainPinger.PING_STILL_PENDING == state ? "open" : DomainPinger.PING_SUCCEDED.equals(state) ? "success" : "failed");
updatePingResult.setString(2, result);
- updatePingResult.setInt(3, configId);
+ updatePingResult.setInt(3, config.getId());
updatePingResult.setString(4, token);
updatePingResult.execute();
}
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
import club.wpia.gigi.dbObjects.User;
import club.wpia.gigi.email.MailProbe;
import club.wpia.gigi.localisation.Language;
public class EmailPinger extends DomainPinger {
@Override
- public void ping(Domain domain, String configuration, CertificateOwner u, int confId) {
+ public DomainPingExecution ping(Domain domain, String configuration, CertificateOwner u, DomainPingConfiguration conf) {
String mail = configuration + "@" + domain.getSuffix();
String token = RandomToken.generateToken(16);
+ DomainPingExecution r = enterPingResult(conf, PING_STILL_PENDING, "", token);
try {
- enterPingResult(confId, PING_STILL_PENDING, "", token);
Locale l = Locale.ENGLISH;
if (u instanceof User) {
l = ((User) u).getPreferredLocale();
MailProbe.sendMailProbe(Language.getInstance(l), "domain", domain.getId(), token, mail);
} catch (IOException e) {
e.printStackTrace();
- updatePingResult(confId, "error", "Mail connection interrupted", token);
+ updatePingResult(conf, "error", "Mail connection interrupted", token);
}
+ return r;
}
}
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
import club.wpia.gigi.util.SystemKeywords;
public class HTTPFetch extends DomainPinger {
@Override
- public void ping(Domain domain, String expToken, CertificateOwner user, int confId) {
+ public DomainPingExecution ping(Domain domain, String expToken, CertificateOwner user, DomainPingConfiguration conf) {
try {
String[] tokenParts = expToken.split(":", 2);
URL u = new URL("http://" + domain.getSuffix() + "/" + SystemKeywords.HTTP_CHALLENGE_PREFIX + tokenParts[0] + ".txt");
HttpURLConnection huc = (HttpURLConnection) u.openConnection();
if (huc.getResponseCode() != 200) {
- enterPingResult(confId, "error", "Invalid status code " + huc.getResponseCode() + ".", null);
- return;
+ return enterPingResult(conf, "error", "Invalid status code " + huc.getResponseCode() + ".", null);
}
BufferedReader br = new BufferedReader(new InputStreamReader(huc.getInputStream(), "UTF-8"));
String line = br.readLine();
if (line == null) {
- enterPingResult(confId, "error", "Empty document.", null);
- return;
+ return enterPingResult(conf, "error", "Empty document.", null);
}
if (line.trim().equals(tokenParts[1])) {
- enterPingResult(confId, PING_SUCCEDED, "", null);
- return;
+ return enterPingResult(conf, PING_SUCCEDED, "", null);
}
- enterPingResult(confId, "error", "Challenge tokens differed.", null);
- return;
+ return enterPingResult(conf, "error", "Challenge tokens differed.", null);
} catch (IOException e) {
e.printStackTrace();
- enterPingResult(confId, "error", "Exception: connection closed.", null);
- return;
+ return enterPingResult(conf, "error", "Exception: connection closed.", null);
}
}
}
--- /dev/null
+Subject: <?=_Ping for domain '${domain}' failed.?>
+
+<?=_Hi,?>
+
+<?=_A check of the ownership for your domain '${domain}' failed.?> \
+<?=_This check might either have been triggered manually or automatically for routine housekeeping.?> \
+<?=_Seeing a check fail we assume you might have lost ownership of this domain.?>
+
+<? if($valid) { ?>
+<?=_However there are currently enough succeeding proofs so the state of your domain is not endangered yet.?> \
+<?=_You might however want to correct (or remove) this ping to ensure that your domain stays valid.?>
+<? } else { ?>
+<?=_If you keep lacking sufficient proof of ownership for this domain after the grace period of two weeks expired we are going to revoke all affected certificates.?>
+
+<?=_Affected certificates:?>
+<? foreach($certs){ ?>- <?=_serial: ${serial} issued by ${ca} valid from ${from} to ${to}.?>
+<? } ?>
+<? } ?>
+
+<?=_Visit ${domainLink} to check the current state of pings of your domain and to find out more information about which ping failed.?>
package club.wpia.gigi.ping;
+import java.io.IOException;
+import java.security.GeneralSecurityException;
import java.security.KeyStore;
+import java.security.cert.X509Certificate;
+import java.sql.Timestamp;
+import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
+import java.util.Locale;
+import java.util.Map;
import java.util.Queue;
+import club.wpia.gigi.GigiApiException;
import club.wpia.gigi.database.DatabaseConnection;
import club.wpia.gigi.database.DatabaseConnection.Link;
import club.wpia.gigi.database.GigiPreparedStatement;
import club.wpia.gigi.database.GigiResultSet;
+import club.wpia.gigi.dbObjects.Certificate;
+import club.wpia.gigi.dbObjects.Certificate.RevocationType;
+import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Domain;
import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
import club.wpia.gigi.dbObjects.DomainPingType;
+import club.wpia.gigi.dbObjects.Organisation;
+import club.wpia.gigi.dbObjects.User;
+import club.wpia.gigi.localisation.Language;
+import club.wpia.gigi.output.ArrayIterable;
+import club.wpia.gigi.output.template.MailTemplate;
+import club.wpia.gigi.pages.account.domain.EditDomain;
+import club.wpia.gigi.ping.DomainPinger.PingState;
+import club.wpia.gigi.util.ServerConstants;
+import club.wpia.gigi.util.ServerConstants.Host;
public class PingerDaemon extends Thread {
private Queue<DomainPingConfiguration> toExecute = new LinkedList<>();
+ private final MailTemplate pingFailedMail = new MailTemplate(PingerDaemon.class.getResource("PingFailedWithActiveCertificates.templ"));
+
public PingerDaemon(KeyStore truststore) {
this.truststore = truststore;
}
public void runWithConnection() {
- pingers.put(DomainPingType.EMAIL, new EmailPinger());
- pingers.put(DomainPingType.SSL, new SSLPinger(truststore));
- pingers.put(DomainPingType.HTTP, new HTTPFetch());
- pingers.put(DomainPingType.DNS, new DNSPinger());
+ initializeConnectionUsage();
while (true) {
try {
}
notifyAll();
}
- try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `pc`.`id`" //
- + " FROM `pingconfig` AS `pc`" //
- + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid`" //
- + " WHERE `d`.`deleted` IS NULL" //
- + " AND `pc`.`deleted` IS NULL" //
- + " AND NOT EXISTS (" //
- + " SELECT 1 FROM `domainPinglog` AS `dpl`" //
- + " WHERE `dpl`.`configId` = `pc`.`id`" //
- + " AND `dpl`.`when` >= CURRENT_TIMESTAMP - interval '6 mons')")) {
-
- GigiResultSet rs = searchNeededPings.executeQuery();
- while (rs.next()) {
- worked = true;
- handle(DomainPingConfiguration.getById(rs.getInt("id")));
- }
- }
+ long time = System.currentTimeMillis();
+ worked |= executeNeededPings(new Date(time));
try {
if ( !worked) {
Thread.sleep(5000);
}
}
- private void handle(DomainPingConfiguration conf) {
+ protected void initializeConnectionUsage() {
+ pingers.put(DomainPingType.EMAIL, new EmailPinger());
+ pingers.put(DomainPingType.SSL, new SSLPinger(truststore));
+ pingers.put(DomainPingType.HTTP, new HTTPFetch());
+ pingers.put(DomainPingType.DNS, new DNSPinger());
+ }
+
+ public synchronized boolean executeNeededPings(Date time) {
+ boolean worked = false;
+ try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `d`.`id`, `dpl`.`configId`, `dpl`.`when`," //
+ // .. for all found pings we want to know, if we do not have a more
+ // recent successful ping
+ + " NOT EXISTS (" //
+ + " SELECT 1 FROM `domainPinglog` AS `dpl2`" //
+ + " WHERE `dpl`.`configId` = `dpl2`.`configId`" //
+ + " AND `dpl2`.state = 'success' AND `dpl2`.`when` > `dpl`.`when`) AS `resucceeded`" //
+ + " FROM `domainPinglog` AS `dpl`" //
+ // We search valid pings
+ + " INNER JOIN `pingconfig` AS `pc` ON `pc`.`id` = `dpl`.`configId` AND `pc`.`deleted` IS NULL" //
+ + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid` AND `d`.`deleted` IS NULL" //
+ // .. that failed, ..
+ + " WHERE `dpl`.`state` = 'failed'" //
+ // .. are older than 2 weeks
+ + " AND `dpl`.`when` <= ?::timestamp - interval '2 weeks'" //
+ // .. and are flagged for corrective action
+ + " AND `dpl`.`needsAction`" //
+ )) {
+ searchNeededPings.setTimestamp(1, new Timestamp(time.getTime()));
+ GigiResultSet rs = searchNeededPings.executeQuery();
+ try (GigiPreparedStatement updateDone = new GigiPreparedStatement("UPDATE `domainPinglog` SET `needsAction`=false WHERE `configId`=? AND `when`=?")) {
+ while (rs.next()) {
+ worked = true;
+ // Give this ping a last chance to succeed.
+ handle(DomainPingConfiguration.getById(rs.getInt(2)));
+ // We only consider revoking if this ping has not been
+ // superseded by a following successful ping.
+ if (rs.getBoolean(4)) {
+ Domain d = Domain.getById(rs.getInt(1));
+ int ct = 0;
+ boolean[] used = new boolean[DomainPingType.values().length];
+ // We only revoke, there are not 2 pings that are not
+ // 'strictly invalid'
+ for (DomainPingConfiguration cfg : d.getConfiguredPings()) {
+ if ( !cfg.isStrictlyInvalid(time) && !used[cfg.getType().ordinal()]) {
+ ct++;
+ used[cfg.getType().ordinal()] = true;
+ }
+ if (ct >= 2) {
+ break;
+ }
+ }
+ if (ct < 2) {
+ for (Certificate c : d.fetchActiveCertificates()) {
+ // TODO notify user
+ c.revoke(RevocationType.PING_TIMEOUT);
+ }
+ }
+ }
+ updateDone.setInt(1, rs.getInt(2));
+ updateDone.setTimestamp(2, rs.getTimestamp(3));
+ updateDone.executeUpdate();
+ }
+ }
+ }
+ try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `pc`.`id`" //
+ + " FROM `pingconfig` AS `pc`" //
+ + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid`" //
+ + " WHERE `d`.`deleted` IS NULL" //
+ + " AND `pc`.`deleted` IS NULL" //
+ + " AND NOT EXISTS (" //
+ + " SELECT 1 FROM `domainPinglog` AS `dpl`" //
+ + " WHERE `dpl`.`configId` = `pc`.`id`" //
+ + " AND `dpl`.`when` >= ?::timestamp - interval '6 mons')")) {
+ searchNeededPings.setTimestamp(1, new Timestamp(time.getTime()));
+ GigiResultSet rs = searchNeededPings.executeQuery();
+ while (rs.next()) {
+ worked = true;
+ handle(DomainPingConfiguration.getById(rs.getInt("id")));
+ }
+ }
+ return worked;
+ }
+
+ protected void handle(DomainPingConfiguration conf) {
DomainPingType type = conf.getType();
String config = conf.getInfo();
DomainPinger dp = pingers.get(type);
Domain target = conf.getTarget();
System.err.println("Executing " + dp + " on " + target + " (" + System.currentTimeMillis() + ")");
try {
- dp.ping(target, config, target.getOwner(), conf.getId());
+ DomainPingExecution x = dp.ping(target, config, target.getOwner(), conf);
+ if (x.getState() == PingState.FAILED) {
+ Certificate[] cs = target.fetchActiveCertificates();
+ if (cs.length != 0) {
+ CertificateOwner o = target.getOwner();
+ Locale l = Locale.ENGLISH;
+ String contact;
+ if (o instanceof User) {
+ l = ((User) o).getPreferredLocale();
+ contact = ((User) o).getEmail();
+ } else if (o instanceof Organisation) {
+ contact = ((Organisation) o).getContactEmail();
+
+ } else {
+ throw new Error();
+ }
+ HashMap<String, Object> vars = new HashMap<>();
+ vars.put("valid", target.isVerified());
+ vars.put("domain", target.getSuffix());
+ vars.put("domainLink", "https://" + ServerConstants.getHostNamePortSecure(Host.WWW) + "/" + EditDomain.PATH + target.getId());
+ vars.put("certs", new ArrayIterable<Certificate>(cs) {
+
+ @Override
+ public void apply(Certificate t, Language l, Map<String, Object> vars) {
+ vars.put("serial", t.getSerial());
+ vars.put("ca", t.getParent().getKeyname());
+ try {
+ X509Certificate c = t.cert();
+ vars.put("from", c.getNotBefore());
+ vars.put("to", c.getNotAfter());
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (GeneralSecurityException e) {
+ e.printStackTrace();
+ } catch (GigiApiException e) {
+ e.printStackTrace();
+ }
+ }
+
+ });
+ pingFailedMail.sendMail(Language.getInstance(l), vars, contact);
+ System.out.println("Ping failed with active certificates");
+ }
+ }
} catch (Throwable t) {
t.printStackTrace();
- DomainPinger.enterPingResult(conf.getId(), "error", "exception", null);
+ DomainPinger.enterPingResult(conf, "error", "exception", null);
}
System.err.println("done (" + System.currentTimeMillis() + ")");
}
import club.wpia.gigi.dbObjects.Certificate;
import club.wpia.gigi.dbObjects.CertificateOwner;
import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
import sun.security.x509.AVA;
import sun.security.x509.X500Name;
}
@Override
- public void ping(Domain domain, String configuration, CertificateOwner u, int confId) {
+ public DomainPingExecution ping(Domain domain, String configuration, CertificateOwner u, DomainPingConfiguration conf) {
try (SocketChannel sch = SocketChannel.open()) {
sch.socket().setSoTimeout(5000);
String[] parts = configuration.split(":", 4);
String key = parts[0];
String value = parts[1];
String res = test(sch, domain.getSuffix(), u, value);
- enterPingResult(confId, res, res, null);
- return;
+ return enterPingResult(conf, res, res, null);
} catch (IOException e) {
- enterPingResult(confId, "error", "connection Failed", null);
- return;
+ return enterPingResult(conf, "error", "connection Failed", null);
}
}
--- /dev/null
+package club.wpia.gigi.ping;
+
+import static org.junit.Assert.*;
+import static org.junit.Assume.*;
+
+import java.io.IOException;
+import java.security.GeneralSecurityException;
+import java.security.KeyPair;
+import java.util.Date;
+import java.util.List;
+
+import org.hamcrest.CoreMatchers;
+import org.junit.Test;
+
+import club.wpia.gigi.GigiApiException;
+import club.wpia.gigi.dbObjects.Certificate;
+import club.wpia.gigi.dbObjects.Certificate.CSRType;
+import club.wpia.gigi.dbObjects.Certificate.CertificateStatus;
+import club.wpia.gigi.dbObjects.Certificate.SANType;
+import club.wpia.gigi.dbObjects.CertificateProfile;
+import club.wpia.gigi.dbObjects.Digest;
+import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
+import club.wpia.gigi.dbObjects.DomainPingType;
+import club.wpia.gigi.ping.DomainPinger.PingState;
+import club.wpia.gigi.testUtils.PingTest;
+import club.wpia.gigi.testUtils.TestEmailReceiver.TestMail;
+import club.wpia.gigi.util.RandomToken;
+import club.wpia.gigi.util.SimpleSigner;
+
+public class TestTiming extends PingTest {
+
+ @Test
+ public void httpAndMailSuccessCert() throws GigiApiException, IOException, InterruptedException, GeneralSecurityException {
+ httpAndMailSuccess(true, false);
+ }
+
+ @Test
+ public void httpAndMailSuccessCertAndCorrect() throws GigiApiException, IOException, InterruptedException, GeneralSecurityException {
+ httpAndMailSuccess(true, true);
+ }
+
+ @Test
+ public void httpAndMailSuccessNoCert() throws GigiApiException, IOException, InterruptedException, GeneralSecurityException {
+ httpAndMailSuccess(false, false);
+ }
+
+ public void httpAndMailSuccess(boolean certs, boolean correct) throws GigiApiException, IOException, InterruptedException, GeneralSecurityException {
+ String test = getTestProps().getProperty("domain.http");
+ assumeNotNull(test);
+
+ // When we have a domain.
+ Domain d = new Domain(u, u, test);
+ String token = RandomToken.generateToken(16);
+ String value = RandomToken.generateToken(16);
+
+ // If we run the sub case that we have certificates on the domain,
+ // create a certificate now.
+ Certificate c = null;
+ if (certs) {
+ KeyPair kp = generateKeypair();
+ String key = generatePEMCSR(kp, "CN=testmail@example.com");
+ c = new Certificate(u, u, Certificate.buildDN("CN", "testmail@example.com"), Digest.SHA256, key, CSRType.CSR, CertificateProfile.getByName("server"), new Certificate.SubjectAlternateName(SANType.DNS, test));
+ await(c.issue(null, "2y", u));
+ }
+
+ // Register HTTP and Email pings.
+ updateService(token, value, "http");
+ d.addPing(DomainPingType.EMAIL, "postmaster");
+ d.addPing(DomainPingType.HTTP, token + ":" + value);
+
+ // Two successful pings
+ getMailReceiver().receive("postmaster@" + test).verify();
+ waitForPings(2);
+
+ assertEquals(0, countFailed(d.getPings(), 2));
+
+ // An own Pinger Daemon to control ping execution locally.
+ PingerDaemon pd = new PingerDaemon(null);
+ pd.initializeConnectionUsage();
+
+ // After 6 months the pings are executed again
+ pd.executeNeededPings(new Date(System.currentTimeMillis() + 6 * 31 * 24 * 60 * 60L * 1000));
+ getMailReceiver().receive("postmaster@" + test).verify();
+ waitForPings(4);
+ assertEquals(0, countFailed(d.getPings(), 4));
+
+ // After 6 months the pings are executed again, but when the HTTP file
+ // is wrong, that ping fails.
+ updateService(token, value + "broken", "http");
+ // Note that the time is still 6 months in the future, as the pings from
+ // before were still executed (and logged)
+ // as executed now.
+ pd.executeNeededPings(new Date(System.currentTimeMillis() + 6 * 31 * 24 * 60 * 60L * 1000));
+ getMailReceiver().receive("postmaster@" + test).verify();
+ waitForPings(6);
+ assertEquals(1, countFailed(d.getPings(), 6));
+ // Which renders the domain invalid
+ assertFalse(d.isVerified());
+
+ if (certs) {
+ // And the user gets a warning-mail if there was a cert
+ TestMail mail = getMailReceiver().receive(u.getEmail());
+ assertThat(mail.getMessage(), CoreMatchers.containsString(d.getSuffix()));
+ assertThat(mail.getMessage(), CoreMatchers.containsString(c.getSerial()));
+ if ( !correct) {
+ // If the user ignores the warning, after two weeks
+ pd.executeNeededPings(new Date(System.currentTimeMillis() + 15 * 24 * 60 * 60L * 1000));
+ // The user receives another warning mail.
+ mail = getMailReceiver().receive(u.getEmail());
+ assertThat(mail.getMessage(), CoreMatchers.containsString(d.getSuffix()));
+ assertThat(mail.getMessage(), CoreMatchers.containsString(c.getSerial()));
+ // And when the revocation is carried out
+ SimpleSigner.ping();
+ // ... and the certificate gets revoked.
+ assertEquals(CertificateStatus.REVOKED, c.getStatus());
+ } else {
+ // But if the user corrects the ping, ...
+ updateService(token, value, "http");
+ // ... and the ping is re-executed,
+ pd.handle(getPing(d.getConfiguredPings(), DomainPingType.HTTP));
+ waitForPings(7);
+ assertEquals(1, countFailed(d.getPings(), 7));
+
+ // Even after two weeks
+ pd.executeNeededPings(new Date(System.currentTimeMillis() + 15 * 24 * 60 * 60L * 1000));
+ // and all resulting jobs are executed
+ SimpleSigner.ping();
+ // ... the certificate stays valid.
+ assertEquals(CertificateStatus.ISSUED, c.getStatus());
+ }
+ } else {
+ // otherwise there is no mail
+ }
+
+ }
+
+ private DomainPingConfiguration getPing(List<DomainPingConfiguration> cp, DomainPingType tp) {
+ for (DomainPingConfiguration d : cp) {
+ if (d.getType() == tp) {
+ return d;
+ }
+ }
+ throw new Error("Type not found.");
+ }
+
+ private int countFailed(DomainPingExecution[] pg, int count) {
+ assertEquals(count, pg.length);
+ int fld = 0;
+ for (DomainPingExecution e : pg) {
+ PingState state = e.getState();
+ if (e.getConfig().getType() == DomainPingType.HTTP) {
+ if (state == PingState.FAILED) {
+ fld++;
+ continue;
+ }
+ }
+ assertEquals(PingState.SUCCESS, state);
+ }
+ return fld;
+ }
+
+}
d.addPing(DomainPingType.EMAIL, "admin");
TestMail testMail = getMailReceiver().receive("admin@" + d.getSuffix());
testMail.verify();
+ // Enforce successful ping :-)
+ d.addPing(DomainPingType.HTTP, "a:b");
+ try (GigiPreparedStatement gps = new GigiPreparedStatement("INSERT INTO `domainPinglog` SET `configId`=(SELECT `id` FROM `pingconfig` WHERE `domainid`=? AND `type`='http'), state='success', needsAction=false")) {
+ gps.setInt(1, d.getId());
+ gps.execute();
+ }
assertTrue(d.isVerified());
} catch (GigiApiException e) {
throw new Error(e);
/**
* Base class for test suites that check extensively if the domain-ping
- * functionality wroks as expected.
+ * functionality works as expected.
*/
public abstract class PingTest extends ClientTest {
assertEquals(200, ((HttpURLConnection) new URL(url).openConnection()).getResponseCode());
}
- protected void waitForPings(int count) throws SQLException, InterruptedException {
+ protected void waitForPings(int count) throws InterruptedException {
try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT COUNT(*) FROM `domainPinglog`")) {
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < 10000) {
return m;
}
+ /**
+ * Needs to be @After class. In order to init {@link ClientTest#id}
+ * correctly, this needs to be after test. Before test this might be
+ * executed after the init of {@link ClientTest#id} and make that value
+ * invalid.
+ */
@After
public void purgeDbAfterTest() throws SQLException, IOException {
purgeDatabase();
import club.wpia.gigi.dbObjects.Country;
import club.wpia.gigi.dbObjects.Digest;
import club.wpia.gigi.dbObjects.Domain;
+import club.wpia.gigi.dbObjects.DomainPingConfiguration;
+import club.wpia.gigi.dbObjects.DomainPingExecution;
import club.wpia.gigi.dbObjects.DomainPingType;
import club.wpia.gigi.dbObjects.EmailAddress;
import club.wpia.gigi.dbObjects.Group;
}
@Override
- public void ping(Domain domain, String configuration, CertificateOwner target, int confId) {
+ public DomainPingExecution ping(Domain domain, String configuration, CertificateOwner target, DomainPingConfiguration conf) {
System.err.println("TestManager: " + domain.getSuffix());
if (pingExempt.contains(domain.getSuffix())) {
- enterPingResult(confId, DomainPinger.PING_SUCCEDED, "Succeeded by TestManager pass-by", null);
+ return enterPingResult(conf, DomainPinger.PING_SUCCEDED, "Succeeded by TestManager pass-by", null);
} else {
DomainPinger pinger = dps.get(dpt);
System.err.println("Forward to old pinger: " + pinger);
- pinger.ping(domain, configuration, target, confId);
+ return pinger.ping(domain, configuration, target, conf);
}
}