]> WPIA git - gigi.git/blob - src/club/wpia/gigi/ping/PingerDaemon.java
upd: correct url in email when ping fails
[gigi.git] / src / club / wpia / gigi / ping / PingerDaemon.java
1 package club.wpia.gigi.ping;
2
3 import java.io.IOException;
4 import java.security.GeneralSecurityException;
5 import java.security.KeyStore;
6 import java.security.cert.X509Certificate;
7 import java.sql.Timestamp;
8 import java.util.Date;
9 import java.util.HashMap;
10 import java.util.LinkedList;
11 import java.util.Locale;
12 import java.util.Map;
13 import java.util.Queue;
14
15 import club.wpia.gigi.GigiApiException;
16 import club.wpia.gigi.database.DatabaseConnection;
17 import club.wpia.gigi.database.DatabaseConnection.Link;
18 import club.wpia.gigi.database.GigiPreparedStatement;
19 import club.wpia.gigi.database.GigiResultSet;
20 import club.wpia.gigi.dbObjects.Certificate;
21 import club.wpia.gigi.dbObjects.Certificate.RevocationType;
22 import club.wpia.gigi.dbObjects.CertificateOwner;
23 import club.wpia.gigi.dbObjects.Domain;
24 import club.wpia.gigi.dbObjects.DomainPingConfiguration;
25 import club.wpia.gigi.dbObjects.DomainPingExecution;
26 import club.wpia.gigi.dbObjects.DomainPingType;
27 import club.wpia.gigi.dbObjects.Organisation;
28 import club.wpia.gigi.dbObjects.User;
29 import club.wpia.gigi.localisation.Language;
30 import club.wpia.gigi.output.ArrayIterable;
31 import club.wpia.gigi.output.template.MailTemplate;
32 import club.wpia.gigi.pages.account.domain.EditDomain;
33 import club.wpia.gigi.ping.DomainPinger.PingState;
34 import club.wpia.gigi.util.ServerConstants;
35 import club.wpia.gigi.util.ServerConstants.Host;
36
37 public class PingerDaemon extends Thread {
38
39     HashMap<DomainPingType, DomainPinger> pingers = new HashMap<>();
40
41     private KeyStore truststore;
42
43     private Queue<DomainPingConfiguration> toExecute = new LinkedList<>();
44
45     private final MailTemplate pingFailedMail = new MailTemplate(PingerDaemon.class.getResource("PingFailedWithActiveCertificates.templ"));
46
47     public PingerDaemon(KeyStore truststore) {
48         this.truststore = truststore;
49     }
50
51     @Override
52     public void run() {
53         try (Link l = DatabaseConnection.newLink(false)) {
54             runWithConnection();
55         } catch (InterruptedException e) {
56             e.printStackTrace();
57         }
58     }
59
60     public void runWithConnection() {
61
62         initializeConnectionUsage();
63
64         while (true) {
65             try {
66                 boolean worked = false;
67                 synchronized (this) {
68                     DomainPingConfiguration conf;
69                     while ((conf = toExecute.peek()) != null) {
70                         worked = true;
71                         handle(conf);
72                         toExecute.remove();
73                     }
74                     notifyAll();
75                 }
76                 long time = System.currentTimeMillis();
77                 worked |= executeNeededPings(new Date(time));
78                 try {
79                     if ( !worked) {
80                         Thread.sleep(5000);
81                     }
82                 } catch (InterruptedException e) {
83                 }
84             } catch (Throwable t) {
85                 t.printStackTrace();
86                 try {
87                     Thread.sleep(5000);
88                 } catch (InterruptedException e) {
89                 }
90             }
91         }
92     }
93
94     protected void initializeConnectionUsage() {
95         pingers.put(DomainPingType.EMAIL, new EmailPinger());
96         pingers.put(DomainPingType.SSL, new SSLPinger(truststore));
97         pingers.put(DomainPingType.HTTP, new HTTPFetch());
98         pingers.put(DomainPingType.DNS, new DNSPinger());
99     }
100
101     public synchronized boolean executeNeededPings(Date time) {
102         boolean worked = false;
103         try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `d`.`id`, `dpl`.`configId`, `dpl`.`when`," //
104         // .. for all found pings we want to know, if we do not have a more
105         // recent successful ping
106                 + "  NOT EXISTS (" //
107                 + "    SELECT 1 FROM `domainPinglog` AS `dpl2`" //
108                 + "    WHERE `dpl`.`configId` = `dpl2`.`configId`" //
109                 + "     AND `dpl2`.state = 'success' AND `dpl2`.`when` > `dpl`.`when`) AS `resucceeded`" //
110                 + " FROM `domainPinglog` AS `dpl`" //
111                 // We search valid pings
112                 + " INNER JOIN `pingconfig` AS `pc` ON `pc`.`id` = `dpl`.`configId` AND `pc`.`deleted` IS NULL" //
113                 + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid` AND `d`.`deleted` IS NULL" //
114                 // .. that failed, ..
115                 + " WHERE `dpl`.`state` = 'failed'" //
116                 // .. are older than 2 weeks
117                 + " AND `dpl`.`when` <= ?::timestamp - interval '2 weeks'" //
118                 // .. and are flagged for corrective action
119                 + " AND `dpl`.`needsAction`" //
120         )) {
121             searchNeededPings.setTimestamp(1, new Timestamp(time.getTime()));
122             GigiResultSet rs = searchNeededPings.executeQuery();
123             try (GigiPreparedStatement updateDone = new GigiPreparedStatement("UPDATE `domainPinglog` SET `needsAction`=false WHERE `configId`=? AND `when`=?")) {
124                 while (rs.next()) {
125                     worked = true;
126                     // Give this ping a last chance to succeed.
127                     handle(DomainPingConfiguration.getById(rs.getInt(2)));
128                     // We only consider revoking if this ping has not been
129                     // superseded by a following successful ping.
130                     if (rs.getBoolean(4)) {
131                         Domain d = Domain.getById(rs.getInt(1));
132                         int ct = 0;
133                         boolean[] used = new boolean[DomainPingType.values().length];
134                         // We only revoke, there are not 2 pings that are not
135                         // 'strictly invalid'
136                         for (DomainPingConfiguration cfg : d.getConfiguredPings()) {
137                             if ( !cfg.isStrictlyInvalid(time) && !used[cfg.getType().ordinal()]) {
138                                 ct++;
139                                 used[cfg.getType().ordinal()] = true;
140                             }
141                             if (ct >= 2) {
142                                 break;
143                             }
144                         }
145                         if (ct < 2) {
146                             for (Certificate c : d.fetchActiveCertificates()) {
147                                 // TODO notify user
148                                 c.revoke(RevocationType.PING_TIMEOUT);
149                             }
150                         }
151                     }
152                     updateDone.setInt(1, rs.getInt(2));
153                     updateDone.setTimestamp(2, rs.getTimestamp(3));
154                     updateDone.executeUpdate();
155                 }
156             }
157         }
158         try (GigiPreparedStatement searchNeededPings = new GigiPreparedStatement("SELECT `pc`.`id`" //
159                 + " FROM `pingconfig` AS `pc`" //
160                 + " INNER JOIN `domains` AS `d` ON `d`.`id` = `pc`.`domainid`" //
161                 + " WHERE `d`.`deleted` IS NULL" //
162                 + "  AND `pc`.`deleted` IS NULL" //
163                 + "  AND NOT EXISTS (" //
164                 + "    SELECT 1 FROM `domainPinglog` AS `dpl`" //
165                 + "    WHERE `dpl`.`configId` = `pc`.`id`" //
166                 + "     AND `dpl`.`when` >= ?::timestamp - interval '6 mons')")) {
167             searchNeededPings.setTimestamp(1, new Timestamp(time.getTime()));
168             GigiResultSet rs = searchNeededPings.executeQuery();
169             while (rs.next()) {
170                 worked = true;
171                 handle(DomainPingConfiguration.getById(rs.getInt("id")));
172             }
173         }
174         return worked;
175     }
176
177     protected void handle(DomainPingConfiguration conf) {
178         DomainPingType type = conf.getType();
179         String config = conf.getInfo();
180         DomainPinger dp = pingers.get(type);
181         if (dp != null) {
182             Domain target = conf.getTarget();
183             System.err.println("Executing " + dp + " on " + target + " (" + System.currentTimeMillis() + ")");
184             try {
185                 DomainPingExecution x = dp.ping(target, config, target.getOwner(), conf);
186                 if (x.getState() == PingState.FAILED) {
187                     Certificate[] cs = target.fetchActiveCertificates();
188                     if (cs.length != 0) {
189                         CertificateOwner o = target.getOwner();
190                         Locale l = Locale.ENGLISH;
191                         String contact;
192                         if (o instanceof User) {
193                             l = ((User) o).getPreferredLocale();
194                             contact = ((User) o).getEmail();
195                         } else if (o instanceof Organisation) {
196                             contact = ((Organisation) o).getContactEmail();
197
198                         } else {
199                             throw new Error();
200                         }
201                         HashMap<String, Object> vars = new HashMap<>();
202                         vars.put("valid", target.isVerified());
203                         vars.put("domain", target.getSuffix());
204                         vars.put("domainLink", "https://" + ServerConstants.getHostNamePortSecure(Host.WWW) + EditDomain.PATH + target.getId());
205                         vars.put("certs", new ArrayIterable<Certificate>(cs) {
206
207                             @Override
208                             public void apply(Certificate t, Language l, Map<String, Object> vars) {
209                                 vars.put("serial", t.getSerial());
210                                 vars.put("ca", t.getParent().getKeyname());
211                                 try {
212                                     X509Certificate c = t.cert();
213                                     vars.put("from", c.getNotBefore());
214                                     vars.put("to", c.getNotAfter());
215                                 } catch (IOException e) {
216                                     e.printStackTrace();
217                                 } catch (GeneralSecurityException e) {
218                                     e.printStackTrace();
219                                 } catch (GigiApiException e) {
220                                     e.printStackTrace();
221                                 }
222                             }
223
224                         });
225                         pingFailedMail.sendMail(Language.getInstance(l), vars, contact);
226                         System.out.println("Ping failed with active certificates");
227                     }
228                 }
229             } catch (Throwable t) {
230                 t.printStackTrace();
231                 DomainPinger.enterPingResult(conf, "error", "exception", null);
232             }
233             System.err.println("done (" + System.currentTimeMillis() + ")");
234         }
235     }
236
237     public synchronized void queue(DomainPingConfiguration toReping) {
238         interrupt();
239         toExecute.add(toReping);
240         while (toExecute.size() > 0) {
241             try {
242                 wait();
243             } catch (InterruptedException e) {
244                 e.printStackTrace();
245             }
246         }
247     }
248 }