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