1 | /* |
2 | * Copyright 2004-2014 H2 Group. Multiple-Licensed under the MPL 2.0, |
3 | * and the EPL 1.0 (http://h2database.com/html/license.html). |
4 | * Initial Developer: H2 Group |
5 | */ |
6 | package org.h2.engine; |
7 | |
8 | import java.util.HashMap; |
9 | |
10 | import org.h2.api.ErrorCode; |
11 | import org.h2.command.CommandInterface; |
12 | import org.h2.command.Parser; |
13 | import org.h2.command.dml.SetTypes; |
14 | import org.h2.message.DbException; |
15 | import org.h2.store.FileLock; |
16 | import org.h2.util.MathUtils; |
17 | import org.h2.util.New; |
18 | import org.h2.util.StringUtils; |
19 | import org.h2.util.Utils; |
20 | |
21 | /** |
22 | * The engine contains a map of all open databases. |
23 | * It is also responsible for opening and creating new databases. |
24 | * This is a singleton class. |
25 | */ |
26 | public class Engine implements SessionFactory { |
27 | |
28 | private static final Engine INSTANCE = new Engine(); |
29 | private static final HashMap<String, Database> DATABASES = New.hashMap(); |
30 | |
31 | private volatile long wrongPasswordDelay = |
32 | SysProperties.DELAY_WRONG_PASSWORD_MIN; |
33 | private boolean jmx; |
34 | |
35 | private Engine() { |
36 | // use getInstance() |
37 | } |
38 | |
39 | public static Engine getInstance() { |
40 | return INSTANCE; |
41 | } |
42 | |
43 | private Session openSession(ConnectionInfo ci, boolean ifExists, |
44 | String cipher) { |
45 | String name = ci.getName(); |
46 | Database database; |
47 | ci.removeProperty("NO_UPGRADE", false); |
48 | boolean openNew = ci.getProperty("OPEN_NEW", false); |
49 | if (openNew || ci.isUnnamedInMemory()) { |
50 | database = null; |
51 | } else { |
52 | database = DATABASES.get(name); |
53 | } |
54 | User user = null; |
55 | boolean opened = false; |
56 | if (database == null) { |
57 | if (ifExists && !Database.exists(name)) { |
58 | throw DbException.get(ErrorCode.DATABASE_NOT_FOUND_1, name); |
59 | } |
60 | database = new Database(ci, cipher); |
61 | opened = true; |
62 | if (database.getAllUsers().size() == 0) { |
63 | // users is the last thing we add, so if no user is around, |
64 | // the database is new (or not initialized correctly) |
65 | user = new User(database, database.allocateObjectId(), |
66 | ci.getUserName(), false); |
67 | user.setAdmin(true); |
68 | user.setUserPasswordHash(ci.getUserPasswordHash()); |
69 | database.setMasterUser(user); |
70 | } |
71 | if (!ci.isUnnamedInMemory()) { |
72 | DATABASES.put(name, database); |
73 | } |
74 | } |
75 | synchronized (database) { |
76 | if (opened) { |
77 | // start the thread when already synchronizing on the database |
78 | // otherwise a deadlock can occur when the writer thread |
79 | // opens a new database (as in recovery testing) |
80 | database.opened(); |
81 | } |
82 | if (database.isClosing()) { |
83 | return null; |
84 | } |
85 | if (user == null) { |
86 | if (database.validateFilePasswordHash(cipher, ci.getFilePasswordHash())) { |
87 | user = database.findUser(ci.getUserName()); |
88 | if (user != null) { |
89 | if (!user.validateUserPasswordHash(ci.getUserPasswordHash())) { |
90 | user = null; |
91 | } |
92 | } |
93 | } |
94 | if (opened && (user == null || !user.isAdmin())) { |
95 | // reset - because the user is not an admin, and has no |
96 | // right to listen to exceptions |
97 | database.setEventListener(null); |
98 | } |
99 | } |
100 | if (user == null) { |
101 | database.removeSession(null); |
102 | throw DbException.get(ErrorCode.WRONG_USER_OR_PASSWORD); |
103 | } |
104 | checkClustering(ci, database); |
105 | Session session = database.createSession(user); |
106 | if (ci.getProperty("JMX", false)) { |
107 | try { |
108 | Utils.callStaticMethod( |
109 | "org.h2.jmx.DatabaseInfo.registerMBean", ci, database); |
110 | } catch (Exception e) { |
111 | database.removeSession(session); |
112 | throw DbException.get(ErrorCode.FEATURE_NOT_SUPPORTED_1, e, "JMX"); |
113 | } |
114 | jmx = true; |
115 | } |
116 | return session; |
117 | } |
118 | } |
119 | |
120 | /** |
121 | * Open a database connection with the given connection information. |
122 | * |
123 | * @param ci the connection information |
124 | * @return the session |
125 | */ |
126 | @Override |
127 | public Session createSession(ConnectionInfo ci) { |
128 | return INSTANCE.createSessionAndValidate(ci); |
129 | } |
130 | |
131 | private Session createSessionAndValidate(ConnectionInfo ci) { |
132 | try { |
133 | ConnectionInfo backup = null; |
134 | String lockMethodName = ci.getProperty("FILE_LOCK", null); |
135 | int fileLockMethod = FileLock.getFileLockMethod(lockMethodName); |
136 | if (fileLockMethod == FileLock.LOCK_SERIALIZED) { |
137 | // In serialized mode, database instance sharing is not possible |
138 | ci.setProperty("OPEN_NEW", "TRUE"); |
139 | try { |
140 | backup = ci.clone(); |
141 | } catch (CloneNotSupportedException e) { |
142 | throw DbException.convert(e); |
143 | } |
144 | } |
145 | Session session = openSession(ci); |
146 | validateUserAndPassword(true); |
147 | if (backup != null) { |
148 | session.setConnectionInfo(backup); |
149 | } |
150 | return session; |
151 | } catch (DbException e) { |
152 | if (e.getErrorCode() == ErrorCode.WRONG_USER_OR_PASSWORD) { |
153 | validateUserAndPassword(false); |
154 | } |
155 | throw e; |
156 | } |
157 | } |
158 | |
159 | private synchronized Session openSession(ConnectionInfo ci) { |
160 | boolean ifExists = ci.removeProperty("IFEXISTS", false); |
161 | boolean ignoreUnknownSetting = ci.removeProperty( |
162 | "IGNORE_UNKNOWN_SETTINGS", false); |
163 | String cipher = ci.removeProperty("CIPHER", null); |
164 | String init = ci.removeProperty("INIT", null); |
165 | Session session; |
166 | for (int i = 0;; i++) { |
167 | session = openSession(ci, ifExists, cipher); |
168 | if (session != null) { |
169 | break; |
170 | } |
171 | // we found a database that is currently closing |
172 | // wait a bit to avoid a busy loop (the method is synchronized) |
173 | if (i > 60 * 1000) { |
174 | // retry at most 1 minute |
175 | throw DbException.get(ErrorCode.DATABASE_ALREADY_OPEN_1, |
176 | "Waited for database closing longer than 1 minute"); |
177 | } |
178 | try { |
179 | Thread.sleep(1); |
180 | } catch (InterruptedException e) { |
181 | // ignore |
182 | } |
183 | } |
184 | session.setAllowLiterals(true); |
185 | DbSettings defaultSettings = DbSettings.getDefaultSettings(); |
186 | for (String setting : ci.getKeys()) { |
187 | if (defaultSettings.containsKey(setting)) { |
188 | // database setting are only used when opening the database |
189 | continue; |
190 | } |
191 | String value = ci.getProperty(setting); |
192 | try { |
193 | CommandInterface command = session.prepareCommand( |
194 | "SET " + Parser.quoteIdentifier(setting) + " " + value, |
195 | Integer.MAX_VALUE); |
196 | command.executeUpdate(); |
197 | } catch (DbException e) { |
198 | if (!ignoreUnknownSetting) { |
199 | session.close(); |
200 | throw e; |
201 | } |
202 | } |
203 | } |
204 | if (init != null) { |
205 | try { |
206 | CommandInterface command = session.prepareCommand(init, |
207 | Integer.MAX_VALUE); |
208 | command.executeUpdate(); |
209 | } catch (DbException e) { |
210 | if (!ignoreUnknownSetting) { |
211 | session.close(); |
212 | throw e; |
213 | } |
214 | } |
215 | } |
216 | session.setAllowLiterals(false); |
217 | session.commit(true); |
218 | return session; |
219 | } |
220 | |
221 | private static void checkClustering(ConnectionInfo ci, Database database) { |
222 | String clusterSession = ci.getProperty(SetTypes.CLUSTER, null); |
223 | if (Constants.CLUSTERING_DISABLED.equals(clusterSession)) { |
224 | // in this case, no checking is made |
225 | // (so that a connection can be made to disable/change clustering) |
226 | return; |
227 | } |
228 | String clusterDb = database.getCluster(); |
229 | if (!Constants.CLUSTERING_DISABLED.equals(clusterDb)) { |
230 | if (!Constants.CLUSTERING_ENABLED.equals(clusterSession)) { |
231 | if (!StringUtils.equals(clusterSession, clusterDb)) { |
232 | if (clusterDb.equals(Constants.CLUSTERING_DISABLED)) { |
233 | throw DbException.get( |
234 | ErrorCode.CLUSTER_ERROR_DATABASE_RUNS_ALONE); |
235 | } |
236 | throw DbException.get( |
237 | ErrorCode.CLUSTER_ERROR_DATABASE_RUNS_CLUSTERED_1, |
238 | clusterDb); |
239 | } |
240 | } |
241 | } |
242 | } |
243 | |
244 | /** |
245 | * Called after a database has been closed, to remove the object from the |
246 | * list of open databases. |
247 | * |
248 | * @param name the database name |
249 | */ |
250 | void close(String name) { |
251 | if (jmx) { |
252 | try { |
253 | Utils.callStaticMethod("org.h2.jmx.DatabaseInfo.unregisterMBean", name); |
254 | } catch (Exception e) { |
255 | throw DbException.get(ErrorCode.FEATURE_NOT_SUPPORTED_1, e, "JMX"); |
256 | } |
257 | } |
258 | DATABASES.remove(name); |
259 | } |
260 | |
261 | /** |
262 | * This method is called after validating user name and password. If user |
263 | * name and password were correct, the sleep time is reset, otherwise this |
264 | * method waits some time (to make brute force / rainbow table attacks |
265 | * harder) and then throws a 'wrong user or password' exception. The delay |
266 | * is a bit randomized to protect against timing attacks. Also the delay |
267 | * doubles after each unsuccessful logins, to make brute force attacks |
268 | * harder. |
269 | * |
270 | * There is only one exception message both for wrong user and for |
271 | * wrong password, to make it harder to get the list of user names. This |
272 | * method must only be called from one place, so it is not possible from the |
273 | * stack trace to see if the user name was wrong or the password. |
274 | * |
275 | * @param correct if the user name or the password was correct |
276 | * @throws DbException the exception 'wrong user or password' |
277 | */ |
278 | private void validateUserAndPassword(boolean correct) { |
279 | int min = SysProperties.DELAY_WRONG_PASSWORD_MIN; |
280 | if (correct) { |
281 | long delay = wrongPasswordDelay; |
282 | if (delay > min && delay > 0) { |
283 | // the first correct password must be blocked, |
284 | // otherwise parallel attacks are possible |
285 | synchronized (INSTANCE) { |
286 | // delay up to the last delay |
287 | // an attacker can't know how long it will be |
288 | delay = MathUtils.secureRandomInt((int) delay); |
289 | try { |
290 | Thread.sleep(delay); |
291 | } catch (InterruptedException e) { |
292 | // ignore |
293 | } |
294 | wrongPasswordDelay = min; |
295 | } |
296 | } |
297 | } else { |
298 | // this method is not synchronized on the Engine, so that |
299 | // regular successful attempts are not blocked |
300 | synchronized (INSTANCE) { |
301 | long delay = wrongPasswordDelay; |
302 | int max = SysProperties.DELAY_WRONG_PASSWORD_MAX; |
303 | if (max <= 0) { |
304 | max = Integer.MAX_VALUE; |
305 | } |
306 | wrongPasswordDelay += wrongPasswordDelay; |
307 | if (wrongPasswordDelay > max || wrongPasswordDelay < 0) { |
308 | wrongPasswordDelay = max; |
309 | } |
310 | if (min > 0) { |
311 | // a bit more to protect against timing attacks |
312 | delay += Math.abs(MathUtils.secureRandomLong() % 100); |
313 | try { |
314 | Thread.sleep(delay); |
315 | } catch (InterruptedException e) { |
316 | // ignore |
317 | } |
318 | } |
319 | throw DbException.get(ErrorCode.WRONG_USER_OR_PASSWORD); |
320 | } |
321 | } |
322 | } |
323 | |
324 | } |