1 package org
.argeo
.cms
.internal
.runtime
;
3 import java
.io
.BufferedInputStream
;
4 import java
.io
.IOException
;
6 import java
.net
.Inet6Address
;
7 import java
.net
.InetAddress
;
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
;
25 import java
.util
.Objects
;
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
;
36 import javax
.security
.auth
.login
.Configuration
;
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
;
50 * Implementation of a {@link CmsState}, initialising the required services.
52 public class CmsStateImpl
implements CmsState
, NodeIdSupplier
{
53 private final static CmsLog log
= CmsLog
.getLog(CmsStateImpl
.class);
56 private Long availableSince
;
59 // private final boolean cleanState;
60 private String hostname
;
61 private InetAddress inetAddress
;
63 // private UuidFactory uuidFactory;
65 private final Map
<CmsDeployProperty
, String
> deployPropertyDefaults
;
67 public CmsStateImpl() {
68 this.deployPropertyDefaults
= Collections
.unmodifiableMap(createDeployPropertiesDefaults());
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());
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());
84 Path trustStorePath
= getDataPath(KernelConstants
.DEFAULT_TRUSTSTORE_PATH
);
85 if (trustStorePath
!= null) {
86 deployPropertyDefaults
.put(CmsDeployProperty
.SSL_TRUSTSTORE
, trustStorePath
.toAbsolutePath().toString());
88 deployPropertyDefaults
.put(CmsDeployProperty
.SSL_TRUSTSTORETYPE
, KernelConstants
.PKCS12
);
89 deployPropertyDefaults
.put(CmsDeployProperty
.SSL_TRUSTSTOREPASSWORD
, KernelConstants
.DEFAULT_KEYSTORE_PASSWORD
);
92 Path authorizedKeysPath
= getDataPath(KernelConstants
.NODE_SSHD_AUTHORIZED_KEYS_PATH
);
93 if (authorizedKeysPath
!= null) {
94 deployPropertyDefaults
.put(CmsDeployProperty
.SSHD_AUTHORIZEDKEYS
,
95 authorizedKeysPath
.toAbsolutePath().toString());
97 return deployPropertyDefaults
;
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
);
106 Path privateBase
= getDataPath(KernelConstants
.DIR_PRIVATE
);
107 if (privateBase
!= null && !Files
.exists(privateBase
)) {// first init
109 Files
.createDirectories(privateBase
);
113 // initArgeoLogger();
115 if (log
.isTraceEnabled())
116 log
.trace("CMS State started");
118 String frameworkUuid
= KernelUtils
.getFrameworkProp(KernelUtils
.OSGI_FRAMEWORK_UUID
);
119 this.uuid
= frameworkUuid
!= null ? UUID
.fromString(frameworkUuid
) : UUID
.randomUUID();
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(() -> {
128 this.inetAddress
= InetAddress
.getLocalHost();
129 String hostname
= this.inetAddress
.getHostName();
131 } catch (UnknownHostException e
) {
132 throw new IllegalStateException("Cannot get local hostname", e
);
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
);
142 InetAddress
[] addresses
= InetAddress
.getAllByName(this.hostname
);
143 InetAddress selectedAddr
= null;
144 addresses
: for (InetAddress addr
: addresses
) {
145 if (selectedAddr
== null)
147 if (selectedAddr
instanceof Inet6Address
)
150 this.inetAddress
= selectedAddr
;
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
);
165 boolean isDefault
= deployPropertyDefaults
.containsKey(deployProperty
)
166 && value
.equals(deployPropertyDefaults
.get(deployProperty
));
167 String line
= deployProperty
.getProperty() + (i
== 0 ?
"" : "." + i
) + "=" + value
168 + (isDefault ?
" (default)" : "");
173 log
.debug("## CMS starting on " + hostname
+ " ... (" + uuid
+ ")\n" + sb
+ "\n");
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()));
182 log
.trace("System properties:\n" + sb
+ "\n");
186 } catch (RuntimeException
| IOException e
) {
187 log
.error("## FATAL: CMS state failed", e
);
188 throw new IllegalStateException(e
);
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
);
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
);
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
);
218 // explicitly load JAAS configuration
219 Configuration
.getConfiguration();
221 boolean initCertificates
= (getDeployProperty(CmsDeployProperty
.HTTPS_PORT
) != null)
222 || (getDeployProperty(CmsDeployProperty
.SSHD_PORT
) != null);
223 if (initCertificates
) {
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();
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
);
254 Path trustStorePath
= Paths
.get(getDeployProperty(CmsDeployProperty
.SSL_TRUSTSTORE
));
255 char[] trustStorePassword
= getDeployProperty(CmsDeployProperty
.SSL_TRUSTSTOREPASSWORD
).toCharArray();
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
);
275 if (log
.isDebugEnabled())
276 log
.debug("CMS stopping... (" + this.uuid
+ ")");
278 long duration
= ((System
.currentTimeMillis() - availableSince
) / 1000) / 60;
279 log
.info("## ARGEO CMS " + uuid
+ " STOPPED after " + (duration
/ 60) + "h " + (duration
% 60)
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
);
292 public String
getDeployProperty(String property
) {
293 CmsDeployProperty deployProperty
= CmsDeployProperty
.find(property
);
294 if (deployProperty
== null) {
296 if (property
.startsWith("argeo.node.")) {
297 return doGetDeployProperty(property
);
299 if (property
.equals("argeo.i18n.locales")) {
300 String value
= doGetDeployProperty(property
);
302 log
.warn("Property " + property
+ " was ignored (value=" + value
+ ")");
307 throw new IllegalArgumentException("Unsupported deploy property " + property
);
309 int index
= CmsDeployProperty
.getPropertyIndex(property
);
310 return getDeployProperty(deployProperty
, index
);
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
);
321 public static List
<String
> getDeployProperties(CmsState cmsState
, CmsDeployProperty deployProperty
) {
322 return ((CmsStateImpl
) cmsState
).getDeployProperties(deployProperty
);
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
);
336 public static String
getDeployProperty(CmsState cmsState
, CmsDeployProperty deployProperty
) {
337 return ((CmsStateImpl
) cmsState
).getDeployProperty(deployProperty
);
340 public String
getDeployProperty(CmsDeployProperty deployProperty
) {
341 String value
= getDeployProperty(deployProperty
, 0);
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) {
350 if (deployPropertyDefaults
.containsKey(deployProperty
)) {
351 value
= deployPropertyDefaults
.get(deployProperty
);
352 if (deployProperty
.isSystemPropertyOnly())
353 System
.setProperty(deployProperty
.getProperty(), value
);
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";
370 if (legacyProperty
!= null) {
371 value
= doGetDeployProperty(legacyProperty
);
373 log
.warn("Retrieved deploy property " + deployProperty
.getProperty()
374 + " through deprecated property " + legacyProperty
);
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
);
386 return value
!= null ? value
.toString() : null;
389 protected String
getLegacyProperty(String legacyProperty
, CmsDeployProperty deployProperty
) {
390 String value
= doGetDeployProperty(legacyProperty
);
392 log
.warn("Retrieved deploy property " + deployProperty
.getProperty() + " through deprecated property "
393 + legacyProperty
+ ".");
398 protected String
doGetDeployProperty(String property
) {
399 return KernelUtils
.getFrameworkProp(property
);
403 public Path
getDataPath(String relativePath
) {
404 return KernelUtils
.getOsgiInstancePath(relativePath
);
408 public Path
getStatePath(String relativePath
) {
409 return KernelUtils
.getOsgiConfigurationPath(relativePath
);
413 public Long
getAvailableSince() {
414 return availableSince
;
423 return NodeIdSupplier
.toNodeIdBase(getIpBytes());
426 /** Returns an SHA1 digest of one of the IP addresses. */
427 protected byte[] getIpBytes() {
428 // Enumeration<NetworkInterface> netInterfaces = null;
430 // netInterfaces = NetworkInterface.getNetworkInterfaces();
431 // } catch (SocketException e) {
432 // throw new IllegalStateException(e);
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;
442 // hardwareAddress = netInterface.getHardwareAddress();
443 // if (hardwareAddress != null) {
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())
451 // selectedIpv6 = ipv6;
452 // break netInterfaces;
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())
463 // selectedIpv4 = ipv4;
464 // // we keep searching for IPv6
469 // } catch (SocketException e) {
470 // throw new IllegalStateException(e);
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();
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);
492 public UUID
getUuid() {
496 // public void setUuidFactory(UuidFactory uuidFactory) {
497 // this.uuidFactory = uuidFactory;
500 public String
getHostname() {
505 * Called before node initialisation, in order populate OSGi instance are with
506 * some files (typically LDIF, etc).
508 public static void prepareFirstInitInstanceArea(List
<String
> nodeInits
) {
510 for (String nodeInit
: nodeInits
) {
511 if (nodeInit
== null)
514 if (nodeInit
.startsWith("http")) {
516 // registerRemoteInit(nodeInit);
519 // TODO use java.nio.file
521 if (nodeInit
.startsWith("."))
522 initDir
= KernelUtils
.getExecutionDir(nodeInit
);
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
);
538 public static IdentClient
getIdentClient(String remoteAddr
) {
539 if (!IdentClient
.isDefaultAuthdPassphraseFileAvailable())
541 // TODO make passphrase more configurable
542 return new IdentClient(remoteAddr
);