]> WPIA git - gigi.git/blob - src/org/cacert/gigi/dbObjects/Domain.java
Merge "Update notes about password security"
[gigi.git] / src / org / cacert / gigi / dbObjects / Domain.java
1 package org.cacert.gigi.dbObjects;
2
3 import java.io.IOException;
4 import java.io.InputStream;
5 import java.net.IDN;
6 import java.util.Arrays;
7 import java.util.Collections;
8 import java.util.HashSet;
9 import java.util.LinkedList;
10 import java.util.List;
11 import java.util.Properties;
12 import java.util.Set;
13
14 import org.cacert.gigi.GigiApiException;
15 import org.cacert.gigi.database.GigiPreparedStatement;
16 import org.cacert.gigi.database.GigiResultSet;
17 import org.cacert.gigi.util.PublicSuffixes;
18
19 public class Domain implements IdCachable, Verifyable {
20
21     private CertificateOwner owner;
22
23     private String suffix;
24
25     private int id;
26
27     private static final Set<String> IDNEnabledTLDs;
28
29     static {
30         Properties CPS = new Properties();
31         try (InputStream resourceAsStream = Domain.class.getResourceAsStream("CPS.properties")) {
32             CPS.load(resourceAsStream);
33             IDNEnabledTLDs = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(CPS.getProperty("IDN-enabled").split(","))));
34         } catch (IOException e) {
35             throw new Error(e);
36         }
37     }
38
39     private Domain(int id) {
40         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid`, `domain` FROM `domains` WHERE `id`=? AND `deleted` IS NULL")) {
41             ps.setInt(1, id);
42
43             GigiResultSet rs = ps.executeQuery();
44             if ( !rs.next()) {
45                 throw new IllegalArgumentException("Invalid domain id " + id);
46             }
47             this.id = id;
48             owner = CertificateOwner.getById(rs.getInt(1));
49             suffix = rs.getString(2);
50         }
51     }
52
53     public Domain(User actor, CertificateOwner owner, String suffix) throws GigiApiException {
54         suffix = suffix.toLowerCase();
55         synchronized (Domain.class) {
56             checkCertifyableDomain(suffix, actor.isInGroup(Group.CODESIGNING));
57             this.owner = owner;
58             this.suffix = suffix;
59             insert();
60         }
61     }
62
63     public static void checkCertifyableDomain(String s, boolean hasPunycodeRight) throws GigiApiException {
64         String[] parts = s.split("\\.", -1);
65         if (parts.length < 2) {
66             throw new GigiApiException("Domain does not contain '.'.");
67         }
68         for (int i = parts.length - 1; i >= 0; i--) {
69             if ( !isVaildDomainPart(parts[i], hasPunycodeRight)) {
70                 throw new GigiApiException("Syntax error in Domain");
71             }
72         }
73         String publicSuffix = PublicSuffixes.getInstance().getRegistrablePart(s);
74         if ( !s.equals(publicSuffix)) {
75             throw new GigiApiException("You may only register a domain with exactly one lable before the public suffix.");
76         }
77         if (("." + s).matches("(\\.[0-9]*)*")) {
78             // This is not reached because we currently have no TLD that is
79             // numbers only. But who knows..
80             // Better safe than sorry.
81             throw new GigiApiException("IP Addresses are not allowed");
82         }
83         checkPunycode(parts[0], s.substring(parts[0].length() + 1));
84     }
85
86     private static void checkPunycode(String label, String domainContext) throws GigiApiException {
87         if (label.charAt(2) != '-' || label.charAt(3) != '-') {
88             return; // is no punycode
89         }
90         if ( !IDNEnabledTLDs.contains(domainContext)) {
91             throw new GigiApiException("Punycode label could not be positively verified.");
92         }
93         if ( !label.startsWith("xn--")) {
94             throw new GigiApiException("Unknown ACE prefix.");
95         }
96         try {
97             String unicode = IDN.toUnicode(label);
98             if (unicode.startsWith("xn--")) {
99                 throw new GigiApiException("Punycode label could not be positively verified.");
100             }
101         } catch (IllegalArgumentException e) {
102             throw new GigiApiException("Punycode label could not be positively verified.");
103         }
104     }
105
106     public static boolean isVaildDomainPart(String s, boolean allowPunycode) {
107         if ( !s.matches("[a-z0-9-]+")) {
108             return false;
109         }
110         if (s.charAt(0) == '-' || s.charAt(s.length() - 1) == '-') {
111             return false;
112         }
113         if (s.length() > 63) {
114             return false;
115         }
116         boolean canBePunycode = s.length() >= 4 && s.charAt(2) == '-' && s.charAt(3) == '-';
117         if (canBePunycode && !allowPunycode) {
118             return false;
119         }
120         return true;
121     }
122
123     private static void checkInsert(String suffix) throws GigiApiException {
124         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domains` WHERE (`domain`=? OR (CONCAT('.', `domain`)=RIGHT(?,LENGTH(`domain`)+1)  OR RIGHT(`domain`,LENGTH(?)+1)=CONCAT('.',?))) AND `deleted` IS NULL")) {
125             ps.setString(1, suffix);
126             ps.setString(2, suffix);
127             ps.setString(3, suffix);
128             ps.setString(4, suffix);
129             GigiResultSet rs = ps.executeQuery();
130             boolean existed = rs.next();
131             rs.close();
132             if (existed) {
133                 throw new GigiApiException("Domain could not be inserted. Domain is already known to the system.");
134             }
135         }
136     }
137
138     private void insert() throws GigiApiException {
139         if (id != 0) {
140             throw new GigiApiException("already inserted.");
141         }
142         checkInsert(suffix);
143         try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `domains` SET memid=?, domain=?")) {
144             ps.setInt(1, owner.getId());
145             ps.setString(2, suffix);
146             ps.execute();
147             id = ps.lastInsertId();
148         }
149         myCache.put(this);
150     }
151
152     public void delete() throws GigiApiException {
153         if (id == 0) {
154             throw new GigiApiException("not inserted.");
155         }
156         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `domains` SET `deleted`=CURRENT_TIMESTAMP WHERE `id`=?")) {
157             ps.setInt(1, id);
158             ps.execute();
159         }
160     }
161
162     public CertificateOwner getOwner() {
163         return owner;
164     }
165
166     @Override
167     public int getId() {
168         return id;
169     }
170
171     public String getSuffix() {
172         return suffix;
173     }
174
175     private LinkedList<DomainPingConfiguration> configs = null;
176
177     public List<DomainPingConfiguration> getConfiguredPings() throws GigiApiException {
178         LinkedList<DomainPingConfiguration> configs = this.configs;
179         if (configs == null) {
180             configs = new LinkedList<>();
181             try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT id FROM pingconfig WHERE domainid=? AND `deleted` IS NULL")) {
182                 ps.setInt(1, id);
183                 GigiResultSet rs = ps.executeQuery();
184                 while (rs.next()) {
185                     configs.add(DomainPingConfiguration.getById(rs.getInt(1)));
186                 }
187             }
188             this.configs = configs;
189
190         }
191         return Collections.unmodifiableList(configs);
192     }
193
194     public void addPing(DomainPingType type, String config) throws GigiApiException {
195         try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `pingconfig` SET `domainid`=?, `type`=?::`pingType`, `info`=?")) {
196             ps.setInt(1, id);
197             ps.setString(2, type.toString().toLowerCase());
198             ps.setString(3, config);
199             ps.execute();
200         }
201         configs = null;
202     }
203
204     public void clearPings() throws GigiApiException {
205         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `pingconfig` SET `deleted`=CURRENT_TIMESTAMP WHERE `deleted` is NULL AND `domainid`=?")) {
206             ps.setInt(1, id);
207             ps.execute();
208         }
209         configs = null;
210     }
211
212     public synchronized void verify(String hash) throws GigiApiException {
213         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `domainPinglog` SET `state`='success' WHERE `challenge`=? AND `state`='open' AND `configId` IN (SELECT `id` FROM `pingconfig` WHERE `domainid`=? AND `type`='email')")) {
214             ps.setString(1, hash);
215             ps.setInt(2, id);
216             ps.executeUpdate();
217         }
218     }
219
220     public boolean isVerified() {
221         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `domainid`=? AND `state`='success'")) {
222             ps.setInt(1, id);
223             GigiResultSet rs = ps.executeQuery();
224             return rs.next();
225         }
226     }
227
228     public DomainPingExecution[] getPings() throws GigiApiException {
229         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `state`, `type`, `info`, `result`, `configId`, `when` FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `pingconfig`.`domainid`=? ORDER BY `when` DESC;", true)) {
230             ps.setInt(1, id);
231             GigiResultSet rs = ps.executeQuery();
232             rs.last();
233             DomainPingExecution[] contents = new DomainPingExecution[rs.getRow()];
234             rs.beforeFirst();
235             for (int i = 0; i < contents.length && rs.next(); i++) {
236                 contents[i] = new DomainPingExecution(rs);
237             }
238             return contents;
239         }
240
241     }
242
243     private static final ObjectCache<Domain> myCache = new ObjectCache<>();
244
245     public static synchronized Domain getById(int id) throws IllegalArgumentException {
246         Domain em = myCache.get(id);
247         if (em == null) {
248             myCache.put(em = new Domain(id));
249         }
250         return em;
251     }
252
253     public static Domain searchUserIdByDomain(String domain) {
254         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `id` FROM `domains` WHERE `domain` = ?")) {
255             ps.setString(1, domain);
256             GigiResultSet res = ps.executeQuery();
257             if (res.next()) {
258                 return getById(res.getInt(1));
259             } else {
260                 return null;
261             }
262         }
263     }
264
265 }