]> WPIA git - gigi.git/blob - src/org/cacert/gigi/dbObjects/Domain.java
fix: SQL change database call pattern
[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         checkPunycode(parts[0], s.substring(parts[0].length() + 1));
77     }
78
79     private static void checkPunycode(String label, String domainContext) throws GigiApiException {
80         if (label.charAt(2) != '-' || label.charAt(3) != '-') {
81             return; // is no punycode
82         }
83         if ( !IDNEnabledTLDs.contains(domainContext)) {
84             throw new GigiApiException("Punycode label could not be positively verified.");
85         }
86         if ( !label.startsWith("xn--")) {
87             throw new GigiApiException("Unknown ACE prefix.");
88         }
89         try {
90             String unicode = IDN.toUnicode(label);
91             if (unicode.startsWith("xn--")) {
92                 throw new GigiApiException("Punycode label could not be positively verified.");
93             }
94         } catch (IllegalArgumentException e) {
95             throw new GigiApiException("Punycode label could not be positively verified.");
96         }
97     }
98
99     public static boolean isVaildDomainPart(String s, boolean allowPunycode) {
100         if ( !s.matches("[a-z0-9-]+")) {
101             return false;
102         }
103         if (s.charAt(0) == '-' || s.charAt(s.length() - 1) == '-') {
104             return false;
105         }
106         if (s.length() > 63) {
107             return false;
108         }
109         boolean canBePunycode = s.length() >= 4 && s.charAt(2) == '-' && s.charAt(3) == '-';
110         if (canBePunycode && !allowPunycode) {
111             return false;
112         }
113         return true;
114     }
115
116     private static void checkInsert(String suffix) throws GigiApiException {
117         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")) {
118             ps.setString(1, suffix);
119             ps.setString(2, suffix);
120             ps.setString(3, suffix);
121             ps.setString(4, suffix);
122             GigiResultSet rs = ps.executeQuery();
123             boolean existed = rs.next();
124             rs.close();
125             if (existed) {
126                 throw new GigiApiException("Domain could not be inserted. Domain is already valid.");
127             }
128         }
129     }
130
131     private void insert() throws GigiApiException {
132         if (id != 0) {
133             throw new GigiApiException("already inserted.");
134         }
135         checkInsert(suffix);
136         try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `domains` SET memid=?, domain=?")) {
137             ps.setInt(1, owner.getId());
138             ps.setString(2, suffix);
139             ps.execute();
140             id = ps.lastInsertId();
141         }
142         myCache.put(this);
143     }
144
145     public void delete() throws GigiApiException {
146         if (id == 0) {
147             throw new GigiApiException("not inserted.");
148         }
149         try (GigiPreparedStatement ps = new GigiPreparedStatement("UPDATE `domains` SET `deleted`=CURRENT_TIMESTAMP WHERE `id`=?")) {
150             ps.setInt(1, id);
151             ps.execute();
152         }
153     }
154
155     public CertificateOwner getOwner() {
156         return owner;
157     }
158
159     @Override
160     public int getId() {
161         return id;
162     }
163
164     public String getSuffix() {
165         return suffix;
166     }
167
168     private LinkedList<DomainPingConfiguration> configs = null;
169
170     public List<DomainPingConfiguration> getConfiguredPings() throws GigiApiException {
171         LinkedList<DomainPingConfiguration> configs = this.configs;
172         if (configs == null) {
173             configs = new LinkedList<>();
174             try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT id FROM pingconfig WHERE domainid=?")) {
175                 ps.setInt(1, id);
176                 GigiResultSet rs = ps.executeQuery();
177                 while (rs.next()) {
178                     configs.add(DomainPingConfiguration.getById(rs.getInt(1)));
179                 }
180             }
181             this.configs = configs;
182
183         }
184         return Collections.unmodifiableList(configs);
185     }
186
187     public void addPing(DomainPingType type, String config) throws GigiApiException {
188         try (GigiPreparedStatement ps = new GigiPreparedStatement("INSERT INTO `pingconfig` SET `domainid`=?, `type`=?::`pingType`, `info`=?")) {
189             ps.setInt(1, id);
190             ps.setString(2, type.toString().toLowerCase());
191             ps.setString(3, config);
192             ps.execute();
193         }
194         configs = null;
195     }
196
197     public synchronized void verify(String hash) throws GigiApiException {
198         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')")) {
199             ps.setString(1, hash);
200             ps.setInt(2, id);
201             ps.executeUpdate();
202         }
203     }
204
205     public boolean isVerified() {
206         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT 1 FROM `domainPinglog` INNER JOIN `pingconfig` ON `pingconfig`.`id`=`domainPinglog`.`configId` WHERE `domainid`=? AND `state`='success'")) {
207             ps.setInt(1, id);
208             GigiResultSet rs = ps.executeQuery();
209             return rs.next();
210         }
211     }
212
213     public DomainPingExecution[] getPings() throws GigiApiException {
214         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)) {
215             ps.setInt(1, id);
216             GigiResultSet rs = ps.executeQuery();
217             rs.last();
218             DomainPingExecution[] contents = new DomainPingExecution[rs.getRow()];
219             rs.beforeFirst();
220             for (int i = 0; i < contents.length && rs.next(); i++) {
221                 contents[i] = new DomainPingExecution(rs);
222             }
223             return contents;
224         }
225
226     }
227
228     private static final ObjectCache<Domain> myCache = new ObjectCache<>();
229
230     public static synchronized Domain getById(int id) throws IllegalArgumentException {
231         Domain em = myCache.get(id);
232         if (em == null) {
233             myCache.put(em = new Domain(id));
234         }
235         return em;
236     }
237
238     public static int searchUserIdByDomain(String domain) {
239         try (GigiPreparedStatement ps = new GigiPreparedStatement("SELECT `memid` FROM `domains` WHERE `domain` = ?")) {
240             ps.setString(1, domain);
241             GigiResultSet res = ps.executeQuery();
242             if (res.next()) {
243                 return res.getInt(1);
244             } else {
245                 return -1;
246             }
247         }
248     }
249
250 }