]> git.argeo.org Git - lgpl/argeo-commons.git/blob - org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java
Fix IPA initialisation
[lgpl/argeo-commons.git] / org.argeo.cms / src / org / argeo / cms / internal / runtime / CmsStateImpl.java
1 package org.argeo.cms.internal.runtime;
2
3 import java.io.BufferedInputStream;
4 import java.io.IOException;
5 import java.io.Reader;
6 import java.net.Inet6Address;
7 import java.net.InetAddress;
8 import java.net.URL;
9 import java.net.UnknownHostException;
10 import java.nio.charset.Charset;
11 import java.nio.charset.StandardCharsets;
12 import java.nio.file.Files;
13 import java.nio.file.Path;
14 import java.nio.file.Paths;
15 import java.nio.file.attribute.PosixFilePermission;
16 import java.security.KeyStore;
17 import java.util.ArrayList;
18 import java.util.Arrays;
19 import java.util.Collections;
20 import java.util.HashMap;
21 import java.util.HashSet;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25 import java.util.Objects;
26 import java.util.Set;
27 import java.util.StringJoiner;
28 import java.util.TreeMap;
29 import java.util.UUID;
30 import java.util.concurrent.ExecutionException;
31 import java.util.concurrent.ForkJoinPool;
32 import java.util.concurrent.ForkJoinTask;
33 import java.util.concurrent.TimeUnit;
34 import java.util.concurrent.TimeoutException;
35
36 import javax.security.auth.login.Configuration;
37
38 import org.argeo.api.cms.CmsConstants;
39 import org.argeo.api.cms.CmsLog;
40 import org.argeo.api.cms.CmsState;
41 import org.argeo.api.uuid.NodeIdSupplier;
42 import org.argeo.api.uuid.UuidBinaryUtils;
43 import org.argeo.cms.CmsDeployProperty;
44 import org.argeo.cms.auth.ident.IdentClient;
45 import org.argeo.cms.util.DigestUtils;
46 import org.argeo.cms.util.FsUtils;
47 import org.argeo.cms.util.OS;
48
49 /**
50 * Implementation of a {@link CmsState}, initialising the required services.
51 */
52 public class CmsStateImpl implements CmsState, NodeIdSupplier {
53 private final static CmsLog log = CmsLog.getLog(CmsStateImpl.class);
54
55 // REFERENCES
56 private Long availableSince;
57
58 private UUID uuid;
59 // private final boolean cleanState;
60 private String hostname;
61 private InetAddress inetAddress;
62
63 // private UuidFactory uuidFactory;
64
65 private final Map<CmsDeployProperty, String> deployPropertyDefaults;
66
67 public CmsStateImpl() {
68 this.deployPropertyDefaults = Collections.unmodifiableMap(createDeployPropertiesDefaults());
69 }
70
71 protected Map<CmsDeployProperty, String> createDeployPropertiesDefaults() {
72 Map<CmsDeployProperty, String> deployPropertyDefaults = new HashMap<>();
73 deployPropertyDefaults.put(CmsDeployProperty.NODE_INIT, "../../init");
74 deployPropertyDefaults.put(CmsDeployProperty.LOCALE, Locale.getDefault().toString());
75
76 // certificates
77 deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORETYPE, KernelConstants.PKCS12);
78 deployPropertyDefaults.put(CmsDeployProperty.SSL_PASSWORD, KernelConstants.DEFAULT_KEYSTORE_PASSWORD);
79 Path keyStorePath = getDataPath(KernelConstants.DEFAULT_KEYSTORE_PATH);
80 if (keyStorePath != null) {
81 deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORE, keyStorePath.toAbsolutePath().toString());
82 }
83
84 Path trustStorePath = getDataPath(KernelConstants.DEFAULT_TRUSTSTORE_PATH);
85 if (trustStorePath != null) {
86 deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORE, trustStorePath.toAbsolutePath().toString());
87 }
88 deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORETYPE, KernelConstants.PKCS12);
89 deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD, KernelConstants.DEFAULT_KEYSTORE_PASSWORD);
90
91 // SSH
92 Path authorizedKeysPath = getDataPath(KernelConstants.NODE_SSHD_AUTHORIZED_KEYS_PATH);
93 if (authorizedKeysPath != null) {
94 deployPropertyDefaults.put(CmsDeployProperty.SSHD_AUTHORIZEDKEYS,
95 authorizedKeysPath.toAbsolutePath().toString());
96 }
97 return deployPropertyDefaults;
98 }
99
100 public void start() {
101 Charset defaultCharset = Charset.defaultCharset();
102 if (!StandardCharsets.UTF_8.equals(defaultCharset))
103 log.error("Default JVM charset is " + defaultCharset + " and not " + StandardCharsets.UTF_8);
104 try {
105 // First init check
106 Path privateBase = getDataPath(KernelConstants.DIR_PRIVATE);
107 if (privateBase != null && !Files.exists(privateBase)) {// first init
108 firstInit();
109 Files.createDirectories(privateBase);
110 }
111
112 initSecurity();
113 // initArgeoLogger();
114
115 if (log.isTraceEnabled())
116 log.trace("CMS State started");
117
118 String frameworkUuid = KernelUtils.getFrameworkProp(KernelUtils.OSGI_FRAMEWORK_UUID);
119 this.uuid = frameworkUuid != null ? UUID.fromString(frameworkUuid) : UUID.randomUUID();
120
121 // hostname
122 this.hostname = getDeployProperty(CmsDeployProperty.HOST);
123 // TODO verify we have access to the IP address
124 if (hostname == null) {
125 final String LOCALHOST_IP = "::1";
126 ForkJoinTask<String> hostnameFJT = ForkJoinPool.commonPool().submit(() -> {
127 try {
128 this.inetAddress = InetAddress.getLocalHost();
129 String hostname = this.inetAddress.getHostName();
130 return hostname;
131 } catch (UnknownHostException e) {
132 throw new IllegalStateException("Cannot get local hostname", e);
133 }
134 });
135 try {
136 this.hostname = hostnameFJT.get(5, TimeUnit.SECONDS);
137 } catch (InterruptedException | ExecutionException | TimeoutException e) {
138 this.hostname = LOCALHOST_IP;
139 log.warn("Could not get local hostname, using " + this.hostname);
140 }
141 } else {
142 InetAddress[] addresses = InetAddress.getAllByName(this.hostname);
143 InetAddress selectedAddr = null;
144 addresses: for (InetAddress addr : addresses) {
145 if (selectedAddr == null)
146 selectedAddr = addr;
147 if (selectedAddr instanceof Inet6Address)
148 break addresses;
149 }
150 this.inetAddress = selectedAddr;
151 }
152
153 availableSince = System.currentTimeMillis();
154 if (log.isDebugEnabled()) {
155 // log.debug("## CMS starting... stateUuid=" + this.stateUuid + (cleanState ? "
156 // (clean state) " : " "));
157 StringJoiner sb = new StringJoiner("\n");
158 CmsDeployProperty[] deployProperties = CmsDeployProperty.values();
159 Arrays.sort(deployProperties, (o1, o2) -> o1.name().compareTo(o2.name()));
160 for (CmsDeployProperty deployProperty : deployProperties) {
161 List<String> values = getDeployProperties(deployProperty);
162 for (int i = 0; i < values.size(); i++) {
163 String value = values.get(i);
164 if (value != null) {
165 boolean isDefault = deployPropertyDefaults.containsKey(deployProperty)
166 && value.equals(deployPropertyDefaults.get(deployProperty));
167 String line = deployProperty.getProperty() + (i == 0 ? "" : "." + i) + "=" + value
168 + (isDefault ? " (default)" : "");
169 sb.add(line);
170 }
171 }
172 }
173 log.debug("## CMS starting on " + hostname + " ... (" + uuid + ")\n" + sb + "\n");
174 }
175
176 if (log.isTraceEnabled()) {
177 // print system properties
178 StringJoiner sb = new StringJoiner("\n");
179 for (Object key : new TreeMap<>(System.getProperties()).keySet()) {
180 sb.add(key + "=" + System.getProperty(key.toString()));
181 }
182 log.trace("System properties:\n" + sb + "\n");
183
184 }
185
186 } catch (RuntimeException | IOException e) {
187 log.error("## FATAL: CMS state failed", e);
188 throw new IllegalStateException(e);
189 }
190 }
191
192 private void initSecurity() {
193 // private directory permissions
194 Path privateDir = getDataPath(KernelConstants.DIR_PRIVATE);
195 if (privateDir != null) {
196 // TODO rather check whether we can read and write
197 Set<PosixFilePermission> posixPermissions = new HashSet<>();
198 posixPermissions.add(PosixFilePermission.OWNER_READ);
199 posixPermissions.add(PosixFilePermission.OWNER_WRITE);
200 posixPermissions.add(PosixFilePermission.OWNER_EXECUTE);
201 try {
202 if (!Files.exists(privateDir))
203 Files.createDirectories(privateDir);
204 if (!OS.LOCAL.isMSWindows())
205 Files.setPosixFilePermissions(privateDir, posixPermissions);
206 } catch (IOException e) {
207 log.error("Cannot set permissions on " + privateDir, e);
208 }
209 }
210
211 if (getDeployProperty(CmsDeployProperty.JAVA_LOGIN_CONFIG) == null) {
212 String jaasConfig = KernelConstants.JAAS_CONFIG;
213 URL url = getClass().getResource(jaasConfig);
214 // System.setProperty(KernelConstants.JAAS_CONFIG_PROP,
215 // url.toExternalForm());
216 KernelUtils.setJaasConfiguration(url);
217 }
218 // explicitly load JAAS configuration
219 Configuration.getConfiguration();
220
221 boolean initCertificates = (getDeployProperty(CmsDeployProperty.HTTPS_PORT) != null)
222 || (getDeployProperty(CmsDeployProperty.SSHD_PORT) != null);
223 if (initCertificates) {
224 initCertificates();
225 }
226 }
227
228 private void initCertificates() {
229 // server certificate
230 Path keyStorePath = Paths.get(getDeployProperty(CmsDeployProperty.SSL_KEYSTORE));
231 Path pemKeyPath = getDataPath(KernelConstants.DEFAULT_PEM_KEY_PATH);
232 Path pemCertPath = getDataPath(KernelConstants.DEFAULT_PEM_CERT_PATH);
233 char[] keyStorePassword = getDeployProperty(CmsDeployProperty.SSL_PASSWORD).toCharArray();
234
235 // Keystore
236 // if PEM files both exists, update the PKCS12 file
237 if (Files.exists(pemCertPath) && Files.exists(pemKeyPath)) {
238 // TODO check certificate update time? monitor changes?
239 KeyStore keyStore = PkiUtils.getKeyStore(keyStorePath, keyStorePassword,
240 getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE));
241 try (Reader key = Files.newBufferedReader(pemKeyPath, StandardCharsets.US_ASCII);
242 BufferedInputStream cert = new BufferedInputStream(Files.newInputStream(pemCertPath));) {
243 PkiUtils.loadPrivateCertificatePem(keyStore, CmsConstants.NODE, key, keyStorePassword, cert);
244 Files.createDirectories(keyStorePath.getParent());
245 PkiUtils.saveKeyStore(keyStorePath, keyStorePassword, keyStore);
246 if (log.isDebugEnabled())
247 log.debug("PEM certificate stored in " + keyStorePath);
248 } catch (IOException e) {
249 log.error("Cannot read PEM files " + pemKeyPath + " and " + pemCertPath, e);
250 }
251 }
252
253 // Truststore
254 Path trustStorePath = Paths.get(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORE));
255 char[] trustStorePassword = getDeployProperty(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD).toCharArray();
256
257 // IPA CA
258 Path ipaCaCertPath = Paths.get(KernelConstants.IPA_PEM_CA_CERT_PATH);
259 if (Files.exists(ipaCaCertPath)) {
260 KeyStore trustStore = PkiUtils.getKeyStore(trustStorePath, trustStorePassword,
261 getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE));
262 try (BufferedInputStream cert = new BufferedInputStream(Files.newInputStream(ipaCaCertPath));) {
263 PkiUtils.loadTrustedCertificatePem(trustStore, trustStorePassword, cert);
264 Files.createDirectories(keyStorePath.getParent());
265 PkiUtils.saveKeyStore(trustStorePath, trustStorePassword, trustStore);
266 if (log.isDebugEnabled())
267 log.debug("IPA CA certificate stored in " + trustStorePath);
268 } catch (IOException e) {
269 log.error("Cannot trust CA certificate", e);
270 }
271 }
272 }
273
274 public void stop() {
275 if (log.isDebugEnabled())
276 log.debug("CMS stopping... (" + this.uuid + ")");
277
278 long duration = ((System.currentTimeMillis() - availableSince) / 1000) / 60;
279 log.info("## ARGEO CMS " + uuid + " STOPPED after " + (duration / 60) + "h " + (duration % 60)
280 + "min uptime ##");
281 }
282
283 private void firstInit() throws IOException {
284 log.info("## FIRST INIT ##");
285 List<String> nodeInits = getDeployProperties(CmsDeployProperty.NODE_INIT);
286 // if (nodeInits == null)
287 // nodeInits = "../../init";
288 CmsStateImpl.prepareFirstInitInstanceArea(nodeInits);
289 }
290
291 @Override
292 public String getDeployProperty(String property) {
293 CmsDeployProperty deployProperty = CmsDeployProperty.find(property);
294 if (deployProperty == null) {
295 // legacy
296 if (property.startsWith("argeo.node.")) {
297 return doGetDeployProperty(property);
298 }
299 if (property.equals("argeo.i18n.locales")) {
300 String value = doGetDeployProperty(property);
301 if (value != null) {
302 log.warn("Property " + property + " was ignored (value=" + value + ")");
303
304 }
305 return null;
306 }
307 throw new IllegalArgumentException("Unsupported deploy property " + property);
308 }
309 int index = CmsDeployProperty.getPropertyIndex(property);
310 return getDeployProperty(deployProperty, index);
311 }
312
313 @Override
314 public List<String> getDeployProperties(String property) {
315 CmsDeployProperty deployProperty = CmsDeployProperty.find(property);
316 if (deployProperty == null)
317 return new ArrayList<>();
318 return getDeployProperties(deployProperty);
319 }
320
321 public static List<String> getDeployProperties(CmsState cmsState, CmsDeployProperty deployProperty) {
322 return ((CmsStateImpl) cmsState).getDeployProperties(deployProperty);
323 }
324
325 public List<String> getDeployProperties(CmsDeployProperty deployProperty) {
326 List<String> res = new ArrayList<>(deployProperty.getMaxCount());
327 for (int i = 0; i < deployProperty.getMaxCount(); i++) {
328 // String propertyName = i == 0 ? deployProperty.getProperty() :
329 // deployProperty.getProperty() + "." + i;
330 String value = getDeployProperty(deployProperty, i);
331 res.add(i, value);
332 }
333 return res;
334 }
335
336 public static String getDeployProperty(CmsState cmsState, CmsDeployProperty deployProperty) {
337 return ((CmsStateImpl) cmsState).getDeployProperty(deployProperty);
338 }
339
340 public String getDeployProperty(CmsDeployProperty deployProperty) {
341 String value = getDeployProperty(deployProperty, 0);
342 return value;
343 }
344
345 public String getDeployProperty(CmsDeployProperty deployProperty, int index) {
346 String propertyName = deployProperty.getProperty() + (index == 0 ? "" : "." + index);
347 String value = doGetDeployProperty(propertyName);
348 if (value == null && index == 0) {
349 // try defaults
350 if (deployPropertyDefaults.containsKey(deployProperty)) {
351 value = deployPropertyDefaults.get(deployProperty);
352 if (deployProperty.isSystemPropertyOnly())
353 System.setProperty(deployProperty.getProperty(), value);
354 }
355
356 if (value == null) {
357 // try legacy properties
358 String legacyProperty = switch (deployProperty) {
359 case DIRECTORY -> "argeo.node.useradmin.uris";
360 case DB_URL -> "argeo.node.dburl";
361 case DB_USER -> "argeo.node.dbuser";
362 case DB_PASSWORD -> "argeo.node.dbpassword";
363 case HTTP_PORT -> "org.osgi.service.http.port";
364 case HTTPS_PORT -> "org.osgi.service.http.port.secure";
365 case HOST -> "org.eclipse.equinox.http.jetty.http.host";
366 case LOCALE -> "argeo.i18n.defaultLocale";
367
368 default -> null;
369 };
370 if (legacyProperty != null) {
371 value = doGetDeployProperty(legacyProperty);
372 if (value != null) {
373 log.warn("Retrieved deploy property " + deployProperty.getProperty()
374 + " through deprecated property " + legacyProperty);
375 }
376 }
377 }
378 }
379 if (index == 0 && deployProperty.isSystemPropertyOnly()) {
380 String systemPropertyValue = System.getProperty(deployProperty.getProperty());
381 if (!Objects.equals(value, systemPropertyValue))
382 throw new IllegalStateException(
383 "Property " + deployProperty + " must be a ssystem property, but its value is " + value
384 + ", while the system property value is " + systemPropertyValue);
385 }
386 return value != null ? value.toString() : null;
387 }
388
389 protected String getLegacyProperty(String legacyProperty, CmsDeployProperty deployProperty) {
390 String value = doGetDeployProperty(legacyProperty);
391 if (value != null) {
392 log.warn("Retrieved deploy property " + deployProperty.getProperty() + " through deprecated property "
393 + legacyProperty + ".");
394 }
395 return value;
396 }
397
398 protected String doGetDeployProperty(String property) {
399 return KernelUtils.getFrameworkProp(property);
400 }
401
402 @Override
403 public Path getDataPath(String relativePath) {
404 return KernelUtils.getOsgiInstancePath(relativePath);
405 }
406
407 @Override
408 public Path getStatePath(String relativePath) {
409 return KernelUtils.getOsgiConfigurationPath(relativePath);
410 }
411
412 @Override
413 public Long getAvailableSince() {
414 return availableSince;
415 }
416
417 /*
418 * NodeID supplier
419 */
420
421 @Override
422 public Long get() {
423 return NodeIdSupplier.toNodeIdBase(getIpBytes());
424 }
425
426 /** Returns an SHA1 digest of one of the IP addresses. */
427 protected byte[] getIpBytes() {
428 // Enumeration<NetworkInterface> netInterfaces = null;
429 // try {
430 // netInterfaces = NetworkInterface.getNetworkInterfaces();
431 // } catch (SocketException e) {
432 // throw new IllegalStateException(e);
433 // }
434 //
435 // InetAddress selectedIpv6 = null;
436 // InetAddress selectedIpv4 = null;
437 // if (netInterfaces != null) {
438 // netInterfaces: while (netInterfaces.hasMoreElements()) {
439 // NetworkInterface netInterface = netInterfaces.nextElement();
440 // byte[] hardwareAddress = null;
441 // try {
442 // hardwareAddress = netInterface.getHardwareAddress();
443 // if (hardwareAddress != null) {
444 // // first IPv6
445 // addr: for (InterfaceAddress addr : netInterface.getInterfaceAddresses()) {
446 // InetAddress ip = addr.getAddress();
447 // if (ip instanceof Inet6Address) {
448 // Inet6Address ipv6 = (Inet6Address) ip;
449 // if (ipv6.isAnyLocalAddress() || ipv6.isLinkLocalAddress() || ipv6.isLoopbackAddress())
450 // continue addr;
451 // selectedIpv6 = ipv6;
452 // break netInterfaces;
453 // }
454 //
455 // }
456 // // then IPv4
457 // addr: for (InterfaceAddress addr : netInterface.getInterfaceAddresses()) {
458 // InetAddress ip = addr.getAddress();
459 // if (ip instanceof Inet4Address) {
460 // Inet4Address ipv4 = (Inet4Address) ip;
461 // if (ipv4.isAnyLocalAddress() || ipv4.isLinkLocalAddress() || ipv4.isLoopbackAddress())
462 // continue addr;
463 // selectedIpv4 = ipv4;
464 // // we keep searching for IPv6
465 // }
466 //
467 // }
468 // }
469 // } catch (SocketException e) {
470 // throw new IllegalStateException(e);
471 // }
472 // }
473 // }
474 // InetAddress selectedIp = selectedIpv6 != null ? selectedIpv6 : selectedIpv4;
475 if (this.inetAddress.isLoopbackAddress()) {
476 log.warn("No IP address found, using a random node id for UUID generation");
477 return NodeIdSupplier.randomNodeId();
478 }
479 InetAddress selectedIp = this.inetAddress;
480 byte[] digest = DigestUtils.sha1(selectedIp.getAddress());
481 log.debug("Use IP " + selectedIp + " hashed as " + UuidBinaryUtils.toHexString(digest) + " as node id");
482 byte[] nodeId = NodeIdSupplier.toNodeIdBytes(digest, 0);
483 // marks that this is not based on MAC address
484 NodeIdSupplier.forceToNoMacAddress(nodeId, 0);
485 return nodeId;
486 }
487
488 /*
489 * ACCESSORS
490 */
491 @Override
492 public UUID getUuid() {
493 return uuid;
494 }
495
496 // public void setUuidFactory(UuidFactory uuidFactory) {
497 // this.uuidFactory = uuidFactory;
498 // }
499
500 public String getHostname() {
501 return hostname;
502 }
503
504 /**
505 * Called before node initialisation, in order populate OSGi instance are with
506 * some files (typically LDIF, etc).
507 */
508 public static void prepareFirstInitInstanceArea(List<String> nodeInits) {
509
510 for (String nodeInit : nodeInits) {
511 if (nodeInit == null)
512 continue;
513
514 if (nodeInit.startsWith("http")) {
515 // TODO reconnect it
516 // registerRemoteInit(nodeInit);
517 } else {
518
519 // TODO use java.nio.file
520 Path initDir;
521 if (nodeInit.startsWith("."))
522 initDir = KernelUtils.getExecutionDir(nodeInit);
523 else
524 initDir = Paths.get(nodeInit);
525 // TODO also uncompress archives
526 if (Files.exists(initDir)) {
527 Path dataPath = KernelUtils.getOsgiInstancePath("");
528 FsUtils.copyDirectory(initDir, dataPath);
529 log.info("CMS initialized from " + initDir);
530 }
531 }
532 }
533 }
534
535 /*
536 * STATIC
537 */
538 public static IdentClient getIdentClient(String remoteAddr) {
539 if (!IdentClient.isDefaultAuthdPassphraseFileAvailable())
540 return null;
541 // TODO make passphrase more configurable
542 return new IdentClient(remoteAddr);
543 }
544
545 }