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.io.IOException; |
9 | import java.util.ArrayList; |
10 | import java.util.Arrays; |
11 | import java.util.HashMap; |
12 | import java.util.HashSet; |
13 | import java.util.Properties; |
14 | |
15 | import org.h2.api.ErrorCode; |
16 | import org.h2.command.dml.SetTypes; |
17 | import org.h2.message.DbException; |
18 | import org.h2.security.SHA256; |
19 | import org.h2.store.fs.FilePathEncrypt; |
20 | import org.h2.store.fs.FilePathRec; |
21 | import org.h2.store.fs.FileUtils; |
22 | import org.h2.util.New; |
23 | import org.h2.util.SortedProperties; |
24 | import org.h2.util.StringUtils; |
25 | import org.h2.util.Utils; |
26 | |
27 | /** |
28 | * Encapsulates the connection settings, including user name and password. |
29 | */ |
30 | public class ConnectionInfo implements Cloneable { |
31 | private static final HashSet<String> KNOWN_SETTINGS = New.hashSet(); |
32 | |
33 | private Properties prop = new Properties(); |
34 | private String originalURL; |
35 | private String url; |
36 | private String user; |
37 | private byte[] filePasswordHash; |
38 | private byte[] fileEncryptionKey; |
39 | private byte[] userPasswordHash; |
40 | |
41 | /** |
42 | * The database name |
43 | */ |
44 | private String name; |
45 | private String nameNormalized; |
46 | private boolean remote; |
47 | private boolean ssl; |
48 | private boolean persistent; |
49 | private boolean unnamed; |
50 | |
51 | /** |
52 | * Create a connection info object. |
53 | * |
54 | * @param name the database name (including tags), but without the |
55 | * "jdbc:h2:" prefix |
56 | */ |
57 | public ConnectionInfo(String name) { |
58 | this.name = name; |
59 | this.url = Constants.START_URL + name; |
60 | parseName(); |
61 | } |
62 | |
63 | /** |
64 | * Create a connection info object. |
65 | * |
66 | * @param u the database URL (must start with jdbc:h2:) |
67 | * @param info the connection properties |
68 | */ |
69 | public ConnectionInfo(String u, Properties info) { |
70 | u = remapURL(u); |
71 | this.originalURL = u; |
72 | if (!u.startsWith(Constants.START_URL)) { |
73 | throw DbException.getInvalidValueException("url", u); |
74 | } |
75 | this.url = u; |
76 | readProperties(info); |
77 | readSettingsFromURL(); |
78 | setUserName(removeProperty("USER", "")); |
79 | convertPasswords(); |
80 | name = url.substring(Constants.START_URL.length()); |
81 | parseName(); |
82 | String recoverTest = removeProperty("RECOVER_TEST", null); |
83 | if (recoverTest != null) { |
84 | FilePathRec.register(); |
85 | try { |
86 | Utils.callStaticMethod("org.h2.store.RecoverTester.init", recoverTest); |
87 | } catch (Exception e) { |
88 | throw DbException.convert(e); |
89 | } |
90 | name = "rec:" + name; |
91 | } |
92 | } |
93 | |
94 | static { |
95 | ArrayList<String> list = SetTypes.getTypes(); |
96 | HashSet<String> set = KNOWN_SETTINGS; |
97 | set.addAll(list); |
98 | String[] connectionTime = { "ACCESS_MODE_DATA", "AUTOCOMMIT", "CIPHER", |
99 | "CREATE", "CACHE_TYPE", "FILE_LOCK", "IGNORE_UNKNOWN_SETTINGS", |
100 | "IFEXISTS", "INIT", "PASSWORD", "RECOVER", "RECOVER_TEST", |
101 | "USER", "AUTO_SERVER", "AUTO_SERVER_PORT", "NO_UPGRADE", |
102 | "AUTO_RECONNECT", "OPEN_NEW", "PAGE_SIZE", "PASSWORD_HASH", "JMX" }; |
103 | for (String key : connectionTime) { |
104 | if (SysProperties.CHECK && set.contains(key)) { |
105 | DbException.throwInternalError(key); |
106 | } |
107 | set.add(key); |
108 | } |
109 | } |
110 | |
111 | private static boolean isKnownSetting(String s) { |
112 | return KNOWN_SETTINGS.contains(s); |
113 | } |
114 | |
115 | @Override |
116 | public ConnectionInfo clone() throws CloneNotSupportedException { |
117 | ConnectionInfo clone = (ConnectionInfo) super.clone(); |
118 | clone.prop = (Properties) prop.clone(); |
119 | clone.filePasswordHash = Utils.cloneByteArray(filePasswordHash); |
120 | clone.fileEncryptionKey = Utils.cloneByteArray(fileEncryptionKey); |
121 | clone.userPasswordHash = Utils.cloneByteArray(userPasswordHash); |
122 | return clone; |
123 | } |
124 | |
125 | private void parseName() { |
126 | if (".".equals(name)) { |
127 | name = "mem:"; |
128 | } |
129 | if (name.startsWith("tcp:")) { |
130 | remote = true; |
131 | name = name.substring("tcp:".length()); |
132 | } else if (name.startsWith("ssl:")) { |
133 | remote = true; |
134 | ssl = true; |
135 | name = name.substring("ssl:".length()); |
136 | } else if (name.startsWith("mem:")) { |
137 | persistent = false; |
138 | if ("mem:".equals(name)) { |
139 | unnamed = true; |
140 | } |
141 | } else if (name.startsWith("file:")) { |
142 | name = name.substring("file:".length()); |
143 | persistent = true; |
144 | } else { |
145 | persistent = true; |
146 | } |
147 | if (persistent && !remote) { |
148 | if ("/".equals(SysProperties.FILE_SEPARATOR)) { |
149 | name = name.replace('\\', '/'); |
150 | } else { |
151 | name = name.replace('/', '\\'); |
152 | } |
153 | } |
154 | } |
155 | |
156 | /** |
157 | * Set the base directory of persistent databases, unless the database is in |
158 | * the user home folder (~). |
159 | * |
160 | * @param dir the new base directory |
161 | */ |
162 | public void setBaseDir(String dir) { |
163 | if (persistent) { |
164 | String absDir = FileUtils.unwrap(FileUtils.toRealPath(dir)); |
165 | boolean absolute = FileUtils.isAbsolute(name); |
166 | String n; |
167 | String prefix = null; |
168 | if (dir.endsWith(SysProperties.FILE_SEPARATOR)) { |
169 | dir = dir.substring(0, dir.length() - 1); |
170 | } |
171 | if (absolute) { |
172 | n = name; |
173 | } else { |
174 | n = FileUtils.unwrap(name); |
175 | prefix = name.substring(0, name.length() - n.length()); |
176 | n = dir + SysProperties.FILE_SEPARATOR + n; |
177 | } |
178 | String normalizedName = FileUtils.unwrap(FileUtils.toRealPath(n)); |
179 | if (normalizedName.equals(absDir) || !normalizedName.startsWith(absDir)) { |
180 | // database name matches the baseDir or |
181 | // database name is clearly outside of the baseDir |
182 | throw DbException.get(ErrorCode.IO_EXCEPTION_1, normalizedName + " outside " + |
183 | absDir); |
184 | } |
185 | if (absDir.endsWith("/") || absDir.endsWith("\\")) { |
186 | // no further checks are needed for C:/ and similar |
187 | } else if (normalizedName.charAt(absDir.length()) != '/') { |
188 | // database must be within the directory |
189 | // (with baseDir=/test, the database name must not be |
190 | // /test2/x and not /test2) |
191 | throw DbException.get(ErrorCode.IO_EXCEPTION_1, normalizedName + " outside " + |
192 | absDir); |
193 | } |
194 | if (!absolute) { |
195 | name = prefix + dir + SysProperties.FILE_SEPARATOR + FileUtils.unwrap(name); |
196 | } |
197 | } |
198 | } |
199 | |
200 | /** |
201 | * Check if this is a remote connection. |
202 | * |
203 | * @return true if it is |
204 | */ |
205 | public boolean isRemote() { |
206 | return remote; |
207 | } |
208 | |
209 | /** |
210 | * Check if the referenced database is persistent. |
211 | * |
212 | * @return true if it is |
213 | */ |
214 | public boolean isPersistent() { |
215 | return persistent; |
216 | } |
217 | |
218 | /** |
219 | * Check if the referenced database is an unnamed in-memory database. |
220 | * |
221 | * @return true if it is |
222 | */ |
223 | boolean isUnnamedInMemory() { |
224 | return unnamed; |
225 | } |
226 | |
227 | private void readProperties(Properties info) { |
228 | Object[] list = new Object[info.size()]; |
229 | info.keySet().toArray(list); |
230 | DbSettings s = null; |
231 | for (Object k : list) { |
232 | String key = StringUtils.toUpperEnglish(k.toString()); |
233 | if (prop.containsKey(key)) { |
234 | throw DbException.get(ErrorCode.DUPLICATE_PROPERTY_1, key); |
235 | } |
236 | Object value = info.get(k); |
237 | if (isKnownSetting(key)) { |
238 | prop.put(key, value); |
239 | } else { |
240 | if (s == null) { |
241 | s = getDbSettings(); |
242 | } |
243 | if (s.containsKey(key)) { |
244 | prop.put(key, value); |
245 | } |
246 | } |
247 | } |
248 | } |
249 | |
250 | private void readSettingsFromURL() { |
251 | DbSettings defaultSettings = DbSettings.getDefaultSettings(); |
252 | int idx = url.indexOf(';'); |
253 | if (idx >= 0) { |
254 | String settings = url.substring(idx + 1); |
255 | url = url.substring(0, idx); |
256 | String[] list = StringUtils.arraySplit(settings, ';', false); |
257 | for (String setting : list) { |
258 | if (setting.length() == 0) { |
259 | continue; |
260 | } |
261 | int equal = setting.indexOf('='); |
262 | if (equal < 0) { |
263 | throw getFormatException(); |
264 | } |
265 | String value = setting.substring(equal + 1); |
266 | String key = setting.substring(0, equal); |
267 | key = StringUtils.toUpperEnglish(key); |
268 | if (!isKnownSetting(key) && !defaultSettings.containsKey(key)) { |
269 | throw DbException.get(ErrorCode.UNSUPPORTED_SETTING_1, key); |
270 | } |
271 | String old = prop.getProperty(key); |
272 | if (old != null && !old.equals(value)) { |
273 | throw DbException.get(ErrorCode.DUPLICATE_PROPERTY_1, key); |
274 | } |
275 | prop.setProperty(key, value); |
276 | } |
277 | } |
278 | } |
279 | |
280 | private char[] removePassword() { |
281 | Object p = prop.remove("PASSWORD"); |
282 | if (p == null) { |
283 | return new char[0]; |
284 | } else if (p instanceof char[]) { |
285 | return (char[]) p; |
286 | } else { |
287 | return p.toString().toCharArray(); |
288 | } |
289 | } |
290 | |
291 | /** |
292 | * Split the password property into file password and user password if |
293 | * necessary, and convert them to the internal hash format. |
294 | */ |
295 | private void convertPasswords() { |
296 | char[] password = removePassword(); |
297 | boolean passwordHash = removeProperty("PASSWORD_HASH", false); |
298 | if (getProperty("CIPHER", null) != null) { |
299 | // split password into (filePassword+' '+userPassword) |
300 | int space = -1; |
301 | for (int i = 0, len = password.length; i < len; i++) { |
302 | if (password[i] == ' ') { |
303 | space = i; |
304 | break; |
305 | } |
306 | } |
307 | if (space < 0) { |
308 | throw DbException.get(ErrorCode.WRONG_PASSWORD_FORMAT); |
309 | } |
310 | char[] np = new char[password.length - space - 1]; |
311 | char[] filePassword = new char[space]; |
312 | System.arraycopy(password, space + 1, np, 0, np.length); |
313 | System.arraycopy(password, 0, filePassword, 0, space); |
314 | Arrays.fill(password, (char) 0); |
315 | password = np; |
316 | fileEncryptionKey = FilePathEncrypt.getPasswordBytes(filePassword); |
317 | filePasswordHash = hashPassword(passwordHash, "file", filePassword); |
318 | } |
319 | userPasswordHash = hashPassword(passwordHash, user, password); |
320 | } |
321 | |
322 | private static byte[] hashPassword(boolean passwordHash, String userName, |
323 | char[] password) { |
324 | if (passwordHash) { |
325 | return StringUtils.convertHexToBytes(new String(password)); |
326 | } |
327 | if (userName.length() == 0 && password.length == 0) { |
328 | return new byte[0]; |
329 | } |
330 | return SHA256.getKeyPasswordHash(userName, password); |
331 | } |
332 | |
333 | /** |
334 | * Get a boolean property if it is set and return the value. |
335 | * |
336 | * @param key the property name |
337 | * @param defaultValue the default value |
338 | * @return the value |
339 | */ |
340 | boolean getProperty(String key, boolean defaultValue) { |
341 | String x = getProperty(key, null); |
342 | if (x == null) { |
343 | return defaultValue; |
344 | } |
345 | // support 0 / 1 (like the parser) |
346 | if (x.length() == 1 && Character.isDigit(x.charAt(0))) { |
347 | return Integer.parseInt(x) != 0; |
348 | } |
349 | return Boolean.parseBoolean(x); |
350 | } |
351 | |
352 | /** |
353 | * Remove a boolean property if it is set and return the value. |
354 | * |
355 | * @param key the property name |
356 | * @param defaultValue the default value |
357 | * @return the value |
358 | */ |
359 | public boolean removeProperty(String key, boolean defaultValue) { |
360 | String x = removeProperty(key, null); |
361 | return x == null ? defaultValue : Boolean.parseBoolean(x); |
362 | } |
363 | |
364 | /** |
365 | * Remove a String property if it is set and return the value. |
366 | * |
367 | * @param key the property name |
368 | * @param defaultValue the default value |
369 | * @return the value |
370 | */ |
371 | String removeProperty(String key, String defaultValue) { |
372 | if (SysProperties.CHECK && !isKnownSetting(key)) { |
373 | DbException.throwInternalError(key); |
374 | } |
375 | Object x = prop.remove(key); |
376 | return x == null ? defaultValue : x.toString(); |
377 | } |
378 | |
379 | /** |
380 | * Get the unique and normalized database name (excluding settings). |
381 | * |
382 | * @return the database name |
383 | */ |
384 | public String getName() { |
385 | if (persistent) { |
386 | if (nameNormalized == null) { |
387 | if (!SysProperties.IMPLICIT_RELATIVE_PATH) { |
388 | if (!FileUtils.isAbsolute(name)) { |
389 | if (name.indexOf("./") < 0 && |
390 | name.indexOf(".\\") < 0 && |
391 | name.indexOf(":/") < 0 && |
392 | name.indexOf(":\\") < 0) { |
393 | // the name could start with "./", or |
394 | // it could start with a prefix such as "nio:./" |
395 | // for Windows, the path "\test" is not considered |
396 | // absolute as the drive letter is missing, |
397 | // but we consider it absolute |
398 | throw DbException.get( |
399 | ErrorCode.URL_RELATIVE_TO_CWD, |
400 | originalURL); |
401 | } |
402 | } |
403 | } |
404 | String suffix = Constants.SUFFIX_PAGE_FILE; |
405 | String n; |
406 | if (FileUtils.exists(name + suffix)) { |
407 | n = FileUtils.toRealPath(name + suffix); |
408 | } else { |
409 | suffix = Constants.SUFFIX_MV_FILE; |
410 | n = FileUtils.toRealPath(name + suffix); |
411 | } |
412 | String fileName = FileUtils.getName(n); |
413 | if (fileName.length() < suffix.length() + 1) { |
414 | throw DbException.get(ErrorCode.INVALID_DATABASE_NAME_1, name); |
415 | } |
416 | nameNormalized = n.substring(0, n.length() - suffix.length()); |
417 | } |
418 | return nameNormalized; |
419 | } |
420 | return name; |
421 | } |
422 | |
423 | /** |
424 | * Get the file password hash if it is set. |
425 | * |
426 | * @return the password hash or null |
427 | */ |
428 | public byte[] getFilePasswordHash() { |
429 | return filePasswordHash; |
430 | } |
431 | |
432 | byte[] getFileEncryptionKey() { |
433 | return fileEncryptionKey; |
434 | } |
435 | |
436 | /** |
437 | * Get the name of the user. |
438 | * |
439 | * @return the user name |
440 | */ |
441 | public String getUserName() { |
442 | return user; |
443 | } |
444 | |
445 | /** |
446 | * Get the user password hash. |
447 | * |
448 | * @return the password hash |
449 | */ |
450 | byte[] getUserPasswordHash() { |
451 | return userPasswordHash; |
452 | } |
453 | |
454 | /** |
455 | * Get the property keys. |
456 | * |
457 | * @return the property keys |
458 | */ |
459 | String[] getKeys() { |
460 | String[] keys = new String[prop.size()]; |
461 | prop.keySet().toArray(keys); |
462 | return keys; |
463 | } |
464 | |
465 | /** |
466 | * Get the value of the given property. |
467 | * |
468 | * @param key the property key |
469 | * @return the value as a String |
470 | */ |
471 | String getProperty(String key) { |
472 | Object value = prop.get(key); |
473 | if (value == null || !(value instanceof String)) { |
474 | return null; |
475 | } |
476 | return value.toString(); |
477 | } |
478 | |
479 | /** |
480 | * Get the value of the given property. |
481 | * |
482 | * @param key the property key |
483 | * @param defaultValue the default value |
484 | * @return the value as a String |
485 | */ |
486 | int getProperty(String key, int defaultValue) { |
487 | if (SysProperties.CHECK && !isKnownSetting(key)) { |
488 | DbException.throwInternalError(key); |
489 | } |
490 | String s = getProperty(key); |
491 | return s == null ? defaultValue : Integer.parseInt(s); |
492 | } |
493 | |
494 | /** |
495 | * Get the value of the given property. |
496 | * |
497 | * @param key the property key |
498 | * @param defaultValue the default value |
499 | * @return the value as a String |
500 | */ |
501 | public String getProperty(String key, String defaultValue) { |
502 | if (SysProperties.CHECK && !isKnownSetting(key)) { |
503 | DbException.throwInternalError(key); |
504 | } |
505 | String s = getProperty(key); |
506 | return s == null ? defaultValue : s; |
507 | } |
508 | |
509 | /** |
510 | * Get the value of the given property. |
511 | * |
512 | * @param setting the setting id |
513 | * @param defaultValue the default value |
514 | * @return the value as a String |
515 | */ |
516 | String getProperty(int setting, String defaultValue) { |
517 | String key = SetTypes.getTypeName(setting); |
518 | String s = getProperty(key); |
519 | return s == null ? defaultValue : s; |
520 | } |
521 | |
522 | /** |
523 | * Get the value of the given property. |
524 | * |
525 | * @param setting the setting id |
526 | * @param defaultValue the default value |
527 | * @return the value as an integer |
528 | */ |
529 | int getIntProperty(int setting, int defaultValue) { |
530 | String key = SetTypes.getTypeName(setting); |
531 | String s = getProperty(key, null); |
532 | try { |
533 | return s == null ? defaultValue : Integer.decode(s); |
534 | } catch (NumberFormatException e) { |
535 | return defaultValue; |
536 | } |
537 | } |
538 | |
539 | /** |
540 | * Check if this is a remote connection with SSL enabled. |
541 | * |
542 | * @return true if it is |
543 | */ |
544 | boolean isSSL() { |
545 | return ssl; |
546 | } |
547 | |
548 | /** |
549 | * Overwrite the user name. The user name is case-insensitive and stored in |
550 | * uppercase. English conversion is used. |
551 | * |
552 | * @param name the user name |
553 | */ |
554 | public void setUserName(String name) { |
555 | this.user = StringUtils.toUpperEnglish(name); |
556 | } |
557 | |
558 | /** |
559 | * Set the user password hash. |
560 | * |
561 | * @param hash the new hash value |
562 | */ |
563 | public void setUserPasswordHash(byte[] hash) { |
564 | this.userPasswordHash = hash; |
565 | } |
566 | |
567 | /** |
568 | * Set the file password hash. |
569 | * |
570 | * @param hash the new hash value |
571 | */ |
572 | public void setFilePasswordHash(byte[] hash) { |
573 | this.filePasswordHash = hash; |
574 | } |
575 | |
576 | public void setFileEncryptionKey(byte[] key) { |
577 | this.fileEncryptionKey = key; |
578 | } |
579 | |
580 | /** |
581 | * Overwrite a property. |
582 | * |
583 | * @param key the property name |
584 | * @param value the value |
585 | */ |
586 | public void setProperty(String key, String value) { |
587 | // value is null if the value is an object |
588 | if (value != null) { |
589 | prop.setProperty(key, value); |
590 | } |
591 | } |
592 | |
593 | /** |
594 | * Get the database URL. |
595 | * |
596 | * @return the URL |
597 | */ |
598 | public String getURL() { |
599 | return url; |
600 | } |
601 | |
602 | /** |
603 | * Get the complete original database URL. |
604 | * |
605 | * @return the database URL |
606 | */ |
607 | public String getOriginalURL() { |
608 | return originalURL; |
609 | } |
610 | |
611 | /** |
612 | * Set the original database URL. |
613 | * |
614 | * @param url the database url |
615 | */ |
616 | public void setOriginalURL(String url) { |
617 | originalURL = url; |
618 | } |
619 | |
620 | /** |
621 | * Generate an URL format exception. |
622 | * |
623 | * @return the exception |
624 | */ |
625 | DbException getFormatException() { |
626 | String format = Constants.URL_FORMAT; |
627 | return DbException.get(ErrorCode.URL_FORMAT_ERROR_2, format, url); |
628 | } |
629 | |
630 | /** |
631 | * Switch to server mode, and set the server name and database key. |
632 | * |
633 | * @param serverKey the server name, '/', and the security key |
634 | */ |
635 | public void setServerKey(String serverKey) { |
636 | remote = true; |
637 | persistent = false; |
638 | this.name = serverKey; |
639 | } |
640 | |
641 | public DbSettings getDbSettings() { |
642 | DbSettings defaultSettings = DbSettings.getDefaultSettings(); |
643 | HashMap<String, String> s = New.hashMap(); |
644 | for (Object k : prop.keySet()) { |
645 | String key = k.toString(); |
646 | if (!isKnownSetting(key) && defaultSettings.containsKey(key)) { |
647 | s.put(key, prop.getProperty(key)); |
648 | } |
649 | } |
650 | return DbSettings.getInstance(s); |
651 | } |
652 | |
653 | private static String remapURL(String url) { |
654 | String urlMap = SysProperties.URL_MAP; |
655 | if (urlMap != null && urlMap.length() > 0) { |
656 | try { |
657 | SortedProperties prop; |
658 | prop = SortedProperties.loadProperties(urlMap); |
659 | String url2 = prop.getProperty(url); |
660 | if (url2 == null) { |
661 | prop.put(url, ""); |
662 | prop.store(urlMap); |
663 | } else { |
664 | url2 = url2.trim(); |
665 | if (url2.length() > 0) { |
666 | return url2; |
667 | } |
668 | } |
669 | } catch (IOException e) { |
670 | throw DbException.convert(e); |
671 | } |
672 | } |
673 | return url; |
674 | } |
675 | |
676 | } |