diff --git a/src/docs/README_fedhub.md b/src/docs/README_fedhub.md index 3002eaa7..ddefcda3 100644 --- a/src/docs/README_fedhub.md +++ b/src/docs/README_fedhub.md @@ -82,6 +82,21 @@ sudo dnf install checkpolicy cd /opt/tak/federation-hub && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver ``` +## Install and Run Debian +Update apt + +``` +sudo apt update -y +``` + +To install from the .deb, run: (if you see the error: couldn't be accessed by user 'apt'. - pkgAcquire::Run (13: Permission denied), that is OK) + +``` +sudo apt install /takserver-fed-hub_*.deb -y +``` + + + ## Install Mongo Make sure /opt/tak/federation-hub/configs/federation-hub-broker.yml has your database credentials defined. Defaults will be generated otherwise ``` @@ -89,8 +104,9 @@ dbUsername: martiuser dbPassword: pass4marti ``` -Mongo Setup +Mongo Setup RHEL ``` +sudo cp /opt/tak/federation-hub/scripts/db/mongodb-org.repo /etc/yum.repos.d/mongodb-org.repo sudo yum install -y mongodb-org sudo systemctl daemon-reload sudo systemctl enable mongod @@ -98,7 +114,22 @@ sudo systemctl restart mongod sudo /opt/tak/federation-hub/scripts/db/configure.sh ``` -## Update from RPM +Mongo Setup Debian +``` +sudo apt install curl software-properties-common gnupg apt-transport-https ca-certificates -y + +curl -fsSL https://pgp.mongodb.com/server-7.0.asc | sudo gpg -o /usr/share/keyrings/mongodb-server-7.0.gpg --dearmor + +echo "deb [ arch=amd64,arm64 signed-by=/usr/share/keyrings/mongodb-server-7.0.gpg ] https://repo.mongodb.org/apt/ubuntu jammy/mongodb-org/7.0 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-7.0.list + +sudo apt update && sudo apt install mongodb-org -y +sudo systemctl enable mongod +sudo systemctl restart mongod + +sudo /opt/tak/federation-hub/scripts/db/configure.sh +``` + +## Update Fedhub Before updating the Federation Hub, you should back up the policy file and list of authorized users: ``` @@ -116,6 +147,11 @@ RHEL8 sudo yum upgrade takserver-fed-hub-*.noarch.rpm ``` +Debian +``` +sudo apt install /takserver-fed-hub_*.deb -y +``` + The policy and authorized can then be replaced: ``` mv /tmp/ui_generated_policy.json /opt/tak/federation-hub/ diff --git a/src/docs/TAK_Server_Configuration_Guide.odt b/src/docs/TAK_Server_Configuration_Guide.odt index c23fa03d..97189cc6 100644 Binary files a/src/docs/TAK_Server_Configuration_Guide.odt and b/src/docs/TAK_Server_Configuration_Guide.odt differ diff --git a/src/docs/TAK_Server_Configuration_Guide.pdf b/src/docs/TAK_Server_Configuration_Guide.pdf index 007a3432..f8fc9923 100644 Binary files a/src/docs/TAK_Server_Configuration_Guide.pdf and b/src/docs/TAK_Server_Configuration_Guide.pdf differ diff --git a/src/federation-common/build.gradle b/src/federation-common/build.gradle index 87e29398..8ef4c020 100644 --- a/src/federation-common/build.gradle +++ b/src/federation-common/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation group: 'org.apache.ignite', name: 'ignite-slf4j', version: ignite_version implementation 'org.apache.commons:commons-lang3:' + commons_lang_version implementation group: 'commons-codec', name: 'commons-codec', version: commons_codec_version + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: jsonwebtoken_version + } compileJava { diff --git a/src/federation-common/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java b/src/federation-common/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java new file mode 100644 index 00000000..b296bbcd --- /dev/null +++ b/src/federation-common/src/main/java/io/netty/handler/ssl/OpenSslServerContext.java @@ -0,0 +1,421 @@ +/* + * Copyright 2014 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.handler.ssl; + +import io.netty.internal.tcnative.SSL; + +import java.io.File; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.cert.X509Certificate; +import java.util.Map; +import java.util.Map.Entry; + +import javax.net.ssl.KeyManager; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLException; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static io.netty.handler.ssl.ReferenceCountedOpenSslServerContext.newSessionContext; + +/** + * A server-side {@link SslContext} which uses OpenSSL's SSL/TLS implementation. + *

This class will use a finalizer to ensure native resources are automatically cleaned up. To avoid finalizers + * and manually release the native memory see {@link ReferenceCountedOpenSslServerContext}. + */ +public final class OpenSslServerContext extends OpenSslContext { + private X509Certificate[] trustCertCollection; + private TrustManagerFactory trustManagerFactory; + private X509Certificate[] keyCertChain; + private PrivateKey key; + private String keyPassword; + private KeyManagerFactory keyManagerFactory; + private Iterable ciphers; + private CipherSuiteFilter cipherFilter; + private OpenSslApplicationProtocolNegotiator apn; + private long sessionCacheSize; + private long sessionTimeout; + private ClientAuth clientAuth; + private String[] protocols; + private boolean startTls; + private boolean enableOcsp; + private String keyStore; + private Entry, Object>[] options; + + private OpenSslServerSessionContext sessionContext; + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext(File certChainFile, File keyFile) throws SSLException { + this(certChainFile, keyFile, null); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword) throws SSLException { + this(certChainFile, keyFile, keyPassword, null, IdentityCipherSuiteFilter.INSTANCE, + ApplicationProtocolConfig.DISABLED, 0, 0); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, + Iterable ciphers, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, keyFile, keyPassword, ciphers, IdentityCipherSuiteFilter.INSTANCE, + apn, sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param nextProtocols the application layer protocols to accept, in the order of preference. + * {@code null} to disable TLS NPN/ALPN extension. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, + Iterable ciphers, Iterable nextProtocols, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, keyFile, keyPassword, ciphers, + toApplicationProtocolConfig(nextProtocols), sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param config Application protocol config. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory, + Iterable ciphers, ApplicationProtocolConfig config, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(certChainFile, keyFile, keyPassword, trustManagerFactory, ciphers, + toNegotiator(config), sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param apn Application protocol negotiator. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory, + Iterable ciphers, OpenSslApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(null, trustManagerFactory, certChainFile, keyFile, keyPassword, null, + ciphers, null, apn, sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(null, null, certChainFile, keyFile, keyPassword, null, + ciphers, cipherFilter, apn, sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param trustCertCollectionFile an X.509 certificate collection file in PEM format. + * This provides the certificate collection used for mutual authentication. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from clients. + * {@code null} to use the default or the results of parsing + * {@code trustCertCollectionFile}. + * @param keyCertChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to clients. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * Only required if {@code provider} is {@link SslProvider#JDK} + * @param config Provides a means to configure parameters related to application protocol negotiation. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig config, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(trustCertCollectionFile, trustManagerFactory, keyCertChainFile, keyFile, keyPassword, keyManagerFactory, + ciphers, cipherFilter, toNegotiator(config), sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param config Application protocol config. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext(File certChainFile, File keyFile, String keyPassword, + TrustManagerFactory trustManagerFactory, Iterable ciphers, + CipherSuiteFilter cipherFilter, ApplicationProtocolConfig config, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(null, trustManagerFactory, certChainFile, keyFile, keyPassword, null, ciphers, cipherFilter, + toNegotiator(config), sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * @param certChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * @param apn Application protocol negotiator. + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder}} + */ + @Deprecated + public OpenSslServerContext( + File certChainFile, File keyFile, String keyPassword, TrustManagerFactory trustManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(null, trustManagerFactory, certChainFile, keyFile, keyPassword, null, ciphers, cipherFilter, + apn, sessionCacheSize, sessionTimeout); + } + + /** + * Creates a new instance. + * + * + * @param trustCertCollectionFile an X.509 certificate collection file in PEM format. + * This provides the certificate collection used for mutual authentication. + * {@code null} to use the system default + * @param trustManagerFactory the {@link TrustManagerFactory} that provides the {@link TrustManager}s + * that verifies the certificates sent from clients. + * {@code null} to use the default or the results of parsing + * {@code trustCertCollectionFile}. + * @param keyCertChainFile an X.509 certificate chain file in PEM format + * @param keyFile a PKCS#8 private key file in PEM format + * @param keyPassword the password of the {@code keyFile}. + * {@code null} if it's not password-protected. + * @param keyManagerFactory the {@link KeyManagerFactory} that provides the {@link KeyManager}s + * that is used to encrypt data being sent to clients. + * {@code null} to use the default or the results of parsing + * {@code keyCertChainFile} and {@code keyFile}. + * @param ciphers the cipher suites to enable, in the order of preference. + * {@code null} to use the default cipher suites. + * @param cipherFilter a filter to apply over the supplied list of ciphers + * Only required if {@code provider} is {@link SslProvider#JDK} + * @param apn Application Protocol Negotiator object + * @param sessionCacheSize the size of the cache used for storing SSL session objects. + * {@code 0} to use the default value. + * @param sessionTimeout the timeout for the cached SSL session objects, in seconds. + * {@code 0} to use the default value. + * @deprecated use {@link SslContextBuilder} + */ + @Deprecated + public OpenSslServerContext( + File trustCertCollectionFile, TrustManagerFactory trustManagerFactory, + File keyCertChainFile, File keyFile, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout) throws SSLException { + this(toX509CertificatesInternal(trustCertCollectionFile), trustManagerFactory, + toX509CertificatesInternal(keyCertChainFile), toPrivateKeyInternal(keyFile, keyPassword), + keyPassword, keyManagerFactory, ciphers, cipherFilter, + apn, sessionCacheSize, sessionTimeout, ClientAuth.NONE, null, false, false, KeyStore.getDefaultType()); + } + + OpenSslServerContext( + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, ApplicationProtocolConfig apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp, String keyStore, Map.Entry, Object>... options) + throws SSLException { + this(trustCertCollection, trustManagerFactory, keyCertChain, key, keyPassword, keyManagerFactory, ciphers, + cipherFilter, toNegotiator(apn), sessionCacheSize, sessionTimeout, clientAuth, protocols, startTls, + enableOcsp, keyStore, options); + } + + @SuppressWarnings("deprecation") + private OpenSslServerContext( + X509Certificate[] trustCertCollection, TrustManagerFactory trustManagerFactory, + X509Certificate[] keyCertChain, PrivateKey key, String keyPassword, KeyManagerFactory keyManagerFactory, + Iterable ciphers, CipherSuiteFilter cipherFilter, OpenSslApplicationProtocolNegotiator apn, + long sessionCacheSize, long sessionTimeout, ClientAuth clientAuth, String[] protocols, boolean startTls, + boolean enableOcsp, String keyStore, Map.Entry, Object>... options) + throws SSLException { + super(ciphers, cipherFilter, apn, SSL.SSL_MODE_SERVER, keyCertChain, + clientAuth, protocols, startTls, enableOcsp, options); + + // BBN Update: set these for reuse in updateSslContext(...) + this.trustCertCollection = trustCertCollection; + this.trustManagerFactory = trustManagerFactory; + this.keyManagerFactory = keyManagerFactory; + this.sessionCacheSize = sessionCacheSize; + this.sessionTimeout = sessionTimeout; + this.keyStore = keyStore; + + // Create a new SSL_CTX and configure it. + boolean success = false; + try { + OpenSslKeyMaterialProvider.validateKeyMaterialSupported(keyCertChain, key, keyPassword); + sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, + keyCertChain, key, keyPassword, keyManagerFactory, keyStore, + sessionCacheSize, sessionTimeout); + success = true; + } finally { + if (!success) { + release(); + } + } + } + // BBN Update: There is no way to update the truststore on an existing server session. + // This method will allow for hot reloading the truststore by creating a new session context + // with the passed in truststore manager. + // Existing connections will NOT be disconnecting + // New connections will be initialized with this updated context + public void updateSslContext(TrustManagerFactory trustManagerFactory) throws SSLException { + // Create a new SSL_CTX and configure it. + boolean success = false; + try { + sessionContext = newSessionContext(this, ctx, engineMap, trustCertCollection, trustManagerFactory, + keyCertChain, key, keyPassword, keyManagerFactory, keyStore, + sessionCacheSize, sessionTimeout); + + success = true; + } finally { + if (!success) { + release(); + } + } + } + + @Override + public OpenSslServerSessionContext sessionContext() { + return sessionContext; + } +} diff --git a/src/federation-common/src/main/java/tak/server/federation/FederateTokenGroup.java b/src/federation-common/src/main/java/tak/server/federation/FederateTokenGroup.java new file mode 100644 index 00000000..085afa34 --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/FederateTokenGroup.java @@ -0,0 +1,18 @@ +package tak.server.federation; + +public class FederateTokenGroup extends FederateGroup { + + private String token; + + public FederateTokenGroup(FederateIdentity federateIdentity) { + super(federateIdentity); + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } +} diff --git a/src/federation-common/src/main/java/tak/server/federation/GuardedStreamHolder.java b/src/federation-common/src/main/java/tak/server/federation/GuardedStreamHolder.java index b30626dc..5b59820c 100644 --- a/src/federation-common/src/main/java/tak/server/federation/GuardedStreamHolder.java +++ b/src/federation-common/src/main/java/tak/server/federation/GuardedStreamHolder.java @@ -2,14 +2,12 @@ import static java.util.Objects.requireNonNull; -import java.math.BigInteger; import java.util.Comparator; import java.util.HashSet; +import java.util.List; import java.util.Set; import java.util.concurrent.ConcurrentSkipListSet; -import javax.net.ssl.SSLSession; - import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -19,9 +17,9 @@ import com.atakmap.Tak.FederateHops; import com.atakmap.Tak.FederateProvenance; import com.atakmap.Tak.FederatedEvent; +import com.atakmap.Tak.Identity.ConnectionType; import com.atakmap.Tak.ROL; import com.atakmap.Tak.Subscription; -import com.atakmap.Tak.Identity.ConnectionType; import com.google.common.base.Strings; import io.grpc.ClientCall; @@ -46,6 +44,8 @@ public class GuardedStreamHolder { private FederateIdentity federateIdentity; private Subscription subscription; private int maxFederateHops = -1; + private String clientFingerprint; + private List clientGroups; private boolean isRunningInHub = false; @@ -75,7 +75,7 @@ public GuardedStreamHolder(ClientCall clientCall, String fedId, } // for incoming connections - public GuardedStreamHolder(StreamObserver clientStream, String clientName, String certHash, SSLSession session, Subscription subscription, Comparator comp, boolean isRunningInHub) { + public GuardedStreamHolder(StreamObserver clientStream, String clientName, String certHash, String sessionId, Subscription subscription, Comparator comp, boolean isRunningInHub) { requireNonNull(clientStream, "FederatedEvent client stream"); @@ -97,7 +97,7 @@ public GuardedStreamHolder(StreamObserver clientStream, String clientName, St // new takservers will send their CoreConfig serverId. if present, use it, otherwise generate a random unique identifier String serverId = subscription.getIdentity().getServerId(); if (Strings.isNullOrEmpty(serverId)) { - serverId = new BigInteger(session.getId()).toString(); + serverId = sessionId; } String fedId = clientName + "-" + certHash + "-" + serverId; @@ -144,8 +144,8 @@ public synchronized void send(T event) { // since hub outgoing connections can forward traffic to other hubs, we need to keep a list of visited nodes // so that we can stop cycles FederateProvenance prov = FederateProvenance.newBuilder() - .setFederationServerId(FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfig().getFullId()) - .setFederationServerName(FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfig().getServerName()) + .setFederationServerId(FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().getConfig().getFullId()) + .setFederationServerName(FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().getConfig().getServerName()) .build(); Set federateProvenances = new HashSet<>(); @@ -345,7 +345,23 @@ public void setMaxFederateHops(int maxFederateHops) { this.maxFederateHops = maxFederateHops; } - @Override + public String getClientFingerprint() { + return clientFingerprint; + } + + public void setClientFingerprint(String clientFingerprint) { + this.clientFingerprint = clientFingerprint; + } + + public List getClientGroups() { + return clientGroups; + } + + public void setClientGroups(List clientGroups) { + this.clientGroups = clientGroups; + } + + @Override public String toString() { return "GuardedStreamHolder [clientStream=" + clientStream + ", clientCall=" + clientCall + ", lastHealthTime=" + lastHealthTime + ", lastHealthStatus=" + lastHealthStatus + ", federateIdentity=" + federateIdentity diff --git a/src/federation-common/src/main/java/tak/server/federation/TokenAuthCredential.java b/src/federation-common/src/main/java/tak/server/federation/TokenAuthCredential.java new file mode 100644 index 00000000..ed60cd1a --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/TokenAuthCredential.java @@ -0,0 +1,40 @@ +package tak.server.federation; + +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; + +import java.util.concurrent.Executor; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import io.grpc.Status; + +public class TokenAuthCredential extends CallCredentials { + public static final String BEARER_TYPE = "Bearer"; + + public static final Metadata.Key AUTHORIZATION_METADATA_KEY = Metadata.Key.of("Authorization", + ASCII_STRING_MARSHALLER); + + private final String token; + + public TokenAuthCredential(String token) { + this.token = token; + } + + @Override + public void applyRequestMetadata(final RequestInfo requestInfo, final Executor executor, + final MetadataApplier metadataApplier) { + + executor.execute(new Runnable() { + @Override + public void run() { + try { + Metadata headers = new Metadata(); + headers.put(AUTHORIZATION_METADATA_KEY, String.format("%s %s", BEARER_TYPE, token)); + metadataApplier.apply(headers); + } catch (Throwable e) { + metadataApplier.fail(Status.UNAUTHENTICATED.withCause(e)); + } + } + }); + } +} \ No newline at end of file diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubDependencyInjectionProxy.java b/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubDependencyInjectionProxy.java index 8b3f3f5f..c4f27e1a 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubDependencyInjectionProxy.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubDependencyInjectionProxy.java @@ -11,7 +11,7 @@ public class FederationHubDependencyInjectionProxy implements ApplicationContextAware { private static ApplicationContext springContext; - private static FederationHubDependencyInjectionProxy instance = null; + private volatile static FederationHubDependencyInjectionProxy instance = null; public static FederationHubDependencyInjectionProxy getInstance() { if (instance == null) { @@ -35,7 +35,7 @@ public void setApplicationContext(ApplicationContext context) throws BeansExcept this.springContext = context; } - private FederationHubPolicyManager fedHubPolicyManager = null; + private volatile FederationHubPolicyManager fedHubPolicyManager = null; public FederationHubPolicyManager fedHubPolicyManager() { if (fedHubPolicyManager == null) { @@ -49,7 +49,7 @@ public FederationHubPolicyManager fedHubPolicyManager() { return fedHubPolicyManager; } - private SSLConfig sslConfig = null; + private volatile SSLConfig sslConfig = null; public SSLConfig sslConfig() { if (sslConfig == null) { @@ -63,21 +63,21 @@ public SSLConfig sslConfig() { return sslConfig; } - private FederationHubServerConfig fedHubServerConfig = null; + private volatile FederationHubServerConfigManager fedHubServerConfigManager = null; - public FederationHubServerConfig fedHubServerConfig() { - if (fedHubServerConfig == null) { + public FederationHubServerConfigManager fedHubServerConfigManager() { + if (fedHubServerConfigManager == null) { synchronized (this) { - if (fedHubServerConfig == null) { - fedHubServerConfig = springContext.getBean(FederationHubServerConfig.class); + if (fedHubServerConfigManager == null) { + fedHubServerConfigManager = springContext.getBean(FederationHubServerConfigManager.class); } } } - return fedHubServerConfig; + return fedHubServerConfigManager; } - private FederationHubBroker federationHubBroker = null; + private volatile FederationHubBroker federationHubBroker = null; public FederationHubBroker federationHubBroker() { if (federationHubBroker == null) { @@ -91,7 +91,7 @@ public FederationHubBroker federationHubBroker() { return federationHubBroker; } - private FederationHubBrokerMetrics federationHubBrokerMetrics = null; + private volatile FederationHubBrokerMetrics federationHubBrokerMetrics = null; public FederationHubBrokerMetrics federationHubBrokerMetrics() { if (federationHubBrokerMetrics == null) { @@ -104,7 +104,7 @@ public FederationHubBrokerMetrics federationHubBrokerMetrics() { return federationHubBrokerMetrics; } - private HubConnectionStore hubConnectionStore = null; + private volatile HubConnectionStore hubConnectionStore = null; public HubConnectionStore hubConnectionStore() { if (hubConnectionStore == null) { diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubUtils.java b/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubUtils.java index 7771fc5c..91a2c3a4 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubUtils.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/FederationHubUtils.java @@ -1,10 +1,27 @@ package tak.server.federation.hub; +import java.io.ByteArrayInputStream; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.IOException; +import java.io.InputStream; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; + +import javax.naming.InvalidNameException; +import javax.naming.ldap.LdapName; +import javax.naming.ldap.Rdn; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import org.apache.ignite.IgniteCheckedException; import org.apache.ignite.configuration.ClientConnectorConfiguration; @@ -17,6 +34,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.bbn.roger.fig.FederationUtils; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -124,4 +142,111 @@ private FederationHubIgniteConfig loadIgniteConfig(String configFile) return new ObjectMapper(new YAMLFactory()).readValue(new FileInputStream(configFile), FederationHubIgniteConfig.class); } + + public static String getCN(String dn) throws RuntimeException { + if (Strings.isNullOrEmpty(dn)) { + throw new IllegalArgumentException("empty DN"); + } + + try { + LdapName ldapName = new LdapName(dn); + + for (Rdn rdn : ldapName.getRdns()) { + if (rdn.getType().equalsIgnoreCase("CN")) { + return rdn.getValue().toString(); + } + } + + throw new RuntimeException("No CN found in DN: " + dn); + } catch (InvalidNameException e) { + throw new RuntimeException(e); + } + } + + public static X509Certificate loadX509CertFromBytes(byte[] cert) throws CertificateException, IOException { + + if (cert == null) { + throw new IllegalArgumentException("empty cert"); + } + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + InputStream is = new ByteArrayInputStream(cert); + try { + return (X509Certificate) cf.generateCertificate(is); + } finally { + is.close(); + } + } + + public static X509Certificate loadX509CertFile(String caFilename) throws CertificateException, IOException { + + if (Strings.isNullOrEmpty(caFilename)) { + throw new IllegalArgumentException("empty ca file name"); + } + + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + + InputStream is = new FileInputStream(caFilename); + try { + return (X509Certificate) cf.generateCertificate(is); + } finally { + is.close(); + } + } + + public static List verifyTrustedClientCert(TrustManagerFactory tmf, X509Certificate clientCert) { + List signingCa = new ArrayList<>(); + for (TrustManager trustManager : tmf.getTrustManagers()) { + if (trustManager instanceof X509TrustManager) { + try { + X509TrustManager x509TrustManager = (X509TrustManager) trustManager; + + // first validate and check if client certificate is trusted + x509TrustManager.checkClientTrusted(new X509Certificate[] { clientCert }, "RSA"); + + // next find the CA(s) that signed the client certificate + for (X509Certificate trustedCa : x509TrustManager.getAcceptedIssuers()) { + try { + clientCert.verify(trustedCa.getPublicKey()); + signingCa.add(trustedCa); + } catch (Exception e) {} + } + } catch (Exception e) {} + } + } + return signingCa; + } + + public static List getCaGroupIdsFromCerts(Certificate[] peerCertificates) { + List caCertGroups = new LinkedList<>(); + /* + * The cert array returned by gRPC's SSLSession is padded with null entries. + * This loop adds all certs in the array from index 1 (index 0 is the peer's + * cert, index 1+ are CA certs) til the first null entry (the start of the + * padding) to a list of CA certs. + */ + for (int i = 1; i < peerCertificates.length; i++) { + if (peerCertificates[i] == null) { + break; + } + + X509Certificate caCert = ((X509Certificate) peerCertificates[i]); + caCertGroups.add(getCaGroupIdFromCert(caCert)); + } + return caCertGroups; + } + + public static String getCaGroupIdFromCert(X509Certificate caCert) { + try { + String fingerprint = FederationUtils.getBytesSHA256(caCert.getEncoded()); + String issuerDN = caCert.getIssuerX500Principal().getName(); + String issuerCN = Optional.ofNullable(getCN(issuerDN)).map(cn -> cn.toLowerCase()).orElse(""); + + return issuerDN + "-" + fingerprint; + } catch (Exception e) { + logger.error("getCaGroupIdFromCert error", e); + return null; + } + } } diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/FedhubJwtUtils.java b/src/federation-common/src/main/java/tak/server/federation/hub/FedhubJwtUtils.java new file mode 100644 index 00000000..17a4a28c --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/FedhubJwtUtils.java @@ -0,0 +1,188 @@ +package tak.server.federation.hub; + +import java.io.FileInputStream; +import java.security.Key; +import java.security.KeyPair; +import java.security.KeyStore; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.cert.Certificate; +import java.util.ArrayList; +import java.util.Date; +import java.util.Enumeration; +import java.util.List; + +import javax.crypto.spec.SecretKeySpec; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.JwtParser; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import tak.server.federation.hub.broker.FederationHubServerConfig; +import tak.server.federation.hub.ui.FederationHubUIConfig; + +public class FedhubJwtUtils { + private static final Logger logger = LoggerFactory.getLogger(FedhubJwtUtils.class); + + private ThreadLocal jwtRsaParser = new ThreadLocal<>(); + private ThreadLocal jwtHmacParser = new ThreadLocal<>(); + private PrivateKey privateKey = null; + private SecretKeySpec secretKeySpec = null; + private PublicKey publicKey = null; + private boolean keysLoaded = false; + + private static String keyStoreType = ""; + private static String keyStoreFile = ""; + private static String keyStorePass = ""; + + private static FedhubJwtUtils instance = null; + + private KeyPair loadKeyPair(String keyStoreType, String keyStoreFile, String keyStorePass) { + try { + KeyStore keyStore = KeyStore.getInstance(keyStoreType); + keyStore.load(new FileInputStream(keyStoreFile), keyStorePass.toCharArray()); + + PrivateKey privateKeyTmp = null; + PublicKey publicKeyTmp = null; + + PrivateKey search = null; + Enumeration aliases = keyStore.aliases(); + while (aliases.hasMoreElements() && privateKeyTmp == null) { + String alias = aliases.nextElement(); + privateKeyTmp = (PrivateKey) keyStore.getKey(alias, keyStorePass.toCharArray()); + if (privateKeyTmp != null) { + Certificate cert = keyStore.getCertificate(alias); + publicKeyTmp = cert.getPublicKey(); + } + } + + KeyPair keyPair = new KeyPair(publicKeyTmp, privateKeyTmp); + return keyPair; + } catch (Exception e) { + logger.error("exception in loadKeyPair", e); + return null; + } + } + + private void loadKeys() { + + if (keysLoaded) { + return; + } + + try { + // + // load keys from tls keystore + KeyPair keyPair = loadKeyPair(keyStoreType, keyStoreFile, keyStorePass); + if (keyPair == null) { + logger.info("loadKeyPair returned null"); + return; + } + + publicKey = keyPair.getPublic(); + privateKey = keyPair.getPrivate(); + secretKeySpec = new SecretKeySpec(privateKey.getEncoded(), privateKey.getAlgorithm()); + + if (privateKey == null) { + logger.info("FedHubJwtUtils unable to find PrivateKey in keystore!"); + return; + } + + keysLoaded = true; + + jwtRsaParser.remove(); + jwtHmacParser.remove(); + + } catch (Exception e) { + logger.error("Exception in loadKeys!", e); + } + } + + public static FedhubJwtUtils getInstance(FederationHubUIConfig fedHubConfig) { + if (instance == null) { + keyStoreFile = fedHubConfig.getKeystoreFile(); + keyStorePass = fedHubConfig.getKeystorePassword(); + keyStoreType = fedHubConfig.getKeystoreType(); + instance = new FedhubJwtUtils(); + instance.loadKeys(); + } + return instance; + } + + public static FedhubJwtUtils getInstance(FederationHubServerConfig fedHubConfig) { + if (instance == null) { + keyStoreFile = fedHubConfig.getKeystoreFile(); + keyStorePass = fedHubConfig.getKeystorePassword(); + keyStoreType = fedHubConfig.getKeystoreType(); + instance = new FedhubJwtUtils(); + instance.loadKeys(); + } + return instance; + } + + public PublicKey getPublicKey() { + return publicKey; + } + + public PrivateKey getPrivateKey() { + return privateKey; + } + + private JwtParser getParser(SignatureAlgorithm signatureAlgorithm, Key key) { + + JwtParser parser = null; + if (signatureAlgorithm == SignatureAlgorithm.RS256) { + parser = jwtRsaParser.get(); + if (parser == null) { + parser = Jwts.parser(); + parser.setSigningKey(key); + jwtRsaParser.set(parser); + } + } else if (signatureAlgorithm == SignatureAlgorithm.HS256) { + parser = jwtHmacParser.get(); + if (parser == null) { + parser = Jwts.parser(); + parser.setSigningKey(key.getEncoded()); + jwtHmacParser.set(parser); + } + } + + return parser; + } + + public Claims parseClaim(String token) { + JwtParser jwtParser = getParser(SignatureAlgorithm.HS256, privateKey); + return jwtParser.parseClaimsJws(token).getBody(); + } + + public String createToken(String clientFingerprint, String clientGroup, long expiration) { + List clientGroups = new ArrayList<>(); + clientGroups.add(clientGroup); + + return createToken(clientFingerprint, clientGroups, expiration); + } + + public String createToken(String clientFingerprint, List clientGroups, long expiration) { + try { + Date now = new Date(); + + JwtBuilder builder = Jwts.builder().setId("someid").setIssuedAt(now).setSubject("subject") + .setIssuer("issuer").signWith(SignatureAlgorithm.HS256, secretKeySpec) + .claim("federation", "federation claim val").claim("clientFingerprint", clientFingerprint) + .claim("clientGroups", clientGroups); + + if (expiration > 0) + builder.setExpiration(new Date(expiration)); + + return builder.compact(); + + } catch (Exception e) { + logger.error("Exception in createMissionToken!", e); + return null; + } + } +} diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBroker.java b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBroker.java index 485256fd..40b51be0 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBroker.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBroker.java @@ -16,4 +16,6 @@ public interface FederationHubBroker { void disconnectFederate(String connectionId); Map getCAsFromFile(); byte[] getSelfCaFile(); + FederationHubServerConfig getFederationHubBrokerConfig(); + FederationHubServerConfig saveFederationHubServerConfig(FederationHubServerConfig brokerConfig); } \ No newline at end of file diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerImpl.java b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerImpl.java index ab36dd6e..08de29d4 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerImpl.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerImpl.java @@ -98,7 +98,7 @@ public void addGroupCa(X509Certificate ca) { FederationHubPolicyManager fedHubPolicyManager = depProxy.fedHubPolicyManager(); FederationHubServerConfig fedHubConfig = - depProxy.fedHubServerConfig(); + depProxy.fedHubServerConfigManager().getConfig(); try { String dn = ca.getSubjectX500Principal().getName(); @@ -108,7 +108,6 @@ public void addGroupCa(X509Certificate ca) { saveTruststoreFile(sslConfig, fedHubConfig); sslConfig.refresh(); FederationHubBrokerUtils.sendCaGroupToFedManager(fedHubPolicyManager, ca); - depProxy.restartV2Server(); } catch (KeyStoreException | RuntimeException | CertificateEncodingException e) { logger.error("Exception adding CA", e); throw new RuntimeException(e); @@ -143,7 +142,7 @@ public void deleteGroupCa(String groupId) { FederationHubDependencyInjectionProxy depProxy = FederationHubDependencyInjectionProxy.getInstance(); FederationHubPolicyManager fedHubPolicyManager = depProxy.fedHubPolicyManager(); SSLConfig sslConfig = depProxy.sslConfig(); - FederationHubServerConfig fedHubConfig = depProxy.fedHubServerConfig(); + FederationHubServerConfig fedHubConfig = depProxy.fedHubServerConfigManager().getConfig(); try { for (Enumeration e = sslConfig.getTrust().aliases(); e.hasMoreElements();) { @@ -171,7 +170,7 @@ public void deleteGroupCa(String groupId) { @Override public byte[] getSelfCaFile() { FederationHubDependencyInjectionProxy depProxy = FederationHubDependencyInjectionProxy.getInstance(); - FederationHubServerConfig fedHubConfig = depProxy.fedHubServerConfig(); + FederationHubServerConfig fedHubConfig = depProxy.fedHubServerConfigManager().getConfig(); String caFilePath = fedHubConfig.getCaFile(); try { @@ -294,4 +293,14 @@ public List getGroupsForNode(String federateId) { public void disconnectFederate(String connectionId) { FederationHubDependencyInjectionProxy.getSpringContext().publishEvent(new ForceDisconnectEvent(this, connectionId)); } + + @Override + public FederationHubServerConfig getFederationHubBrokerConfig() { + return FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().getConfig(); + } + + @Override + public FederationHubServerConfig saveFederationHubServerConfig(FederationHubServerConfig brokerConfig) { + return FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().saveConfig(brokerConfig); + } } \ No newline at end of file diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfig.java b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfig.java index 9a957868..45f2ea31 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfig.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfig.java @@ -74,6 +74,8 @@ public FederationHubServerConfig() { private long missionFederationDisruptionMaxFileSizeBytes = 268435456; private boolean missionFederationDisruptionEnabled = false; + private List federationTokenAuthServers = new ArrayList<>(); + public int getOutgoingReconnectSeconds() { return outgoingReconnectSeconds; } @@ -361,6 +363,14 @@ public void setMissionFederationDisruptionMaxFileSizeBytes(long missionFederatio this.missionFederationDisruptionMaxFileSizeBytes = missionFederationDisruptionMaxFileSizeBytes; } + public List getFederationTokenAuthServers() { + return federationTokenAuthServers; + } + + public void setFederationTokenAuthServers(List federationTokenAuthServers) { + this.federationTokenAuthServers = federationTokenAuthServers; + } + @Override public String toString() { return "FederationHubServerConfig [keystoreType=" + keystoreType + ", keystoreFile=" + keystoreFile @@ -380,4 +390,26 @@ public String toString() { + ", missionFederationDisruptionMaxFileSizeBytes=" + missionFederationDisruptionMaxFileSizeBytes + ", missionFederationDisruptionEnabled=" + missionFederationDisruptionEnabled + "]"; } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class TokenAuthServer { + private int port = 9103; + private String type = "jwt"; + public int getPort() { + return port; + } + public void setPort(int port) { + this.port = port; + } + public String getType() { + return type; + } + public void setType(String type) { + this.type = type; + } + @Override + public String toString() { + return "TokenAuthServer [port=" + port + ", type=" + type + "]"; + } + } } diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfigManager.java b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfigManager.java new file mode 100644 index 00000000..2181b432 --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/broker/FederationHubServerConfigManager.java @@ -0,0 +1,66 @@ +package tak.server.federation.hub.broker; + +import java.io.File; +import java.io.FileInputStream; +import java.util.UUID; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.google.common.base.Strings; + +public class FederationHubServerConfigManager { + private static final Logger logger = LoggerFactory.getLogger(FederationHubServerConfigManager.class); + + private final String configFile; + private FederationHubServerConfig config = null; + + FederationHubServerConfigManager(String configFile) { + this.configFile = configFile; + + config = loadConfig(); + + if (Strings.isNullOrEmpty(config.getId())) { + config.setId(UUID.randomUUID().toString().replace("-", "")); + saveConfig(config); + } + } + + public FederationHubServerConfig getConfig() { + return config; + } + + private FederationHubServerConfig loadConfig() { + try { + if (getClass().getResource(configFile) != null) { + // It's a resource. + config = new ObjectMapper(new YAMLFactory()).readValue(getClass().getResourceAsStream(configFile), + FederationHubServerConfig.class); + } else { + // It's a file. + config = new ObjectMapper(new YAMLFactory()).readValue(new FileInputStream(configFile), + FederationHubServerConfig.class); + } + } catch (Exception e) { + logger.error("Error loading broker config", e); + } + + return config; + } + + public FederationHubServerConfig saveConfig(FederationHubServerConfig config) { + + try { + ObjectMapper om = new ObjectMapper(new YAMLFactory()); + om.writeValue(new File(configFile), config); + } catch (Exception e) { + logger.error("Error writing broker config", e); + } + + config = loadConfig(); + + return config; + } +} diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/broker/HubConnectionStore.java b/src/federation-common/src/main/java/tak/server/federation/hub/broker/HubConnectionStore.java index e3811713..c0f4c912 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/broker/HubConnectionStore.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/broker/HubConnectionStore.java @@ -28,7 +28,6 @@ public class HubConnectionStore { private final Map> clientROLStreamMap = new ConcurrentHashMap<>(); private final Map> clientGroupStreamMap = new ConcurrentHashMap<>(); private final Map clientToGroups = new ConcurrentHashMap<>(); - private final Map sessionMap = new ConcurrentHashMap<>(); private final Map connectionInfos = new ConcurrentHashMap<>(); private final Map> tempRolCache = new ConcurrentHashMap<>(); @@ -53,7 +52,6 @@ public void clearIdFromAllStores(String id) { this.clientROLStreamMap.remove(id); this.clientGroupStreamMap.remove(id); this.clientToGroups.remove(id); - this.sessionMap.remove(id); this.connectionInfos.remove(id); this.tempRolCache.remove(id); } @@ -63,7 +61,6 @@ public void clearStreamMaps() { this.clientROLStreamMap.clear(); this.clientGroupStreamMap.clear(); this.clientToGroups.clear(); - this.sessionMap.clear(); this.connectionInfos.clear(); this.tempRolCache.clear(); } @@ -117,16 +114,4 @@ public void putFederateGroups(String id, FederateGroups federateGroups) { public void removeFederateGroups(String id) { this.clientToGroups.remove(id); } - - public Map getSessionMap() { - return Collections.unmodifiableMap(sessionMap); - } - - public void addSession(String id, SSLSession session) { - this.sessionMap.put(id, session); - } - - public void removeSession(String id) { - this.sessionMap.remove(id); - } } diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/broker/SSLConfig.java b/src/federation-common/src/main/java/tak/server/federation/hub/broker/SSLConfig.java index dc08a8c9..1bfd6974 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/broker/SSLConfig.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/broker/SSLConfig.java @@ -12,12 +12,14 @@ import javax.net.ssl.SSLEngine; import javax.net.ssl.TrustManagerFactory; -import com.google.common.base.Strings; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import com.google.common.base.Strings; + import io.grpc.netty.GrpcSslContexts; import io.netty.handler.ssl.ClientAuth; +import io.netty.handler.ssl.OpenSslServerContext; import io.netty.handler.ssl.SslContext; import io.netty.handler.ssl.SslContextBuilder; import io.netty.handler.ssl.SslProvider; @@ -27,12 +29,13 @@ public class SSLConfig { private TrustManagerFactory trustMgrFactory; private KeyManagerFactory keyMgrFactory; - private SslContext sslContext; // context used to source the tabula rasa engine + private SslContext sslContextClientAuth; + private SslContext sslContextNoAuth; private KeyStore trust; private KeyStore self; private FederationHubServerConfig config; - public SslContext initSslContext(FederationHubServerConfig config) { + public void initSslContext(FederationHubServerConfig config) { this.config = config; try { @@ -54,13 +57,18 @@ public SslContext initSslContext(FederationHubServerConfig config) { trust.load(getFileOrResource(config.getTruststoreFile()), config.getTruststorePassword().toCharArray()); trustMgrFactory.init(trust); - sslContext = GrpcSslContexts.configure(SslContextBuilder.forServer(keyMgrFactory), SslProvider.OPENSSL) // this ensures that we are using OpenSSL, not JRE SSL + sslContextClientAuth = GrpcSslContexts.configure(SslContextBuilder.forServer(keyMgrFactory), SslProvider.OPENSSL) // this ensures that we are using OpenSSL, not JRE SSL .protocols("TLSv1.2","TLSv1.3") .trustManager(trustMgrFactory) .clientAuth(ClientAuth.REQUIRE) // client auth always required .build(); + + sslContextNoAuth = GrpcSslContexts.configure(SslContextBuilder.forServer(keyMgrFactory), SslProvider.OPENSSL) // this ensures that we are using OpenSSL, not JRE SSL + .protocols("TLSv1.2","TLSv1.3") + .trustManager(trustMgrFactory) + .clientAuth(ClientAuth.NONE) // client auth always required + .build(); - return sslContext; } catch (Exception e) { throw new RuntimeException(e); } @@ -74,14 +82,22 @@ public SSLEngine buildClientEngine() { return null; } - public synchronized void refresh() { - try { - this.trust = initTrust(config, trustMgrFactory); -// sslContext.init(keyMgrFactory.getKeyManagers(), trustMgrFactory.getTrustManagers(), new SecureRandom()); - } catch (RuntimeException e) { - logger.warn("Exeception refreshing SSL Context", e); - } - } + public synchronized void refresh() { + try { + this.trust = initTrust(config, trustMgrFactory); + + // Get the current SSL context attached to the running server and update the truststore + // Newly created SSL sessions will use this new truststore. Existing SSL sessions will not be forced to reconnect + // (Note: This only applies to refreshes for adding CA's. When CA's are deleted, all connections will be forced to reconnect) + OpenSslServerContext openSslServerSessionContextClientAuth = (OpenSslServerContext) sslContextClientAuth; + openSslServerSessionContextClientAuth.updateSslContext(trustMgrFactory); + + OpenSslServerContext openSslServerSessionContextNoAuth = (OpenSslServerContext) sslContextNoAuth; + openSslServerSessionContextNoAuth.updateSslContext(trustMgrFactory); + } catch (Exception e) { + logger.warn("Exeception refreshing SSL Context", e); + } + } public static KeyStore initTrust(FederationHubServerConfig config, TrustManagerFactory trustManagerFactory) throws RuntimeException { @@ -119,11 +135,14 @@ public void setKeyMgrFactory(KeyManagerFactory keyMgrFactory) { this.keyMgrFactory = keyMgrFactory; } - public SslContext getSslContext() { - return sslContext; + public SslContext getSslContextClientAuth() { + return sslContextClientAuth; } - + public SslContext getSslContextNoAuth() { + return sslContextNoAuth; + } + public KeyStore getTrust() { return trust; } @@ -146,6 +165,4 @@ private InputStream getFileOrResource(String name) throws IOException { return new FileInputStream(name); } - - } diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/db/FederationHubDatabase.java b/src/federation-common/src/main/java/tak/server/federation/hub/db/FederationHubDatabase.java index f61af742..37e35428 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/db/FederationHubDatabase.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/db/FederationHubDatabase.java @@ -17,6 +17,9 @@ public class FederationHubDatabase { private MongoDatabase cotDatabase; private boolean dbIsConnected = false; + // no-op for when db is disabled + public FederationHubDatabase() {} + public FederationHubDatabase(String username, String password, String host, int port) { try { mongo = new AbstractMongoClientConfiguration() { diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/FederationHubUIConfig.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/FederationHubUIConfig.java index b7a26bc5..a2274b4d 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/ui/FederationHubUIConfig.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/FederationHubUIConfig.java @@ -34,6 +34,8 @@ public class FederationHubUIConfig { private String keycloakAdminClaimValue; private String keycloakAccessTokenName = "access_token"; private String keycloakRefreshTokenName = "refresh_token"; + + private boolean enableFlowIndicators = true; public String getKeystoreType() { return keystoreType; @@ -175,17 +177,23 @@ public String getKeycloakAdminClaimValue() { public void setKeycloakAdminClaimValue(String keycloakAdminClaimValue) { this.keycloakAdminClaimValue = keycloakAdminClaimValue; } + public boolean isEnableFlowIndicators() { + return enableFlowIndicators; + } + public void setEnableFlowIndicators(boolean enableFlowIndicators) { + this.enableFlowIndicators = enableFlowIndicators; + } @Override public String toString() { return "FederationHubUIConfig [keystoreType=" + keystoreType + ", keystoreFile=" + keystoreFile - + ", keystorePassword=" + keystorePassword + ", truststoreType=" + truststoreType + ", truststoreFile=" - + truststoreFile + ", keyAlias=" + keyAlias + ", authUsers=" + authUsers + ", port=" + port - + ", allowOauth=" + allowOauth + ", oauthPort=" + oauthPort + ", keycloakServerName=" - + keycloakServerName + ", keycloakDerLocation=" + keycloakDerLocation + ", keycloakClientId=" - + keycloakClientId + ", keycloakSecret=" + keycloakSecret + ", keycloakrRedirectUri=" + + ", truststoreType=" + truststoreType + ", truststoreFile=" + truststoreFile + ", keyAlias=" + keyAlias + + ", authUsers=" + authUsers + ", port=" + port + ", allowOauth=" + allowOauth + ", oauthPort=" + + oauthPort + ", keycloakServerName=" + keycloakServerName + ", keycloakDerLocation=" + + keycloakDerLocation + ", keycloakClientId=" + keycloakClientId + ", keycloakrRedirectUri=" + keycloakrRedirectUri + ", keycloakAuthEndpoint=" + keycloakAuthEndpoint + ", keycloakTokenEndpoint=" - + keycloakTokenEndpoint + ", keycloakAccessTokenName=" + keycloakAccessTokenName - + ", keycloakRefreshTokenName=" + keycloakRefreshTokenName + ", keycloakClaimName=" + keycloakClaimName - + ", keycloakAdminClaimValue=" + keycloakAdminClaimValue + "]"; + + keycloakTokenEndpoint + ", keycloakClaimName=" + keycloakClaimName + ", keycloakAdminClaimValue=" + + keycloakAdminClaimValue + ", keycloakAccessTokenName=" + keycloakAccessTokenName + + ", keycloakRefreshTokenName=" + keycloakRefreshTokenName + ", enableFlowIndicators=" + + enableFlowIndicators + "]"; } } diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationPolicyModel.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationPolicyModel.java index 0e4045ea..5d95ef3f 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationPolicyModel.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationPolicyModel.java @@ -104,6 +104,17 @@ public FederationPolicyGraph getPolicyGraphFromModel() { group.setFilterExpression(groupCell.getProperties().getFilterExpression()); policyGraph.addGroup(group); }); + + cells.stream().filter(cell -> cell instanceof FederationTokenGroupCell).forEach(cell -> { + FederationTokenGroupCell tokenGroupCell = (FederationTokenGroupCell) cell; + FederateIdentity identity = new FederateIdentity(tokenGroupCell.getProperties().getName()); + FederateGroup group = new FederateGroup(identity); + group.getAttributes().putAll(tokenGroupCell.getProperties().attributesToMap()); + group.setInterconnected(tokenGroupCell.getProperties().isInterconnected()); + group.setFilterExpression(tokenGroupCell.getProperties().getFilterExpression()); + policyGraph.addGroup(group); + }); + cells.stream().filter(cell -> cell instanceof FederateCell).forEach( cell -> { FederateCell federateCell = (FederateCell) cell; @@ -198,6 +209,15 @@ public FederationPolicy getFederationPolicyObjectFromModelWithoutGraphData() { group.setFilterExpression(groupCell.getProperties().getFilterExpression()); policy.getGroups().add(group); }); + + cells.stream().filter(cell -> cell instanceof FederationTokenGroupCell).forEach(cell -> { + FederationTokenGroupCell federationTokenGroupCell = (FederationTokenGroupCell) cell; + GroupHolder group = new GroupHolder(federationTokenGroupCell.getProperties().getName()); + group.setAttributes(federationTokenGroupCell.getProperties().attributesToMap()); + group.setInterconnected(federationTokenGroupCell.getProperties().isInterconnected()); + group.setFilterExpression(federationTokenGroupCell.getProperties().getFilterExpression()); + policy.getGroups().add(group); + }); } return policy; @@ -210,9 +230,10 @@ private String getCellNameFromId(String id) { return ((FederateCell) cell).getProperties().getName(); } else if (cell instanceof GroupCell) { return ((GroupCell) cell).getProperties().getName(); - } - else if (cell instanceof FederationOutgoingCell) { + } else if (cell instanceof FederationOutgoingCell) { return ((FederationOutgoingCell) cell).getProperties().getName(); + } else if (cell instanceof FederationTokenGroupCell) { + return ((FederationTokenGroupCell) cell).getProperties().getName(); } return ((EdgeCell) cell).getProperties().getName(); } diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationTokenGroupCell.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationTokenGroupCell.java new file mode 100644 index 00000000..02641f6f --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationTokenGroupCell.java @@ -0,0 +1,31 @@ +package tak.server.federation.hub.ui.graph; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonTypeName; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + +@JsonTypeName("FederationTokenGroupCell") +public class FederationTokenGroupCell extends PolicyObjectCell { + private String type = "FederationTokenGroup"; + + @JsonProperty("roger_federation") + @JsonDeserialize(as=FederationTokenGroupProperties.class) + private FederationTokenGroupProperties properties; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public FederationTokenGroupProperties getProperties() { + return properties; + } + + public void setProperties(FederationTokenGroupProperties properties) { + this.properties = properties; + } + +} diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationTokenGroupProperties.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationTokenGroupProperties.java new file mode 100644 index 00000000..2a2f8cfc --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/FederationTokenGroupProperties.java @@ -0,0 +1,53 @@ +package tak.server.federation.hub.ui.graph; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +public class FederationTokenGroupProperties extends NodeProperties { + private boolean interconnected; + @JsonProperty("groupFilters") + private List filters; + private List tokens; + + public boolean isInterconnected() { + return interconnected; + } + + public void setInterconnected(boolean interconnected) { + this.interconnected = interconnected; + } + + public List getTokens() { + return tokens; + } + + public void setTokens(List tokens) { + this.tokens = tokens; + } + + public List getFilters() { + return filters; + } + + public void setFilters(List filters) { + this.filters = filters; + } + + /** + * Returns the filter expression generated from the filter node tree of this edge. If there are multiple top level + * nodes, they will be OR'ed. + */ + @JsonIgnore + public String getFilterExpression() { + if (filters.size() < 1) { + return ""; + } else if (filters.size() > 1) { + return FilterUtils.filterNodeToString(filters.get(0)); + } + + return FilterUtils.oredFiltersToString(filters); + } + +} diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/JwtTokenRequestModel.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/JwtTokenRequestModel.java new file mode 100644 index 00000000..fe43eccf --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/JwtTokenRequestModel.java @@ -0,0 +1,30 @@ +package tak.server.federation.hub.ui.graph; + +public class JwtTokenRequestModel { + private String clientFingerprint; + private String clientGroup; + private long expiration; + public String getClientFingerprint() { + return clientFingerprint; + } + public void setClientFingerprint(String clientFingerprint) { + this.clientFingerprint = clientFingerprint; + } + public String getClientGroup() { + return clientGroup; + } + public void setClientGroup(String clientGroup) { + this.clientGroup = clientGroup; + } + public long getExpiration() { + return expiration; + } + public void setExpiration(long expiration) { + this.expiration = expiration; + } + @Override + public String toString() { + return "JwtTokenRequestModel [clientFingerprint=" + clientFingerprint + ", clientGroup=" + clientGroup + + ", expiration=" + expiration + "]"; + } +} diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/JwtTokenResponseModel.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/JwtTokenResponseModel.java new file mode 100644 index 00000000..b0627704 --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/JwtTokenResponseModel.java @@ -0,0 +1,18 @@ +package tak.server.federation.hub.ui.graph; + +public class JwtTokenResponseModel { + private String token; + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + @Override + public String toString() { + return "JwtTokenResponseModel [token=" + token + "]"; + } +} diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/PolicyObjectCell.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/PolicyObjectCell.java index f312ad5f..1ffb11e2 100644 --- a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/PolicyObjectCell.java +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/PolicyObjectCell.java @@ -18,6 +18,7 @@ @JsonSubTypes({ @JsonSubTypes.Type(value = FederateCell.class, name = "FederateCell"), @JsonSubTypes.Type(value = GroupCell.class, name= "GroupCell"), @JsonSubTypes.Type(value = FederationOutgoingCell.class, name= "FederationOutgoingCell"), + @JsonSubTypes.Type(value = FederationTokenGroupCell.class, name= "FederationTokenGroupCell"), @JsonSubTypes.Type(value = EdgeCell.class, name = "EdgeCell")}) public abstract class PolicyObjectCell { diff --git a/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/TokenNode.java b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/TokenNode.java new file mode 100644 index 00000000..fa07fd3b --- /dev/null +++ b/src/federation-common/src/main/java/tak/server/federation/hub/ui/graph/TokenNode.java @@ -0,0 +1,19 @@ +package tak.server.federation.hub.ui.graph; + +public class TokenNode { + private String token; + private long expiration; + + public String getToken() { + return token; + } + public void setToken(String token) { + this.token = token; + } + public long getExpiration() { + return expiration; + } + public void setExpiration(long expiration) { + this.expiration = expiration; + } +} diff --git a/src/federation-hub-broker/build.gradle b/src/federation-hub-broker/build.gradle index 411140da..ffa0dae1 100644 --- a/src/federation-hub-broker/build.gradle +++ b/src/federation-hub-broker/build.gradle @@ -65,7 +65,8 @@ dependencies { implementation group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: spring_boot_version implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-mongodb', version: spring_boot_version api group: 'com.github.ben-manes.caffeine', name: 'caffeine', version: caffeine_version - + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: jsonwebtoken_version + testImplementation group: 'junit', name: 'junit', version: junit_version testImplementation group: 'org.mockito', name: 'mockito-core', version: mockito_version testImplementation("org.springframework.boot:spring-boot-starter-test:$spring_boot_version") { diff --git a/src/federation-hub-broker/scripts/db/configure.sh b/src/federation-hub-broker/scripts/db/configure.sh index 2b8ead32..0e96d774 100755 --- a/src/federation-hub-broker/scripts/db/configure.sh +++ b/src/federation-hub-broker/scripts/db/configure.sh @@ -12,8 +12,16 @@ sleep 1 # create data directory, set mongod permissions, remove lock file echo "Creating Mongo data directory at /var/lib/mongodb" mkdir -p /var/lib/mongodb -chown -R mongod:mongod /var/lib/mongodb -chcon -Rv --type=mongod_var_lib_t /var/lib/mongodb + +# Debian installs +if [ -f /etc/debian_version ]; then + chown -R mongodb:mongodb /var/lib/mongodb +# Other OS +else + chown -R mongod:mongod /var/lib/mongodb + chcon -Rv --type=mongod_var_lib_t /var/lib/mongodb +fi + rm -f /tmp/mongodb-27017.sock sleep 1 cp /opt/tak/federation-hub/scripts/db/mongod-noauth.conf /etc/mongod.conf diff --git a/src/federation-hub-broker/scripts/db/setup-db.sh b/src/federation-hub-broker/scripts/db/setup-db.sh index b81e2eef..36b298d4 100755 --- a/src/federation-hub-broker/scripts/db/setup-db.sh +++ b/src/federation-hub-broker/scripts/db/setup-db.sh @@ -4,7 +4,7 @@ setup_mongo () { # try to get username from /opt/tak/CoreConfig.xml if [ -f "/opt/tak/federation-hub/configs/federation-hub-broker.yml" ]; then - username=$(echo $(grep -m 1 "dbUsername:" /opt/tak/federation-hub/configs/federation-hub-broker.yml) | sed 's/.*dbUsername: *//') + username=$(echo $(grep -m 1 "dbUsername:" /opt/tak/federation-hub/configs/federation-hub-broker.yml) | sed 's/.*dbUsername: *//' | sed 's/"//g') fi # cant find username - use default @@ -17,7 +17,7 @@ setup_mongo () { # try to get password from /opt/tak/CoreConfig.xml if [ -f "/opt/tak/federation-hub/configs/federation-hub-broker.yml" ]; then - password=$(echo $(grep -m 1 "dbPassword:" /opt/tak/federation-hub/configs/federation-hub-broker.yml) | sed 's/.*dbPassword: *//') + password=$(echo $(grep -m 1 "dbPassword:" /opt/tak/federation-hub/configs/federation-hub-broker.yml) | sed 's/.*dbPassword: *//' | sed 's/"//g') fi # cant find password - generate one @@ -34,7 +34,6 @@ setup_mongo () { sleep 1 $(echo "mongosh admin --file /opt/tak/federation-hub/scripts/db/create_user.js") sleep 1 - rm "/opt/tak/federation-hub/scripts/db/create_user.js" } setup_mongo diff --git a/src/federation-hub-broker/scripts/federation-hub-broker b/src/federation-hub-broker/scripts/federation-hub-broker index d167f371..2ad603ac 100755 --- a/src/federation-hub-broker/scripts/federation-hub-broker +++ b/src/federation-hub-broker/scripts/federation-hub-broker @@ -31,7 +31,7 @@ FEDERATION_HUB_HOME=/opt/tak/federation-hub case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${FEDERATION_HUB_HOME}/scripts && ./federation-hub-broker.sh &> /opt/tak/federation-hub/logs/federation-hub-broker-console.log &" + su tak -c "cd ${FEDERATION_HUB_HOME}/scripts && ./federation-hub-broker.sh > /opt/tak/federation-hub/logs/federation-hub-broker-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerService.java b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerService.java index 769d97a5..e77bd51e 100644 --- a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerService.java +++ b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubBrokerService.java @@ -1,8 +1,10 @@ package tak.server.federation.hub.broker; +import static io.grpc.Metadata.ASCII_STRING_MARSHALLER; import static java.util.Objects.requireNonNull; import java.io.FileInputStream; +import java.io.IOException; import java.math.BigInteger; import java.net.SocketAddress; import java.rmi.RemoteException; @@ -10,6 +12,8 @@ import java.security.KeyStoreException; import java.security.cert.Certificate; import java.security.cert.CertificateEncodingException; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Arrays; @@ -24,6 +28,7 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executors; @@ -41,7 +46,9 @@ import javax.net.ssl.SSLException; import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; +import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; import org.antlr.v4.runtime.ANTLRInputStream; import org.antlr.v4.runtime.BailErrorStrategy; @@ -62,6 +69,7 @@ import com.atakmap.Tak.FederateProvenance; import com.atakmap.Tak.FederatedChannelGrpc; import com.atakmap.Tak.FederatedEvent; +import com.atakmap.Tak.FederatedEvent.Builder; import com.atakmap.Tak.Identity; import com.atakmap.Tak.ROL; import com.atakmap.Tak.ServerHealth; @@ -72,6 +80,7 @@ import com.google.common.collect.Sets; import com.google.gson.Gson; +import io.grpc.Attributes.Key; import io.grpc.Context; import io.grpc.Contexts; import io.grpc.Grpc; @@ -85,6 +94,8 @@ import io.grpc.StatusRuntimeException; import io.grpc.netty.NettyServerBuilder; import io.grpc.stub.StreamObserver; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.PooledByteBufAllocator; import io.netty.channel.ChannelFuture; @@ -112,13 +123,17 @@ import mil.af.rl.rol.value.Parameters; import tak.server.federation.Federate; import tak.server.federation.FederateEdge; +import tak.server.federation.FederateGroup; import tak.server.federation.FederateIdentity; import tak.server.federation.FederationException; import tak.server.federation.FederationNode; import tak.server.federation.FederationPolicyGraph; import tak.server.federation.GuardedStreamHolder; +import tak.server.federation.TokenAuthCredential; import tak.server.federation.hub.FederationHubCache; import tak.server.federation.hub.FederationHubResources; +import tak.server.federation.hub.FederationHubUtils; +import tak.server.federation.hub.FedhubJwtUtils; import tak.server.federation.hub.broker.db.FederationHubMissionDisruptionManager; import tak.server.federation.hub.broker.events.BrokerServerEvent; import tak.server.federation.hub.broker.events.ForceDisconnectEvent; @@ -128,7 +143,9 @@ import tak.server.federation.hub.broker.events.UpdatePolicy; import tak.server.federation.hub.policy.FederationHubPolicyManager; import tak.server.federation.hub.ui.graph.FederationOutgoingCell; +import tak.server.federation.hub.ui.graph.FederationTokenGroupCell; import tak.server.federation.hub.ui.graph.PolicyObjectCell; +import tak.server.federation.hub.ui.graph.TokenNode; import tak.server.federation.rol.MissionRolVisitor; public class FederationHubBrokerService implements ApplicationListener { @@ -142,7 +159,7 @@ public class FederationHubBrokerService implements ApplicationListener portToServerMap = new HashMap<>(); + private SyncService syncService = new FileCacheSyncService(); private final FederationProcessorFactory federationProcessorFactory = new FederationProcessorFactory(); @@ -184,12 +202,12 @@ public static FederationHubBrokerService getInstance() { private ContinuousQuery continuousConfigurationQuery = new ContinuousQuery<>(); - public FederationHubBrokerService(Ignite ignite, SSLConfig sslConfig, FederationHubServerConfig fedHubConfig, FederationHubPolicyManager fedHubPolicyManager, HubConnectionStore hubConnectionStore, FederationHubMissionDisruptionManager federationHubMissionDisruptionManager, + public FederationHubBrokerService(Ignite ignite, SSLConfig sslConfig, FederationHubServerConfigManager fedHubConfigManager, FederationHubPolicyManager fedHubPolicyManager, HubConnectionStore hubConnectionStore, FederationHubMissionDisruptionManager federationHubMissionDisruptionManager, FederationHubBrokerMetrics fedHubBrokerMetrics) { instance = this; this.ignite = ignite; this.sslConfig = sslConfig; - this.fedHubConfig = fedHubConfig; + this.fedHubConfigManager = fedHubConfigManager; this.fedHubPolicyManager = fedHubPolicyManager; this.hubConnectionStore = hubConnectionStore; this.fedHubBrokerMetrics = fedHubBrokerMetrics; @@ -205,6 +223,7 @@ public FederationHubBrokerService(Ignite ignite, SSLConfig sslConfig, Federation continuousConfigurationQuery.setLocalListener((evts) -> { for (CacheEntryEvent e : evts) { federationPolicyGraph = e.getValue(); + policyCells = fedHubPolicyManager.getPolicyCells(); } }); @@ -212,6 +231,7 @@ public FederationHubBrokerService(Ignite ignite, SSLConfig sslConfig, Federation } private FederationPolicyGraph federationPolicyGraph; + private Collection policyCells; public FederationPolicyGraph getFederationPolicyGraph() { if (federationPolicyGraph == null) @@ -219,6 +239,13 @@ public FederationPolicyGraph getFederationPolicyGraph() { return federationPolicyGraph; } + + public Collection getFederationPolicyCells() { + if (policyCells == null) + policyCells = fedHubPolicyManager.getPolicyCells(); + + return policyCells; + } private void removeInactiveClientStreams() { if (logger.isDebugEnabled()) { @@ -226,7 +253,7 @@ private void removeInactiveClientStreams() { } for (Map.Entry> clientStreamEntry : hubConnectionStore.getClientStreamMap().entrySet()) { - if (!clientStreamEntry.getValue().isClientHealthy(fedHubConfig.getClientTimeoutTime())) { + if (!clientStreamEntry.getValue().isClientHealthy(fedHubConfigManager.getConfig().getClientTimeoutTime())) { if (logger.isDebugEnabled()) { logger.debug("Detected FederatedEvent client stream {} inactivity", clientStreamEntry.getValue().getFederateIdentity()); @@ -395,7 +422,7 @@ public void sendContactMessagesV1(NioNettyFederationHubServerHandler handler) { /* Mirrors NioNettyBuilder. */ private void setupFederationV1Server() { - boolean useEpoll = Epoll.isAvailable() && fedHubConfig.isUseEpoll(); + boolean useEpoll = Epoll.isAvailable() && fedHubConfigManager.getConfig().isUseEpoll(); bossGroup = useEpoll ? new EpollEventLoopGroup(1) : new NioEventLoopGroup(1); workerGroup = useEpoll ? new EpollEventLoopGroup() : new NioEventLoopGroup(); @@ -407,7 +434,7 @@ private void setupFederationV1Server() { WriteBufferWaterMark waterMark = new WriteBufferWaterMark(lowMark, highMark); /* TODO: set up other fields from xsd of federation-server elements. */ - SslContext sslContext = buildServerSslContext(fedHubConfig); + SslContext sslContext = buildServerSslContext(fedHubConfigManager.getConfig()); ServerBootstrap bootstrap = new ServerBootstrap(); bootstrap.group(bossGroup, workerGroup) @@ -418,7 +445,7 @@ private void setupFederationV1Server() { protected void initChannel(SocketChannel channel) throws Exception { SslHandler sslHandler = sslContext.newHandler(channel.alloc()); sslHandler.engine().setEnabledProtocols( - fedHubConfig.getTlsVersions().toArray(String[]::new)); + fedHubConfigManager.getConfig().getTlsVersions().toArray(String[]::new)); String sessionId = new BigInteger(sslHandler.engine().getSession().getId()).toString(); NioNettyFederationHubServerHandler handler = @@ -448,8 +475,8 @@ public String toString() { .childOption(ChannelOption.WRITE_BUFFER_WATER_MARK, waterMark) .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT); - channelFuture = bootstrap.bind(fedHubConfig.getV1Port()).sync().channel().closeFuture(); - logger.info("Successfully started Federation Hub v1 server on port " + fedHubConfig.getV1Port()); + channelFuture = bootstrap.bind(fedHubConfigManager.getConfig().getV1Port()).sync().channel().closeFuture(); + logger.info("Successfully started Federation Hub v1 server on port " + fedHubConfigManager.getConfig().getV1Port()); } catch (Exception e) { logger.error("Error initializing Federation Hub v1 server", e); } @@ -468,7 +495,7 @@ private void setupFederationV2Server() { throw new RuntimeException(e); } - if (fedHubConfig.isEnableHealthCheck()) { + if (fedHubConfigManager.getConfig().isEnableHealthCheck()) { // Health check thread. Schedule metrics sending every K seconds. inactivitySchedulerFuture = FederationHubResources.healthCheckScheduler.scheduleAtFixedRate(new Runnable() { @Override @@ -479,49 +506,58 @@ public void run() { throw new RuntimeException("Stopping server"); } } - }, fedHubConfig.getClientRefreshTime(), fedHubConfig.getClientRefreshTime(), TimeUnit.SECONDS); + }, fedHubConfigManager.getConfig().getClientRefreshTime(), fedHubConfigManager.getConfig().getClientRefreshTime(), TimeUnit.SECONDS); } + + fedHubConfigManager.getConfig().getFederationTokenAuthServers().forEach(serverConfig -> { + if (Strings.isNullOrEmpty(serverConfig.getType())) { + logger.info("Cannot create token server because the type is invalid " + serverConfig); + } + + if ("jwt".equals(serverConfig.getType().toLowerCase())) { + buildGrpcServer(serverConfig.getPort(), true); + } else { + logger.info("Cannot create token server because the type is invalid " + serverConfig); + } + }); + + buildGrpcServer(fedHubConfigManager.getConfig().getV2Port(), false); + } - NettyServerBuilder serverBuilder = NettyServerBuilder.forPort(fedHubConfig.getV2Port()) - .maxInboundMessageSize(fedHubConfig.getMaxMessageSizeBytes()) - .sslContext(sslConfig.getSslContext()) + private void buildGrpcServer(int port, boolean oauth) { + SslContext sslContext = oauth ? sslConfig.getSslContextNoAuth() : sslConfig.getSslContextClientAuth(); + + NettyServerBuilder serverBuilder = NettyServerBuilder.forPort(port) + .maxInboundMessageSize(fedHubConfigManager.getConfig().getMaxMessageSizeBytes()) + .sslContext(sslContext) .executor(FederationHubResources.federationGrpcExecutor) .workerEventLoopGroup(FederationHubResources.federationGrpcWorkerEventLoopGroup) .bossEventLoopGroup(FederationHubResources.federationGrpcWorkerEventLoopGroup) .channelType(Epoll.isAvailable() ? EpollServerSocketChannel.class : NioServerSocketChannel.class); - if (fedHubConfig.getMaxConcurrentCallsPerConnection() != null - && fedHubConfig.getMaxConcurrentCallsPerConnection() > 0) { - serverBuilder.maxConcurrentCallsPerConnection(fedHubConfig.getMaxConcurrentCallsPerConnection()); + if (fedHubConfigManager.getConfig().getMaxConcurrentCallsPerConnection() != null + && fedHubConfigManager.getConfig().getMaxConcurrentCallsPerConnection() > 0) { + serverBuilder.maxConcurrentCallsPerConnection(fedHubConfigManager.getConfig().getMaxConcurrentCallsPerConnection()); } FederatedChannelService service = new FederatedChannelService(); - server = serverBuilder.addService(ServerInterceptors.intercept(service, tlsInterceptor())).build(); - - /* TODO What is the purpose of this? */ - service.binaryMessageStream(new StreamObserver() { - @Override - public void onNext(Empty value) { - } - - @Override - public void onError(Throwable t) { - } - - @Override - public void onCompleted() { - } - }); + if (oauth) { + Server server = serverBuilder.addService(ServerInterceptors.intercept(service, oauthInterceptor())).build(); + portToServerMap.put(port, server); + } else { + Server server = serverBuilder.addService(ServerInterceptors.intercept(service, tlsInterceptor())).build(); + portToServerMap.put(port, server); + } Executors.newSingleThreadExecutor().submit(new Runnable() { @Override public void run() { - requireNonNull(fedHubConfig, "Federation Hub configuration object"); + requireNonNull(fedHubConfigManager.getConfig(), "Federation Hub configuration object"); try { - server.start(); - logger.info("Federation Hub (v2 protocol) started, listening on port " + fedHubConfig.getV2Port()); + portToServerMap.get(port).start(); + logger.info("Federation Hub (v2 protocol) started, listening on port " + port); Runtime.getRuntime().addShutdownHook(new Thread() { @Override @@ -550,19 +586,20 @@ public void onApplicationEvent(BrokerServerEvent event) { } if (event instanceof RestartServerEvent) { - if (fedHubConfig.isV2Enabled()) { - logger.info("Restarting V2 federation server after truststore update"); - server.shutdown(); - try { - server.awaitTermination(); - } catch (InterruptedException e) { } + if (fedHubConfigManager.getConfig().isV2Enabled()) { + for (Server server: portToServerMap.values()) { + server.shutdown(); + try { + server.awaitTermination(); + } catch (InterruptedException e) { } + } + keepRunning.set(false); - hubConnectionStore.clearStreamMaps(); clientMessageCounter.set(0); clientByteAccumulator.set(0); keepRunning.set(true); - sslConfig.initSslContext(fedHubConfig); + sslConfig.initSslContext(fedHubConfigManager.getConfig()); setupFederationV2Server(); } @@ -584,27 +621,21 @@ public void onApplicationEvent(BrokerServerEvent event) { // we can re-add them based on their active sessions hubConnectionStore.getClientStreamMap().entrySet().forEach(entry -> { - SSLSession session = hubConnectionStore.getSessionMap().get(entry.getKey()); - if (session != null && session.isValid()) { - addFederateToGroupPolicyIfMissingV2(hubConnectionStore.getSessionMap().get(entry.getKey()) ,entry.getValue()); - - // check if the currently connected spoke is still allowed to be connected after the policy change - FederationPolicyGraph fpg = getFederationPolicyGraph(); - Federate clientNode = checkFederateExistsInPolicy(entry.getValue(), session, fpg); - if (clientNode == null) { - logger.info("Permission Denied. Federate/CA Group not found in the policy graph: " + entry.getValue().getFederateIdentity()); - entry.getValue().throwPermissionDeniedToClient(); - - HubFigClient client = outgoingClientMap.get(entry.getKey()); - if (client != null) - client.processDisconnect(); - - return; - } + addFederateToGroupPolicyIfMissingV2(entry.getValue()); - } else { - hubConnectionStore.removeSession(entry.getKey()); - } + // check if the currently connected spoke is still allowed to be connected after the policy change + FederationPolicyGraph fpg = getFederationPolicyGraph(); + Federate clientNode = checkFederateExistsInPolicy(entry.getValue(), fpg); + if (clientNode == null) { + logger.info("Permission Denied. Federate/CA Group not found in the policy graph: " + entry.getValue().getFederateIdentity()); + entry.getValue().throwPermissionDeniedToClient(); + + HubFigClient client = outgoingClientMap.get(entry.getKey()); + if (client != null) + client.processDisconnect(); + + return; + } }); v1ClientStreamMap.entrySet().forEach(entry -> { @@ -618,6 +649,14 @@ public void onApplicationEvent(BrokerServerEvent event) { }); updateOutgoingConnections(((UpdatePolicy) event).getOutgoings()); + + for (Entry> groupStreamEntry : hubConnectionStore.getClientGroupStreamMap().entrySet()) { + String fedId = groupStreamEntry.getValue().getFederateIdentity().getFedId(); + FederateGroups federateGroups = FederationHubBrokerService.getInstance().getFederationHubGroups(fedId).toBuilder() + .setStreamUpdate(ServerHealth.newBuilder().setStatus(ServerHealth.ServingStatus.SERVING).build()) + .build(); + groupStreamEntry.getValue().send(federateGroups); + } } if (event instanceof StreamReadyEvent) { @@ -697,7 +736,7 @@ private synchronized void updateOutgoingConnections(List outgoingConfigMap.entrySet().forEach(e -> { try { if (e.getValue().getProperties().isOutgoingEnabled()) { - HubFigClient client = new HubFigClient(fedHubConfig, federationHubMissionDisruptionManager, e.getValue()); + HubFigClient client = new HubFigClient(fedHubConfigManager, federationHubMissionDisruptionManager, e.getValue()); client.start(); outgoingClientMap.put(e.getKey(), client); } @@ -726,11 +765,11 @@ public void scheduleRetry(String name, FederationOutgoingCell outgoing) { if (outgoingClientRetryMap.get(name) != null) outgoingClientRetryMap.get(name).cancel(true); - if (fedHubConfig.getOutgoingReconnectSeconds() > 0 && outgoing.getProperties().isOutgoingEnabled()) { - logger.info("Connection for {} failed. Trying again in {} seconds.", name, fedHubConfig.getOutgoingReconnectSeconds()); + if (fedHubConfigManager.getConfig().getOutgoingReconnectSeconds() > 0 && outgoing.getProperties().isOutgoingEnabled()) { + logger.info("Connection for {} failed. Trying again in {} seconds.", name, fedHubConfigManager.getConfig().getOutgoingReconnectSeconds()); ScheduledFuture future = FederationHubResources.retryScheduler.scheduleAtFixedRate(() -> { attemptRetry(name, outgoing); - }, fedHubConfig.getOutgoingReconnectSeconds(), fedHubConfig.getOutgoingReconnectSeconds(), TimeUnit.SECONDS); + }, fedHubConfigManager.getConfig().getOutgoingReconnectSeconds(), fedHubConfigManager.getConfig().getOutgoingReconnectSeconds(), TimeUnit.SECONDS); outgoingClientRetryMap.put(name, future); } else { @@ -740,7 +779,7 @@ public void scheduleRetry(String name, FederationOutgoingCell outgoing) { private void attemptRetry(String name, FederationOutgoingCell outgoing) { try { - HubFigClient client = new HubFigClient(fedHubConfig, federationHubMissionDisruptionManager, outgoing); + HubFigClient client = new HubFigClient(fedHubConfigManager, federationHubMissionDisruptionManager, outgoing); client.start(); outgoingClientMap.put(name, client); outgoingClientRetryMap.get(name).cancel(true); @@ -752,13 +791,13 @@ private void attemptRetry(String name, FederationOutgoingCell outgoing) { } public void setupFederationServers() { - sslConfig.initSslContext(fedHubConfig); + sslConfig.initSslContext(fedHubConfigManager.getConfig()); - if (fedHubConfig.isV2Enabled()) { + if (fedHubConfigManager.getConfig().isV2Enabled()) { setupFederationV2Server(); } - if (fedHubConfig.isV1Enabled()) { + if (fedHubConfigManager.getConfig().isV1Enabled()) { setupFederationV1Server(); } @@ -775,9 +814,9 @@ public void setupFederationServers() { } public void stop() { - if (server != null) { - server.shutdown(); - } + for (Server server : portToServerMap.values()) { + server.shutdown(); + } } private void sendCaGroupsToFedManager(KeyStore keyStore) throws KeyStoreException { @@ -831,21 +870,57 @@ public void addFederateToGroupPolicyIfMissingV1(Certificate[] certArray, } } - public void addFederateToGroupPolicyIfMissingV2(SSLSession session, GuardedStreamHolder holder) { - hubConnectionStore.addSession(new BigInteger(session.getId()).toString(), session); - String fedId = holder.getFederateIdentity().getFedId(); + public void addFederateToGroupPolicyIfMissingV2(GuardedStreamHolder holder) { + FederateIdentity federateIdentity = holder.getFederateIdentity(); FederationPolicyGraph fpg = getFederationPolicyGraph(); - if (fpg.getNode(fedId) == null) { - try { - Certificate[] certArray = session.getPeerCertificates(); - addCaFederateToPolicyGraph(holder.getFederateIdentity(), certArray); - } catch (SSLPeerUnverifiedException e) { - logger.error("Could not get peer certificates from the SSL session", e); - } + if (fpg.getNode(federateIdentity.getFedId()) == null) { + @SuppressWarnings("unchecked") + List clientGroups = holder.getClientGroups(); + + Federate federate = new Federate(federateIdentity); + synchronized (federationPolicyGraph) { + federationPolicyGraph = fedHubPolicyManager.addCaFederate(federate, clientGroups); + } } } + + private FederateGroups removeFilteredGroupsFromFederatedGroups(FederateGroups groups, Set allowedGroups, Set disallowedGroups) { + if (groups.getNestedGroupsList().isEmpty()) { + FederateGroups.Builder builder = FederateGroups.newBuilder(groups); + builder.clearFederateGroups(); + + List existingGroups = groups.getFederateGroupsList(); + + List filteredGroups = existingGroups.stream().filter(g -> { + return allowedGroups.contains(g) && !disallowedGroups.contains(g); + }).collect(Collectors.toList()); + + builder.addAllFederateGroups(filteredGroups); + return builder.build(); + } else { + FederateGroups.Builder builder = FederateGroups.newBuilder(groups); + builder.clearFederateGroups(); + builder.clearNestedGroups(); + + for (FederateGroups nestedGroup: groups.getNestedGroupsList()) { + FederateGroups filteredNestedGroup = removeFilteredGroupsFromFederatedGroups(nestedGroup, allowedGroups, disallowedGroups); + builder.addNestedGroups(filteredNestedGroup); + } + + Set federatedGroupsNameSet = + builder.getNestedGroupsList() + .stream() + .map(g->g.getFederateGroupsList()) + .flatMap(list -> list.stream()) + .collect(Collectors.toCollection(HashSet::new)); + + builder.addAllFederateGroups(federatedGroupsNameSet); + + return builder.build(); + } + } // collect all groups for federates connected to the hub that can reach the given federate public FederateGroups getFederationHubGroups(String selfId) { @@ -857,7 +932,7 @@ public FederateGroups getFederationHubGroups(String selfId) { .stream() .map(f -> f.getFederateIdentity().getFedId()) .collect(Collectors.toCollection(HashSet::new)); - + List federatedGroups = hubConnectionStore.getClientGroupStreamMap().entrySet() .stream() // ignore self groups @@ -868,12 +943,44 @@ public FederateGroups getFederationHubGroups(String selfId) { .filter(e -> hubConnectionStore.getClientToGroupsMap().get(e.getKey()) != null) .map(e -> { FederateGroups groups = hubConnectionStore.getClientToGroupsMap().get(e.getKey()); + + FederationPolicyGraph policyGraph = getFederationPolicyGraph(); + Federate srcNode = policyGraph.getFederate(e.getValue().getFederateIdentity().getFedId()); + Federate destNode = policyGraph.getFederate(selfId); + FederateEdge edge = policyGraph.getEdge(srcNode, destNode); + + Set allowedGroups = new HashSet<>(); + Set disallowedGroups = new HashSet<>(); + switch (edge.getFilterType()) { + case ALL: { + allowedGroups.addAll(groups.getFederateGroupsList()); + break; + } + case ALLOWED: { + allowedGroups.addAll(edge.getAllowedGroups()); + break; + } + case DISALLOWED: { + disallowedGroups.addAll(edge.getDisallowedGroups()); + break; + } + case ALLOWED_AND_DISALLOWED: { + allowedGroups.addAll(edge.getAllowedGroups()); + disallowedGroups.addAll(edge.getDisallowedGroups()); + break; + } + default: + break; + } + // return the top level group if there are no nested ones // otherwise ignore the top level group, and return the list of nested ones + FederateGroups filteredGroups = removeFilteredGroupsFromFederatedGroups(groups, allowedGroups, disallowedGroups); + if (groups.getNestedGroupsList().isEmpty()) { - return Arrays.asList(groups); + return Arrays.asList(filteredGroups); } else { - return groups.getNestedGroupsList(); + return filteredGroups.getNestedGroupsList(); } }) // flatmap the lists of FederateGroups @@ -903,13 +1010,12 @@ public FederateGroups getFederationHubGroups(String selfId) { return builder.build(); }) .collect(Collectors.toList()); - + // the reason we must use nested groups here is to maintain the hop limit of each individual FederateGroups object. // all of the group name strings will still be added to getFederateGroupsList() as usual. TAK Servers will still only look at // the getFederateGroupsList(). but federation hubs will look at getNestedGroupsList() before sending to ensure the // hop limit has not been reached. (TAK Servers are end of line, so they don't need to check) FederateGroups.Builder builder = FederateGroups.newBuilder(); - federatedGroups.forEach(g -> builder.addNestedGroups(g)); Set federatedGroupsNameSet = @@ -920,7 +1026,7 @@ public FederateGroups getFederationHubGroups(String selfId) { .collect(Collectors.toCollection(HashSet::new)); builder.addAllFederateGroups(federatedGroupsNameSet); - + return builder.build(); } catch (FederationException e) { logger.error("Could not get Federation Hub Groups", e); @@ -933,42 +1039,46 @@ private class FederatedChannelService extends FederatedChannelGrpc.FederatedChan private final FederationHubBrokerService broker = FederationHubBrokerService.this; AtomicReference start = new AtomicReference<>(); - - @Override public void serverFederateGroupsStream(Subscription request, StreamObserver responseObserver) { try { - SSLSession session = (SSLSession) sslSessionKey.get(Context.current()); - String id = new BigInteger(session.getId()).toString(); - Certificate[] clientCertArray = requireNonNull( - requireNonNull(session, "SSL Session").getPeerCertificates(), "SSL peer certs array"); + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); - if (clientCertArray.length == 0) { - throw new IllegalArgumentException("Client certificate not available"); + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identification not available"); } GuardedStreamHolder streamHolder = new GuardedStreamHolder( responseObserver, request.getIdentity().getName(), - FederationUtils.getBytesSHA256(clientCertArray[0].getEncoded()), session, request, + clientFingerprint, sessionId, request, new Comparator() { @Override public int compare(FederateGroups a, FederateGroups b) { return ComparisonChain.start().compare(a.hashCode(), b.hashCode()).result(); } }, true); + + streamHolder.setClientFingerprint(clientFingerprint); + streamHolder.setClientGroups(clientGroups); - addFederateToGroupPolicyIfMissingV2(session, streamHolder); + addFederateToGroupPolicyIfMissingV2(streamHolder); FederationPolicyGraph fpg = getFederationPolicyGraph(); requireNonNull(fpg, "federation policy graph object"); - Federate clientNode = checkFederateExistsInPolicy(streamHolder, session, fpg); + Federate clientNode = checkFederateExistsInPolicy(streamHolder, fpg); if (clientNode == null) { responseObserver.onError(new StatusRuntimeException(Status.PERMISSION_DENIED)); return; } - hubConnectionStore.addGroupStream(id, streamHolder); + hubConnectionStore.addGroupStream(sessionId, streamHolder); // when a client connects to the hub, indicate a serving status and send it the // current list of groups @@ -976,13 +1086,13 @@ public int compare(FederateGroups a, FederateGroups b) { getFederationHubGroups(streamHolder.getFederateIdentity().getFedId()).toBuilder() .setStreamUpdate(ServerHealth.newBuilder().setStatus(ServerHealth.ServingStatus.SERVING).build()) .build(); - + // do not use the stream holder to send the initial groups. // since this is a handshake, we are expecting groups back, // and the stream holder will attach the provenance, causing // it to be dropped on the way back responseObserver.onNext(federateGroups); - } catch (SSLPeerUnverifiedException | CertificateEncodingException e) { + } catch (Exception e) { throw new RuntimeException("Error in serverFederateGroupsStream", e); } } @@ -992,28 +1102,36 @@ public StreamObserver clientFederateGroupsStream(StreamObserver< return new StreamObserver() { @Override public void onNext(FederateGroups fedGroups) { - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - String id = new BigInteger(session.getId()).toString(); + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); + + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint== null || clientGroups== null) { + throw new IllegalArgumentException("Client identification not available"); + } - GuardedStreamHolder holder = hubConnectionStore.getClientStreamMap().get(id); + GuardedStreamHolder holder = hubConnectionStore.getClientStreamMap().get(sessionId); if (holder != null) { - addFederateToGroupPolicyIfMissingV2(session, hubConnectionStore.getClientStreamMap().get(id)); + addFederateToGroupPolicyIfMissingV2(hubConnectionStore.getClientStreamMap().get(sessionId)); } - GuardedStreamHolder groupHolder = hubConnectionStore.getClientGroupStreamMap().get(id); + GuardedStreamHolder groupHolder = hubConnectionStore.getClientGroupStreamMap().get(sessionId); if (groupHolder != null) { - addFederateToGroupPolicyIfMissingV2(session, hubConnectionStore.getClientGroupStreamMap().get(id)); + addFederateToGroupPolicyIfMissingV2(hubConnectionStore.getClientGroupStreamMap().get(sessionId)); } - addFederateGroups(id, fedGroups); + addFederateGroups(sessionId, fedGroups); } @Override public void onError(Throwable t) { logger.error("clientFederateGroupsStream ",t); - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - String id = new BigInteger(session.getId()).toString(); - hubConnectionStore.clearIdFromAllStores(id); + String sessionId = sslSessionIdKey.get(Context.current()); + hubConnectionStore.clearIdFromAllStores(sessionId); responseObserver.onError(t); } @@ -1039,15 +1157,26 @@ public StreamObserver binaryMessageStream(StreamObserver resp @Override public void onNext(BinaryBlob value) { long latency = new Date().getTime() - value.getTimestamp(); - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - logger.info("binaryMessageStream received binary file from client: " + + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); + + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identification not available"); + } + + logger.info("binaryMessageStream received binary file from client: " + value.getDescription() + " " + new Date(value.getTimestamp()) + " " + value.getSerializedSize() + " bytes (serialized) latency: " + latency + " ms"); - addFederateToGroupPolicyIfMissingV2(session, hubConnectionStore.getClientStreamMap().get(new BigInteger(session.getId()).toString())); + addFederateToGroupPolicyIfMissingV2(hubConnectionStore.getClientStreamMap().get(sessionId)); - FederationHubBrokerService.this.handleRead(value, new BigInteger(session.getId()).toString()); + FederationHubBrokerService.this.handleRead(value, sessionId); } @Override @@ -1066,8 +1195,18 @@ public void onCompleted() { public void sendOneBlob(BinaryBlob request, StreamObserver resp) { start.compareAndSet(null, System.currentTimeMillis()); - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identification not available"); + } + long bytesPerSecond = -1; long elapsed = System.currentTimeMillis() - start.get(); @@ -1094,7 +1233,7 @@ public void sendOneBlob(BinaryBlob request, StreamObserver resp) { clientMessageCounter.incrementAndGet(); clientByteAccumulator.addAndGet(request.getSerializedSize()); - FederationHubBrokerService.this.handleRead(request, new BigInteger(session.getId()).toString()); + FederationHubBrokerService.this.handleRead(request, sessionId); } private final AtomicInteger clientEventStreamCounter = new AtomicInteger(); @@ -1112,8 +1251,6 @@ public void clientEventStream(Subscription subscription, StreamObserver streamHolder = null; if (!Strings.isNullOrEmpty(subscription.getFilter())) { @@ -1125,32 +1262,38 @@ public void clientEventStream(Subscription subscription, StreamObserver clientGroups = clientGroupsKey.get(Context.current()); + + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identifiers not available"); + } try { - Certificate[] clientCertArray = requireNonNull( - requireNonNull(session, "SSL Session").getPeerCertificates(), - "SSL peer certs array"); - - if (clientCertArray.length == 0) { - throw new IllegalArgumentException("Client certificate not available"); - } - streamHolder = new GuardedStreamHolder(clientStream, - clientName, FederationUtils.getBytesSHA256(clientCertArray[0].getEncoded()), - session, subscription, new Comparator() { + clientName, clientFingerprint, + sessionId, subscription, new Comparator() { @Override public int compare(FederatedEvent a, FederatedEvent b) { return ComparisonChain.start().compare(a.hashCode(), b.hashCode()).result(); } }, true ); + streamHolder.setClientFingerprint(clientFingerprint); + streamHolder.setClientGroups(clientGroups); - addFederateToGroupPolicyIfMissingV2(session, streamHolder); + addFederateToGroupPolicyIfMissingV2(streamHolder); FederationPolicyGraph fpg = getFederationPolicyGraph(); requireNonNull(fpg, "federation policy graph object"); - Federate clientNode = checkFederateExistsInPolicy(streamHolder, session, fpg); + Federate clientNode = checkFederateExistsInPolicy(streamHolder, fpg); if (clientNode == null) { logger.info("Permission Denied. Federate/CA Group not found in the policy graph: " + streamHolder.getFederateIdentity()); clientStream.onError(new StatusRuntimeException(Status.PERMISSION_DENIED)); @@ -1183,7 +1326,7 @@ public int compare(FederatedEvent a, FederatedEvent b) { } } } - } catch (SSLPeerUnverifiedException | CertificateEncodingException e) { + } catch (Exception e) { throw new RuntimeException("Error obtaining federate client certficate", e); } @@ -1196,8 +1339,6 @@ public int compare(FederatedEvent a, FederatedEvent b) { * we keep re-adding the same client trying to connect with * a new session id. */ - String sessionId = new BigInteger(session.getId()).toString(); - HubConnectionInfo info = new HubConnectionInfo(); info.setConnectionId(streamHolder.getFederateIdentity().getFedId()); info.setRemoteServerId(subscription.getIdentity().getServerId()); @@ -1216,9 +1357,12 @@ public int compare(FederatedEvent a, FederatedEvent b) { hubConnectionStore.addConnectionInfo(sessionId, info); hubConnectionStore.addClientStreamHolder(sessionId, streamHolder); - + // send a dummy message to make sure things are initialized - streamHolder.send(FederatedEvent.newBuilder().build()); + FederatedEvent.Builder eventBuilder = FederatedEvent.newBuilder(); + FederateGroups groups = getFederationHubGroups(streamHolder.getFederateIdentity().getFedId()); + groups.getFederateGroupsList().forEach(group -> eventBuilder.addFederateGroups(group)); + streamHolder.send(eventBuilder.build()); // if groups for this connection exist, send them here as well incase it failed if (hubConnectionStore.getClientToGroupsMap().get(sessionId) != null) { @@ -1252,35 +1396,40 @@ public void clientROLStream(Subscription subscription, StreamObserver clien } try { - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - Certificate[] clientCertArray = requireNonNull(requireNonNull(session, "SSL Session") - .getPeerCertificates(), "SSL peer certs array"); + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); - if (clientCertArray.length == 0) { - throw new IllegalArgumentException("Client certificate not available"); - } - - String fedCertHash = FederationUtils.getBytesSHA256(clientCertArray[0].getEncoded()); + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identification not available"); + } if (logger.isDebugEnabled()) { - logger.debug("Certificate hash of federate sending ROL: {}, clientName: {}", fedCertHash, clientName); + logger.debug("Certificate hash of federate sending ROL: {}, clientName: {}", clientFingerprint, clientName); } GuardedStreamHolder rolStreamHolder = new GuardedStreamHolder<>(clientStream, - clientName, fedCertHash, session, subscription, new Comparator() { + clientName, clientFingerprint, sessionId, subscription, new Comparator() { @Override public int compare(ROL a, ROL b) { return ComparisonChain.start().compare(a.hashCode(), b.hashCode()).result(); } }, true ); + + rolStreamHolder.setClientFingerprint(clientFingerprint); + rolStreamHolder.setClientGroups(clientGroups); - addFederateToGroupPolicyIfMissingV2(session, rolStreamHolder); + addFederateToGroupPolicyIfMissingV2(rolStreamHolder); FederationPolicyGraph fpg = getFederationPolicyGraph(); requireNonNull(fpg, "federation policy graph object"); - Federate clientNode = checkFederateExistsInPolicy(rolStreamHolder, session, fpg); + Federate clientNode = checkFederateExistsInPolicy(rolStreamHolder, fpg); if (clientNode == null) { clientStream.onError(new StatusRuntimeException(Status.PERMISSION_DENIED)); return; @@ -1290,14 +1439,13 @@ public int compare(ROL a, ROL b) { // down the line for getting the session id FederationHubMissionDisruptionManager.OfflineMissionChanges changes = null; - if (fedHubConfig.isMissionFederationDisruptionEnabled()) { + if (fedHubConfigManager.getConfig().isMissionFederationDisruptionEnabled()) { changes = federationHubMissionDisruptionManager.getMissionChangesAndTrackConnectEvent( - rolStreamHolder.getFederateIdentity().getFedId(), session.getPeerCertificates()); + rolStreamHolder.getFederateIdentity().getFedId(), rolStreamHolder.getClientGroups()); } /* Keep track of client stream and its associated federate identity. */ - String id = new BigInteger(session.getId()).toString(); - hubConnectionStore.addRolStream(id, rolStreamHolder); + hubConnectionStore.addRolStream(sessionId, rolStreamHolder); AtomicLong delayMs = new AtomicLong(0L); if (changes != null) { @@ -1317,8 +1465,8 @@ public int compare(ROL a, ROL b) { logger.info("Client ROL stream added. Count: " + broker.hubConnectionStore.getClientROLStreamMap().size()); - } catch (SSLPeerUnverifiedException | CertificateEncodingException e) { - throw new RuntimeException("Error obtaining federate client certificate", e); + } catch (Exception e) { + throw new RuntimeException("Error clientROLStream", e); } } @@ -1335,10 +1483,19 @@ public void onNext(ROL clientROL) { requireNonNull(clientROL, "ROL message from client"); requireNonNull(clientROL.getProgram(), "ROL program from client"); - SSLSession session = (SSLSession) sslSessionKey.get(Context.current()); + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); - String sessionId = new BigInteger(session.getId()).toString(); - addFederateToGroupPolicyIfMissingV2(session, hubConnectionStore.getClientStreamMap().get(sessionId)); + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identification not available"); + } + + addFederateToGroupPolicyIfMissingV2(hubConnectionStore.getClientStreamMap().get(sessionId)); GuardedStreamHolder streamHolder = hubConnectionStore.getClientROLStreamMap().get(sessionId); // sometimes rol comes in before we're completely ready. @@ -1356,9 +1513,8 @@ public void onNext(ROL clientROL) { @Override public void onError(Throwable t) { Status status = Status.fromThrowable(t); - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - String id = new BigInteger(session.getId()).toString(); - hubConnectionStore.clearIdFromAllStores(id); + String sessionId = sslSessionIdKey.get(Context.current()); + hubConnectionStore.clearIdFromAllStores(sessionId); responseObserver.onError(t); } @@ -1373,12 +1529,21 @@ public void onCompleted() { @Override public StreamObserver serverEventStream(StreamObserver responseObserver) { - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - String id = new BigInteger(session.getId()).toString(); + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client indentification not available"); + } + Subscription subscription = Subscription.newBuilder() .setFilter("") - .setIdentity(Identity.newBuilder().setServerId(fedHubConfig.getFullId()).setType(Identity.ConnectionType.FEDERATION_HUB_SERVER).setName(id).setUid(id).build()) + .setIdentity(Identity.newBuilder().setServerId(fedHubConfigManager.getConfig().getFullId()).setType(Identity.ConnectionType.FEDERATION_HUB_SERVER).setName(sessionId).setUid(sessionId).build()) .build(); responseObserver.onNext(subscription); @@ -1391,12 +1556,12 @@ public void onNext(FederatedEvent fe) { clientByteAccumulator.addAndGet(fe.getSerializedSize()); // Add federate to group in case policy was updated during connection - GuardedStreamHolder holder = hubConnectionStore.getClientStreamMap().get(id); + GuardedStreamHolder holder = hubConnectionStore.getClientStreamMap().get(sessionId); if (holder != null) { - addFederateToGroupPolicyIfMissingV2(session, hubConnectionStore.getClientStreamMap().get(id)); + addFederateToGroupPolicyIfMissingV2(hubConnectionStore.getClientStreamMap().get(sessionId)); } // submit to orchestrator - FederationHubBrokerService.this.handleRead(fe, new BigInteger(session.getId()).toString()); + FederationHubBrokerService.this.handleRead(fe, sessionId); } @Override @@ -1411,7 +1576,7 @@ public void onError(Throwable t) { } else { logger.error("Exception in server event stream call", t); } - hubConnectionStore.clearIdFromAllStores(id); + hubConnectionStore.clearIdFromAllStores(sessionId); responseObserver.onError(t); } @@ -1428,10 +1593,19 @@ public void onCompleted() { @Override public void healthCheck(ClientHealth request, StreamObserver responseObserver) { - if (fedHubConfig.isEnableHealthCheck()) { - SSLSession session = (SSLSession)sslSessionKey.get(Context.current()); - String sessionId = new BigInteger(session.getId()).toString(); + if (fedHubConfigManager.getConfig().isEnableHealthCheck()) { + String sessionId = sslSessionIdKey.get(Context.current()); + String clientFingerprint = clientFingerprintKey.get(Context.current()); + List clientGroups = clientGroupsKey.get(Context.current()); + if (sessionId == null) { + throw new IllegalArgumentException("SSL Session Id not available"); + } + + if (clientFingerprint == null || (clientGroups == null || clientGroups.isEmpty())) { + throw new IllegalArgumentException("Client identification not available"); + } + if (hubConnectionStore.getClientStreamMap().containsKey(sessionId)) { hubConnectionStore.getClientStreamMap().get(sessionId).updateClientHealth(request); responseObserver.onNext(serving); @@ -1458,7 +1632,7 @@ public void healthCheck(ClientHealth request, StreamObserver respo } } - public Federate checkFederateExistsInPolicy(GuardedStreamHolder streamHolder, SSLSession session, FederationPolicyGraph fpg) { + public Federate checkFederateExistsInPolicy(GuardedStreamHolder streamHolder, FederationPolicyGraph fpg) { Federate clientNode = null; try { String fedId = streamHolder.getFederateIdentity().getFedId(); @@ -1477,28 +1651,129 @@ public Federate checkFederateExistsInPolicy(GuardedStreamHolder streamHolder, return clientNode; } - final static private Context.Key sslSessionKey = Context.key("SSLSession"); + final static private Context.Key sslSessionIdKey = Context.key("SSLSessionId"); final static private Context.Key remoteAddressKey = Context.key("RemoteAddress"); + final static private Context.Key clientFingerprintKey = Context.key("oauthClientCert"); + final static private Context.Key> clientGroupsKey = Context.key("oauthCaCerts"); - public static ServerInterceptor tlsInterceptor() { - return new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, - final Metadata requestHeaders, - ServerCallHandler next) { + public ServerInterceptor tlsInterceptor() { + return new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall(ServerCall call, + final Metadata requestHeaders, ServerCallHandler next) { - SSLSession sslSession = call.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); - SocketAddress socketAddress = call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + try { + SSLSession sslSession = call.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); + SocketAddress socketAddress = call.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + Certificate[] certArray = sslSession.getPeerCertificates(); - Context context = Context.current() - .withValue(sslSessionKey, sslSession) - .withValue(remoteAddressKey, socketAddress); + String fingerprint = FederationUtils.getBytesSHA256(((X509Certificate) certArray[0]).getEncoded()); + + List clientGroups = FederationHubUtils.getCaGroupIdsFromCerts(certArray); - return Contexts.interceptCall(context, call, requestHeaders, next); - } - }; - } + Context context = Context.current() + .withValue(sslSessionIdKey, new BigInteger(sslSession.getId()).toString()) + .withValue(remoteAddressKey, socketAddress) + .withValue(clientFingerprintKey, fingerprint) + .withValue(clientGroupsKey, clientGroups); + + return Contexts.interceptCall(context, call, requestHeaders, next); + } catch (Exception e) { + call.close(Status.INTERNAL.withCause(e), new Metadata()); + return new ServerCall.Listener() { + // noop + }; + } + } + }; + } + + public ServerInterceptor oauthInterceptor() { + FedhubJwtUtils jwt = FedhubJwtUtils.getInstance(fedHubConfigManager.getConfig()); + return new ServerInterceptor() { + + @Override + public ServerCall.Listener interceptCall(ServerCall serverCall, + Metadata metadata, ServerCallHandler serverCallHandler) { + String value = metadata.get(TokenAuthCredential.AUTHORIZATION_METADATA_KEY); + + Status status = Status.OK; + String token = null; + if (value == null) { + status = Status.UNAUTHENTICATED.withDescription("Authorization token is missing"); + } else if (!value.startsWith(TokenAuthCredential.BEARER_TYPE)) { + status = Status.UNAUTHENTICATED.withDescription("Unknown authorization type"); + } else { + Claims claims = null; + // remove authorization type prefix + token = value.substring(TokenAuthCredential.BEARER_TYPE.length()).trim(); + try { + // verify token signature and parse claims + claims = jwt.parseClaim(token); + } catch (JwtException e) { + status = Status.UNAUTHENTICATED.withDescription(e.getMessage()).withCause(e); + } + if (claims != null) { + SSLSession sslSession = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_SSL_SESSION); + String sessionId = new BigInteger(sslSession.getId()).toString(); + SocketAddress socketAddress = serverCall.getAttributes().get(Grpc.TRANSPORT_ATTR_REMOTE_ADDR); + + String clientFingerprint = (String) claims.get("clientFingerprint"); + List clientGroups = (List) claims.get("clientGroups"); + Set clientGroupsSet = new HashSet<>(clientGroups); + + boolean activeToken = false; + + Collection cells = getFederationPolicyCells(); + for (PolicyObjectCell cell: cells) { + if (cell instanceof FederationTokenGroupCell) { + FederationTokenGroupCell tokenGroup = (FederationTokenGroupCell) cell; + + if (clientGroupsSet.contains(tokenGroup.getProperties().getName())) { + for(TokenNode tokenNode: tokenGroup.getProperties().getTokens()) { + if (tokenNode.getToken().equals(token)) { + activeToken = true; + } + } + } + } + } + + Context ctx = Context.current() + .withValue(sslSessionIdKey, sessionId) + .withValue(remoteAddressKey, socketAddress) + .withValue(clientFingerprintKey, clientFingerprint) + .withValue(clientGroupsKey, clientGroups); + + if (!activeToken) { + String errMsg = "Token is valid but not part of the active policy!"; + GuardedStreamHolder eventHolder = hubConnectionStore.getClientStreamMap().get(sessionId); + if (eventHolder != null) { + eventHolder.cancel(errMsg, new Exception(errMsg)); + } + GuardedStreamHolder groupHolder = hubConnectionStore.getClientGroupStreamMap().get(sessionId); + if (groupHolder != null) { + groupHolder.cancel(errMsg, new Exception(errMsg)); + } + GuardedStreamHolder rolHolder = hubConnectionStore.getClientROLStreamMap().get(sessionId); + if (rolHolder != null) { + rolHolder.cancel(errMsg, new Exception(errMsg)); + } + + status = Status.UNAUTHENTICATED.withDescription(errMsg); + serverCall.close(status, new Metadata()); + return new ServerCall.Listener() {}; + } else { + return Contexts.interceptCall(ctx, serverCall, metadata, serverCallHandler); + } + } + } + logger.error("CLOSE"); + serverCall.close(status, new Metadata()); + return new ServerCall.Listener() {}; + } + }; + } private SocketAddress getCurrentSocketAddress() { return remoteAddressKey.get(Context.current()); @@ -1508,6 +1783,7 @@ private SocketAddress getCurrentSocketAddress() { public void assignMessageSourceAndDestinationsFromPolicy(Message message, FederateIdentity federateIdentity, FederationPolicyGraph policyGraph) throws FederationException { + message.setSource(new AddressableEntity(federateIdentity)); Set destinationNodes = policyGraph.allReachableFederates(federateIdentity.getFedId()).federates; destinationNodes.stream().forEach(node -> @@ -1801,7 +2077,7 @@ private void deliverGroup(Message message, FederateIdentity src, FederateIdentit "federated group message payload"); if (logger.isTraceEnabled()) { logger.trace("Sending message {} from {} to {}", message.toString(), src, dest); - } + } try { entry.getValue().send(event); @@ -1988,7 +2264,7 @@ private boolean hasAlreadySeenMessage(Object event) { .map(prov -> prov.getFederationServerId()) .collect(Collectors.toSet()); - return visitedHubs.contains(fedHubConfig.getFullId()); + return visitedHubs.contains(fedHubConfigManager.getConfig().getFullId()); } catch (Exception e) { return false; } @@ -1999,7 +2275,6 @@ public void addFederateGroups(String sourceId, FederateGroups groups) { if (logger.isDebugEnabled()) { logger.debug("Stopping circular event " + groups); } - return; } @@ -2034,7 +2309,7 @@ public void parseRol(ROL clientROL, String streamKey) { handleRead(clientROL, streamKey); // no need to process rol any further if mfd is disabled - if (!fedHubConfig.isMissionFederationDisruptionEnabled()) + if (!fedHubConfigManager.getConfig().isMissionFederationDisruptionEnabled()) return; try { diff --git a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubMissionDisruptionManager.java b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubMissionDisruptionManager.java index bdb7c9c4..c9e9e5cf 100644 --- a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubMissionDisruptionManager.java +++ b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubMissionDisruptionManager.java @@ -52,18 +52,13 @@ private Collection getReceivableFederates(String sourceServerId) throws for(Document document : federationHubDatabaseService.getFederateMetadatas()) { String federateId = document.getString("federate_id"); - List certs = (List) document.get("cert_array"); + List clientGroups = (List) document.get("client_groups"); CertificateFactory cf = CertificateFactory.getInstance("X.509"); - for (Binary cert : certs) { - ByteArrayInputStream bais = new ByteArrayInputStream(cert.getData()); - X509Certificate x509Cert = (X509Certificate) cf.generateCertificate(bais); - - String issuerDN = x509Cert.getIssuerX500Principal().getName(); - String group = issuerDN + "-" + FederationUtils.getBytesSHA256(x509Cert.getEncoded()); + for (String clientGroup : clientGroups) { Federate federate = new Federate(new FederateIdentity(federateId)); - federate.addGroupIdentity(new FederateIdentity(group)); + federate.addGroupIdentity(new FederateIdentity(clientGroup)); List federateGroups = new ArrayList<>(); - federateGroups.add(group); + federateGroups.add(clientGroup); FederationHubDependencyInjectionProxy.getInstance().fedHubPolicyManager().addCaFederate(federate, federateGroups); } @@ -80,7 +75,7 @@ private Collection getReceivableFederates(String sourceServerId) throws } @SuppressWarnings("unchecked") - public OfflineMissionChanges getMissionChangesAndTrackConnectEvent(String federateServerId, Certificate[] certificates) { + public OfflineMissionChanges getMissionChangesAndTrackConnectEvent(String federateServerId, List clientGroups) { OfflineMissionChanges changes = new OfflineMissionChanges(); try { @@ -94,7 +89,7 @@ public OfflineMissionChanges getMissionChangesAndTrackConnectEvent(String federa Date now = new Date(); - long recencySecs = FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfig().getMissionFederationRecencySeconds(); + long recencySecs = FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().getConfig().getMissionFederationRecencySeconds(); long maxRecencyMillis; if (recencySecs == -1) { maxRecencyMillis = lastUpdate.getTime(); @@ -157,7 +152,7 @@ public OfflineMissionChanges getMissionChangesAndTrackConnectEvent(String federa } } - federationHubDatabaseService.addFederateMetadata(federateServerId, certificates); + federationHubDatabaseService.addFederateMetadata(federateServerId, clientGroups); } catch (Exception e) { logger.error("getMissionChangesAndTrackConnectEvent error", e); } diff --git a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubServer.java b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubServer.java index 66d3159e..230a9746 100644 --- a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubServer.java +++ b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/FederationHubServer.java @@ -23,7 +23,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; -import org.springframework.data.mongodb.core.MongoTemplate; import com.fasterxml.jackson.core.JsonParseException; import com.fasterxml.jackson.databind.JsonMappingException; @@ -91,8 +90,9 @@ public FederationHubBroker federationHubBroker(Ignite ignite) { FederationHubBrokerImpl hb = new FederationHubBrokerImpl(); ClusterGroup cg = ignite.cluster().forAttribute(FederationHubConstants.FEDERATION_HUB_IGNITE_PROFILE_KEY, FederationHubConstants.FEDERATION_HUB_BROKER_IGNITE_PROFILE); - ignite.services(cg).deployClusterSingleton(FederationHubConstants.FED_HUB_BROKER_SERVICE, hb); - + + ignite.services(cg).deployNodeSingleton(FederationHubConstants.FED_HUB_BROKER_SERVICE, hb); + return hb; } @@ -122,24 +122,18 @@ public FederationHubBrokerMetrics federationHubBrokerMetrics() { } @Bean - public FederationHubServerConfig getFedHubConfig() throws JsonParseException, JsonMappingException, IOException { - FederationHubServerConfig config = loadConfig(configFile); - if (Strings.isNullOrEmpty(config.getId())) { - config.setId(UUID.randomUUID().toString().replace("-", "")); - saveConfig(configFile, config); - } - - return loadConfig(configFile); + public FederationHubServerConfigManager getFedHubConfig() throws JsonParseException, JsonMappingException, IOException { + return new FederationHubServerConfigManager(configFile); } @Bean @Order(Ordered.LOWEST_PRECEDENCE) public FederationHubBrokerService FederationHubBrokerService(Ignite ignite, SSLConfig getSslConfig, - FederationHubServerConfig fedHubConfig, FederationHubPolicyManager fedHubPolicyManager, + FederationHubServerConfigManager fedHubConfigManager, FederationHubPolicyManager fedHubPolicyManager, HubConnectionStore hubConnectionStore, FederationHubMissionDisruptionManager federationHubMissionDisruptionManager, FederationHubBrokerMetrics fedHubBrokerMetrics) { - return new FederationHubBrokerService(ignite, getSslConfig, fedHubConfig, + return new FederationHubBrokerService(ignite, getSslConfig, fedHubConfigManager, fedHubPolicyManager, hubConnectionStore, federationHubMissionDisruptionManager, fedHubBrokerMetrics); } @@ -169,33 +163,14 @@ public FederationHubDatabaseService HubDataBaseService(FederationHubDatabase fed } @Bean - public FederationHubDatabase federationHubDatabase(FederationHubServerConfig fedHubConfig) { - return new FederationHubDatabase(fedHubConfig.getDbUsername(), fedHubConfig.getDbPassword(), - fedHubConfig.getDbHost(), fedHubConfig.getDbPort()); - } - - @Bean - public MongoTemplate mongoTemplate(FederationHubDatabase hubDatabase) throws Exception { - return new MongoTemplate(hubDatabase.getClient(), "cot"); - } - - private FederationHubServerConfig loadConfig(String configFile) - throws JsonParseException, JsonMappingException, FileNotFoundException, IOException { - if (getClass().getResource(configFile) != null) { - // It's a resource. - return new ObjectMapper(new YAMLFactory()).readValue(getClass().getResourceAsStream(configFile), - FederationHubServerConfig.class); + public FederationHubDatabase federationHubDatabase(FederationHubServerConfigManager fedHubConfigManager) { + FederationHubServerConfig fedHubConfig = fedHubConfigManager.getConfig(); + + if (fedHubConfig.isMissionFederationDisruptionEnabled()) { + return new FederationHubDatabase(fedHubConfig.getDbUsername(), fedHubConfig.getDbPassword(), + fedHubConfig.getDbHost(), fedHubConfig.getDbPort()); + } else { + return new FederationHubDatabase(); } - - // It's a file. - return new ObjectMapper(new YAMLFactory()).readValue(new FileInputStream(configFile), - FederationHubServerConfig.class); - } - - private void saveConfig(String configFile, FederationHubServerConfig config) - throws JsonParseException, JsonMappingException, FileNotFoundException, IOException { - - ObjectMapper om = new ObjectMapper(new YAMLFactory()); - om.writeValue(new File(configFile), config); } } \ No newline at end of file diff --git a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/HubFigClient.java b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/HubFigClient.java index 2313406d..2d6c75a0 100644 --- a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/HubFigClient.java +++ b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/HubFigClient.java @@ -18,6 +18,7 @@ import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +import org.apache.ignite.internal.processors.platform.client.cache.ClientCacheSqlFieldsQueryRequest; import org.bson.types.ObjectId; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -61,6 +62,7 @@ import tak.server.federation.GuardedStreamHolder; import tak.server.federation.hub.FederationHubDependencyInjectionProxy; import tak.server.federation.hub.FederationHubResources; +import tak.server.federation.hub.FederationHubUtils; import tak.server.federation.hub.broker.db.FederationHubMissionDisruptionManager; import tak.server.federation.hub.broker.events.HubClientDisconnectEvent; import tak.server.federation.hub.ui.graph.FederationOutgoingCell; @@ -76,10 +78,13 @@ public class HubFigClient implements Serializable { private static final Logger logger = LoggerFactory.getLogger(HubFigClient.class); - private FederationHubServerConfig fedHubConfig; + private FederationHubServerConfigManager fedHubConfigManager; + private String host; private int port; private X509Certificate[] sessionCerts; + private String clientFingerprint; + private List clientGroups; private String fedName; private String clientUid = UUID.randomUUID().toString().replace("-", ""); @@ -108,8 +113,8 @@ public class HubFigClient implements Serializable { private FederationHubMissionDisruptionManager federationHubMissionDisruptionManager; - public HubFigClient(FederationHubServerConfig fedHubConfig, FederationHubMissionDisruptionManager federationHubMissionDisruptionManager, FederationOutgoingCell federationOutgoingCell) { - this.fedHubConfig = fedHubConfig; + public HubFigClient(FederationHubServerConfigManager fedHubConfigManager, FederationHubMissionDisruptionManager federationHubMissionDisruptionManager, FederationOutgoingCell federationOutgoingCell) { + this.fedHubConfigManager = fedHubConfigManager; this.federationHubMissionDisruptionManager = federationHubMissionDisruptionManager; this.host = federationOutgoingCell.getProperties().getHost(); this.port = federationOutgoingCell.getProperties().getPort(); @@ -126,7 +131,7 @@ public FederatedChannelStub getAsyncFederatedChannel() { } public void start() throws Exception { - sslConfig.initSslContext(fedHubConfig); + sslConfig.initSslContext(fedHubConfigManager.getConfig()); channel = openFigConnection(host, port, GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.OPENSSL) @@ -142,7 +147,7 @@ public void start() throws Exception { // Send a subscription request, get back a stream of messages from server asyncFederatedChannel.clientEventStream(Subscription.newBuilder().setFilter("") - .setIdentity(Identity.newBuilder().setType(Identity.ConnectionType.FEDERATION_HUB_CLIENT).setServerId(fedHubConfig.getFullId()).setName(fedName).setUid(clientUid).build()).build(), + .setIdentity(Identity.newBuilder().setType(Identity.ConnectionType.FEDERATION_HUB_CLIENT).setServerId(fedHubConfigManager.getConfig().getFullId()).setName(fedName).setUid(clientUid).build()).build(), new StreamObserver() { @Override @@ -264,7 +269,7 @@ private ManagedChannel openFigConnection(final String host, final int port, SslC return NettyChannelBuilder.forAddress(host, port) .negotiationType(NegotiationType.TLS) .sslContext(sslContext) - .maxInboundMessageSize(fedHubConfig.getMaxMessageSizeBytes()) + .maxInboundMessageSize(fedHubConfigManager.getConfig().getMaxMessageSizeBytes()) .channelType(Epoll.isAvailable() ? EpollSocketChannel.class : NioSocketChannel.class) .executor(FederationHubResources.federationGrpcExecutor) .eventLoopGroup(FederationHubResources.federationGrpcWorkerEventLoopGroup) @@ -275,18 +280,19 @@ public X509Certificate[] propogate(X509Certificate[] certs) { sessionCerts = certs; X509Certificate clientCert = certs[0]; X509Certificate caCert = certs[1]; - - String fingerprint = FederationUtils.getBytesSHA256(clientCert.getEncoded()); - String issuerDN = clientCert.getIssuerX500Principal().getName(); - String issuerCN = Optional.ofNullable(FederationHubBrokerImpl.getCN(issuerDN)).map(cn -> cn.toLowerCase()).orElse(""); - if (fedHubConfig.isUseCaGroups()) { + clientFingerprint = FederationUtils.getBytesSHA256(((X509Certificate)clientCert).getEncoded()); + clientGroups = FederationHubUtils.getCaGroupIdsFromCerts(certs); + + if (fedHubConfigManager.getConfig().isUseCaGroups()) { try { hubClientFederate = new Federate(new FederateIdentity(fedName)); - hubClientFederate.addGroupIdentity(new FederateIdentity(issuerDN + "-" + FederationUtils.getBytesSHA256(caCert.getEncoded()))); - String group = issuerDN + "-" + FederationUtils.getBytesSHA256(caCert.getEncoded()); - hubClientGroups = new ArrayList<>(); - hubClientGroups.add(group); + + for (String clientGroup: clientGroups) { + hubClientFederate.addGroupIdentity(new FederateIdentity(clientGroup)); + hubClientGroups.add(clientGroup); + } + FederationHubDependencyInjectionProxy.getInstance().fedHubPolicyManager().addCaFederate(hubClientFederate, hubClientGroups); } catch (Exception e) { logger.error("error updating federate node", e); @@ -354,6 +360,10 @@ public int compare(FederateGroups a, FederateGroups b) { } }, true ); + + groupStreamHolder.setClientFingerprint(clientFingerprint); + groupStreamHolder.setClientGroups(clientGroups); + FederationHubDependencyInjectionProxy.getInstance().hubConnectionStore().addGroupStream(fedName, groupStreamHolder); } @@ -427,6 +437,9 @@ public int compare(FederatedEvent a, FederatedEvent b) { }, true ); + eventStreamHolder.setClientFingerprint(clientFingerprint); + eventStreamHolder.setClientGroups(clientGroups); + setServerSubscriptionForConnection(); eventStreamHolder.send(FederatedEvent.newBuilder().build()); @@ -503,12 +516,15 @@ public int compare(ROL a, ROL b) { }, true ); + rolStreamHolder.setClientFingerprint(clientFingerprint); + rolStreamHolder.setClientGroups(clientGroups); + // get the changes, but don't send till we add the rolStream because the stream will get used // down the line for getting the session id FederationHubMissionDisruptionManager.OfflineMissionChanges changes = null; - if (fedHubConfig.isMissionFederationDisruptionEnabled()) { + if (fedHubConfigManager.getConfig().isMissionFederationDisruptionEnabled()) { changes = federationHubMissionDisruptionManager.getMissionChangesAndTrackConnectEvent( - rolStreamHolder.getFederateIdentity().getFedId(), sessionCerts); + rolStreamHolder.getFederateIdentity().getFedId(), clientGroups); } FederationHubDependencyInjectionProxy.getInstance().hubConnectionStore().addRolStream(fedName, rolStreamHolder); diff --git a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseService.java b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseService.java index dce96abb..6b33a334 100644 --- a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseService.java +++ b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseService.java @@ -1,6 +1,5 @@ package tak.server.federation.hub.broker.db; -import java.security.cert.Certificate; import java.util.Date; import java.util.List; import java.util.Map; @@ -12,7 +11,7 @@ public interface FederationHubDatabaseService { void storeRol(Document rol); - Document addFederateMetadata(String id, Certificate[] certificates); + Document addFederateMetadata(String id, List clientGroups); Document getFederateMetadata(String id); List getFederateMetadatas(); diff --git a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseServiceImpl.java b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseServiceImpl.java index 9e48cf11..a870668f 100644 --- a/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseServiceImpl.java +++ b/src/federation-hub-broker/src/main/java/tak/server/federation/hub/broker/db/FederationHubDatabaseServiceImpl.java @@ -2,7 +2,6 @@ import java.io.IOException; import java.io.InputStream; -import java.security.cert.Certificate; import java.time.Instant; import java.util.ArrayList; import java.util.Date; @@ -60,7 +59,7 @@ public FederationHubDatabaseServiceImpl(FederationHubDatabase federationHubDatab if (!isDBConnected()) return; - long retentionDays = FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfig().getMissionFederationDBRetentionDays(); + long retentionDays = FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().getConfig().getMissionFederationDBRetentionDays(); // delete expired events based on received_time Bson receivedTimeFilter = Filters.lt("received_time", new Date(Instant.now().toEpochMilli() - retentionDays*24*60*60 * 1000)); @@ -166,7 +165,7 @@ public byte[] getResource(ObjectId resourceObjectId) { try (GridFSDownloadStream downloadStream = resourceCollection().openDownloadStream(resourceObjectId)) { int fileLength = (int) downloadStream.getGridFSFile().getLength(); - long maxSizeBytes = FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfig().getMissionFederationDisruptionMaxFileSizeBytes(); + long maxSizeBytes = FederationHubDependencyInjectionProxy.getInstance().fedHubServerConfigManager().getConfig().getMissionFederationDisruptionMaxFileSizeBytes(); if (fileLength > maxSizeBytes) { logger.info("File size of " + fileLength + " exceeds config limit of " + maxSizeBytes + "MB. Skipping " + downloadStream.getGridFSFile().getMetadata()); return null; @@ -215,23 +214,15 @@ public List getFederateMetadatas() { @Override @CachePut(value = "federate_metadata", key = "{#root.args[0]}") - public Document addFederateMetadata(String id, Certificate[] certificates) { + public Document addFederateMetadata(String id, List clientGroups) { if (!isDBConnected()) return null; try { - List binaryCerts = new ArrayList<>(); - for (int i = 1; i < certificates.length; i++) { - if (certificates[i] == null) { - break; - } - binaryCerts.add(certificates[i].getEncoded()); - } - Bson filter = Filters.eq("federate_id", id); Bson update = Updates.combine( Updates.set("federate_id", id), - Updates.set("cert_array", binaryCerts), + Updates.set("client_groups", clientGroups), Updates.set("last_update", Instant.now())); UpdateOptions options = new UpdateOptions().upsert(true); diff --git a/src/federation-hub-policy/scripts/federation-hub-policy b/src/federation-hub-policy/scripts/federation-hub-policy index de0d692e..45a8bd82 100755 --- a/src/federation-hub-policy/scripts/federation-hub-policy +++ b/src/federation-hub-policy/scripts/federation-hub-policy @@ -31,7 +31,7 @@ FEDERATION_HUB_HOME=/opt/tak/federation-hub case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${FEDERATION_HUB_HOME}/scripts && ./federation-hub-policy.sh &> /opt/tak/federation-hub/logs/federation-hub-policy-console.log &" + su tak -c "cd ${FEDERATION_HUB_HOME}/scripts && ./federation-hub-policy.sh > /opt/tak/federation-hub/logs/federation-hub-policy-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/federation-hub-policy/src/main/java/tak/server/federation/hub/policy/FederationHubPolicyManagerService.java b/src/federation-hub-policy/src/main/java/tak/server/federation/hub/policy/FederationHubPolicyManagerService.java index d3aded3e..519a7fcb 100644 --- a/src/federation-hub-policy/src/main/java/tak/server/federation/hub/policy/FederationHubPolicyManagerService.java +++ b/src/federation-hub-policy/src/main/java/tak/server/federation/hub/policy/FederationHubPolicyManagerService.java @@ -56,7 +56,7 @@ public void run(String... args) throws Exception { ClusterGroup cg = ignite.cluster().forAttribute( FederationHubConstants.FEDERATION_HUB_IGNITE_PROFILE_KEY, FederationHubConstants.FEDERATION_HUB_POLICY_IGNITE_PROFILE); - ignite.services(cg).deployClusterSingleton( + ignite.services(cg).deployNodeSingleton( FederationHubConstants.FED_HUB_POLICY_MANAGER_SERVICE, hpm); } diff --git a/src/federation-hub-ui/build.gradle b/src/federation-hub-ui/build.gradle index 604b54fb..0d96f99b 100644 --- a/src/federation-hub-ui/build.gradle +++ b/src/federation-hub-ui/build.gradle @@ -85,7 +85,7 @@ dependencies { implementation group: 'com.h2database', name: 'h2', version: h2_version - implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1' + implementation group: 'io.jsonwebtoken', name: 'jjwt', version: jsonwebtoken_version implementation group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.70' diff --git a/src/federation-hub-ui/scripts/federation-hub-ui b/src/federation-hub-ui/scripts/federation-hub-ui index 3728fb29..e2a17bdf 100755 --- a/src/federation-hub-ui/scripts/federation-hub-ui +++ b/src/federation-hub-ui/scripts/federation-hub-ui @@ -31,7 +31,7 @@ FEDERATION_HUB_HOME=/opt/tak/federation-hub case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${FEDERATION_HUB_HOME}/scripts && ./federation-hub-ui.sh &> /opt/tak/federation-hub/logs/federation-hub-ui-console.log &" + su tak -c "cd ${FEDERATION_HUB_HOME}/scripts && ./federation-hub-ui.sh > /opt/tak/federation-hub/logs/federation-hub-ui-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/FederationHubUIService.java b/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/FederationHubUIService.java index 84436609..58f6e2b8 100644 --- a/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/FederationHubUIService.java +++ b/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/FederationHubUIService.java @@ -1,5 +1,6 @@ package tak.server.federation.hub.ui; +import java.io.File; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.security.MessageDigest; @@ -15,6 +16,7 @@ import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import org.owasp.esapi.Validator; import org.slf4j.Logger; @@ -62,8 +64,11 @@ import tak.server.federation.FederateGroup; import tak.server.federation.FederationException; import tak.server.federation.FederationPolicyGraph; +import tak.server.federation.hub.FederationHubUtils; +import tak.server.federation.hub.FedhubJwtUtils; import tak.server.federation.hub.broker.FederationHubBroker; import tak.server.federation.hub.broker.FederationHubBrokerMetrics; +import tak.server.federation.hub.broker.FederationHubServerConfig; import tak.server.federation.hub.broker.HubConnectionInfo; import tak.server.federation.hub.policy.FederationHubPolicyManager; import tak.server.federation.hub.policy.FederationPolicy; @@ -71,8 +76,11 @@ import tak.server.federation.hub.ui.graph.FederateCell; import tak.server.federation.hub.ui.graph.FederationOutgoingCell; import tak.server.federation.hub.ui.graph.FederationPolicyModel; +import tak.server.federation.hub.ui.graph.FederationTokenGroupCell; import tak.server.federation.hub.ui.graph.FilterUtils; import tak.server.federation.hub.ui.graph.GroupCell; +import tak.server.federation.hub.ui.graph.JwtTokenRequestModel; +import tak.server.federation.hub.ui.graph.JwtTokenResponseModel; import tak.server.federation.hub.ui.jwt.AuthRequest; import tak.server.federation.hub.ui.jwt.AuthResponse; import tak.server.federation.hub.ui.keycloak.AuthCookieUtils; @@ -249,6 +257,32 @@ public ResponseEntity login(@RequestBody AuthRequest request) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); } } + + @RequestMapping(value = "/fig/generateJwtToken", method = RequestMethod.POST) + public ResponseEntity generateJwtToken(@RequestBody JwtTokenRequestModel tokenRequest) { + try { + String token = FedhubJwtUtils.getInstance(fedHubConfig).createToken(tokenRequest.getClientFingerprint(), + tokenRequest.getClientGroup(), tokenRequest.getExpiration()); + + JwtTokenResponseModel response = new JwtTokenResponseModel(); + response.setToken(token); + + return new ResponseEntity<>(response, new HttpHeaders(), HttpStatus.OK); + } catch (Exception e) { + logger.error("error with generateJwtToken", e); + return new ResponseEntity<>(new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @RequestMapping(value="/fig/allowFlowIndicators", method=RequestMethod.GET) + public ResponseEntity isAllowFlowIndicators() { + try { + return new ResponseEntity<>(fedHubConfig.isEnableFlowIndicators() ,new HttpHeaders(), HttpStatus.OK); + } catch (Exception e) { + logger.error("error with isAllowFlowIndicators", e); + return new ResponseEntity<>(new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } @RequestMapping(value = "/fig/saveFederation", method = RequestMethod.POST) @ResponseBody @@ -404,6 +438,10 @@ public ResponseEntity> getKnownGroupsForGraphNode(@PathVariable Str FederationOutgoingCell oCell = (FederationOutgoingCell) cell; groupsForNode.addAll(fedHubBroker.getGroupsForNode(oCell.getProperties().getName())); } + if (cell instanceof FederationTokenGroupCell) { + FederationTokenGroupCell tCell = (FederationTokenGroupCell) cell; + groupsForNode.addAll(fedHubBroker.getGroupsForNode(tCell.getProperties().getName())); + } } }); @@ -487,6 +525,77 @@ public ResponseEntity getSelfCaFile() { return new ResponseEntity(new HttpHeaders(), HttpStatus.BAD_REQUEST); } } + + @RequestMapping(value = "/fig/getBrokerConfig", method = RequestMethod.GET) + public ResponseEntity addFederateGroup() { + return new ResponseEntity(fedHubBroker.getFederationHubBrokerConfig(), new HttpHeaders(), HttpStatus.OK); + } + + @RequestMapping(value = "/fig/updateBrokerConfig", method = RequestMethod.POST) + public ResponseEntity updateBrokerConfig(@RequestBody FederationHubServerConfig brokerConfig) { + return new ResponseEntity(fedHubBroker.saveFederationHubServerConfig(brokerConfig), new HttpHeaders(), HttpStatus.OK); + } + + @RequestMapping(value="/fig/restartBroker", method=RequestMethod.GET) + public ResponseEntity serverRestart() { + try { + logger.info("Restarting Broker Service"); + + killBrokerProcess(); + + // start + String startCmd= "./federation-hub-broker.sh &> /opt/tak/federation-hub/logs/federation-hub-broker-console.log &"; + String[] startCmds = new String[] {"bash", "-c", startCmd}; + + ProcessBuilder startProcessBuilder = new ProcessBuilder(startCmds); + startProcessBuilder.directory(new File("/opt/tak/federation-hub/scripts")); + Process startProcess = startProcessBuilder.start(); + + startProcess.waitFor(); + + canAccessBrokerProcess().get(); + logger.info("Federation Hub Broker is available"); + + return new ResponseEntity<>(new HttpHeaders(), HttpStatus.OK); + } catch (Exception e) { + logger.error("error with restartBroker", e); + return new ResponseEntity<>(new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + private void killBrokerProcess() { + try { + String killCmd = "pkill -9 -f \"federation-hub-broker\""; + String[] killCmds = new String[] {"bash", "-c", killCmd}; + + ProcessBuilder killProcessBuilder = new ProcessBuilder(killCmds); + killProcessBuilder.directory(new File("/opt/tak/federation-hub/scripts")); + Process killProcess = killProcessBuilder.start(); + + killProcess.waitFor(); + } catch (Exception e) { + logger.error("error with killBrokerProcess", e); + } + } + + private boolean canAccessBrokerProcessServices() throws Exception { + fedHubBroker.getActiveConnections(); + return true; + } + + private CompletableFuture canAccessBrokerProcess() { + try { + logger.info("Waiting for the Broker process..."); + return CompletableFuture.completedFuture(canAccessBrokerProcessServices()); + } catch (Exception e) { + try { + Thread.sleep(1000L); + } catch (InterruptedException e1) { + logger.error("interruped sleep", e1); + } + return canAccessBrokerProcess(); + } + } private String sha256(String input) throws UnsupportedEncodingException, NoSuchAlgorithmException { byte[] bytes = input.getBytes("US-ASCII"); @@ -518,6 +627,14 @@ private FederationPolicyModel getActivePolicyAsPolicyModel() { } private List federateGroupsToGroupHolders(Collection groups) { + X509Certificate selfCa = null; + try { + byte[] selfCaB = fedHubBroker.getSelfCaFile(); + selfCa = FederationHubUtils.loadX509CertFromBytes(selfCaB); + } catch (Exception e) { + logger.error("error computing self ca", e); + } + Map cas = fedHubBroker.getCAsFromFile(); List groupList = new LinkedList<>(); for (FederateGroup group : groups) { @@ -529,6 +646,7 @@ private List federateGroupsToGroupHolders(Collection X509Certificate ca = cas.get(fedId); remoteGroup.setIssuer(ca.getIssuerX500Principal().getName()); remoteGroup.setSubject(ca.getSubjectX500Principal().getName()); + remoteGroup.setIsHubGroup(ca.equals(selfCa)); } groupList.add(remoteGroup); @@ -540,7 +658,8 @@ private class RemoteGroup { private String uid; private String issuer; private String subject; - + private boolean isHubGroup = false; + public String getUid() { return uid; } @@ -559,7 +678,11 @@ public String getSubject() { public void setSubject(String subject) { this.subject = subject; } - - + public boolean isHubGroup() { + return isHubGroup; + } + public void setIsHubGroup(boolean isHubGroup) { + this.isHubGroup = isHubGroup; + } } } \ No newline at end of file diff --git a/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/SecurityConfig.java b/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/SecurityConfig.java index 431b4b75..0e91b9ea 100644 --- a/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/SecurityConfig.java +++ b/src/federation-hub-ui/src/main/java/tak/server/federation/hub/ui/SecurityConfig.java @@ -3,6 +3,8 @@ import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.List; import javax.naming.InvalidNameException; @@ -25,7 +27,13 @@ import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken; +import org.springframework.security.web.util.matcher.AndRequestMatcher; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.security.web.util.matcher.RequestMatcher; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import tak.server.federation.hub.ui.jwt.UserService; import tak.server.federation.hub.ui.manage.AuthManager; @@ -44,6 +52,9 @@ public class SecurityConfig { @Autowired private AuthorizationFileWatcher authFileWatcher; + + @Autowired + private FederationHubUIConfig fedHubConfig; // @Override // protected void configure(AuthenticationManagerBuilder auth) throws Exception { @@ -59,6 +70,14 @@ public AuthenticationManager authenticationManager(UserService userService) { authenticationProvider.setUserDetailsService(userService); return new ProviderManager(authenticationProvider); } + + private RequestMatcher oauthPortMatch = new RequestMatcher() { + @Override + public boolean matches(HttpServletRequest request) { + return fedHubConfig.isAllowOauth() && fedHubConfig.getOauthPort() != null + && request.getLocalPort() == fedHubConfig.getOauthPort(); + } + }; @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { @@ -66,12 +85,10 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.csrf().disable(); http.x509().authenticationUserDetailsService(new X509AuthenticatedUserDetailsService()); - + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.ALWAYS); -// http.authorizeRequests().antMatchers("/error","/oauth/**", "/login**", "/login/**", "/bowerDependencies/**", "/favicon.ico").permitAll() -// .anyRequest().authenticated(); - http.authorizeHttpRequests().requestMatchers("/error","/oauth/**", "/login**", "/login/**", "/bowerDependencies/**", "/favicon.ico").permitAll() + http.authorizeHttpRequests().requestMatchers(getMatchers("/error","/oauth/**", "/login**", "/login/**", "/bowerDependencies/**", "/favicon.ico")).permitAll() .anyRequest().authenticated(); http.exceptionHandling().authenticationEntryPoint((request, response, ex) -> { @@ -102,4 +119,14 @@ public UserDetails loadUserDetails(PreAuthenticatedAuthenticationToken token) th throw new UsernameNotFoundException("Authorized user for certificate " + dn + " not found"); } } + + private RequestMatcher[] getMatchers(String... paths) { + List matchers = new ArrayList<>(); + for (String path: paths) { + RequestMatcher pathMatch = new AntPathRequestMatcher(path); + RequestMatcher oauthAndPortMatch = new AndRequestMatcher(oauthPortMatch, pathMatch); + matchers.add(oauthAndPortMatch); + } + return matchers.toArray(new RequestMatcher[0]); + } } \ No newline at end of file diff --git a/src/federation-hub-ui/src/main/resources/federation-hub-ui.yml b/src/federation-hub-ui/src/main/resources/federation-hub-ui.yml index c1afc908..b295e048 100644 --- a/src/federation-hub-ui/src/main/resources/federation-hub-ui.yml +++ b/src/federation-hub-ui/src/main/resources/federation-hub-ui.yml @@ -3,7 +3,7 @@ keystoreFile: /opt/tak/federation-hub/certs/files/takserver.jks keystorePassword: atakatak truststoreType: JKS -truststoreFile: /opt/tak/federation-hub/certs/files/fed-truststore.jks +truststoreFile: /opt/tak/federation-hub/certs/files/truststore-root.jks truststorePassword: atakatak keyAlias: takserver diff --git a/src/federation-hub-ui/src/main/webapp/home.html b/src/federation-hub-ui/src/main/webapp/home.html index be203706..d1695e52 100644 --- a/src/federation-hub-ui/src/main/webapp/home.html +++ b/src/federation-hub-ui/src/main/webapp/home.html @@ -148,6 +148,7 @@ + diff --git a/src/federation-hub-ui/src/main/webapp/modules/workflows/add_federation_token_group_controller.js b/src/federation-hub-ui/src/main/webapp/modules/workflows/add_federation_token_group_controller.js new file mode 100644 index 00000000..02918f1d --- /dev/null +++ b/src/federation-hub-ui/src/main/webapp/modules/workflows/add_federation_token_group_controller.js @@ -0,0 +1,122 @@ +/******************************************************************************* + * DISTRIBUTION C. Distribution authorized to U.S. Government agencies and their contractors. Other requests for this document shall be referred to the United States Air Force Research Laboratory. + * + * Copyright (c) 2019 Raytheon BBN Technologies. + *******************************************************************************/ +"use strict"; + +angular.module('roger_federation.Workflows') + .controller('AddFederationTokenGroupController', ['$rootScope', '$scope', '$http', '$stateParams', '$modalInstance', '$timeout', '$log', '$cookieStore', 'growl', 'WorkflowTemplate', 'WorkflowService', 'OntologyService', 'JointPaper', addFederationTokenGroupController]); + +function addFederationTokenGroupController($rootScope, $scope, $http, $stateParams, $modalInstance, $timeout, $log, $cookieStore, growl, WorkflowTemplate, WorkflowService, OntologyService, JointPaper) { + $scope.editorTitle = "Add"; + $scope.tokenNameExists = false; + $scope.editExisting = false; + $scope.roger_federation = undefined; + var cellView; + + $scope.initialize = function() { + cellView = JointPaper.inpector.options.cellView; + $scope.roger_federation = JSON.parse(JSON.stringify(cellView.model.attributes.roger_federation)); //Clone + if ($scope.roger_federation.name !== "") { + $scope.editorTitle = "Modify"; + $scope.editExisting = true; + } else { + $scope.roger_federation.tokens = [] + $scope.roger_federation.expiration = -1 + } + }; + + $scope.submit = function() { + let existingName = false; + + let cells = JointPaper.graph.toJSON().cells; + cells.forEach(cell => { + if (cell.graphType === 'FederationTokenGroupCell' && cell.id !== cellView.model.attributes.id) { + if (cell.roger_federation.stringId === $scope.roger_federation.stringId) + existingName = true + } + }) + + if (existingName) { + $scope.tokenNameExists = true; + } else { + $scope.tokenNameExists = false; + + $scope.roger_federation.name = $scope.roger_federation.stringId + '_token_group' + if (JointPaper.inpector !== undefined) { + //Construct Label + var shapeLabel = joint.util.breakText($scope.roger_federation.stringId, { + width: JointPaper.options.maxLabelWidth + }); + cellView.model.set('content', shapeLabel); + cellView.model.attributes.roger_federation = JSON.parse(JSON.stringify($scope.roger_federation)); + cellView.resize(); //Sometimes the label wraps when it shouldn't. This seems to fix it. + } + $modalInstance.close('ok'); + } + }; + + $scope.cancel = function() { + if (!$scope.editExisting && JointPaper.inpector !== undefined) { + JointPaper.inpector.options.cellView.model.remove(); + } + $modalInstance.dismiss('cancel'); + }; + + $scope.showActiveConnections = function() { + if (!$scope.editExisting && JointPaper.inpector !== undefined) { + JointPaper.inpector.options.cellView.model.remove(); + } + $rootScope.selectedCa = $scope.roger_federation.name + $state.go('workflows.editor.connections'); + } + + $scope.previewToken = function(token) { + return token.substring(0,8) + '...' + token.substring(token.length-8);; + } + + $scope.copyToken = function(token) { + navigator.clipboard.writeText(token) + growl.info("Token Copied to Clipboard!"); + } + + $scope.generateToken = function() { + if (!$scope.editExisting) { + growl.error("Please save this node to the graph before generating tokens."); + return + } + + if (!$scope.roger_federation.expiration || !$scope.roger_federation.name) { + growl.error("Cannot generate a token until a name and expiration are set!"); + return + } + + let now = Date.now(); + + let expiration = $scope.roger_federation.expiration === -1 ? -1 + : $scope.roger_federation.expiration + now + + WorkflowService.generateJwtToken({ + clientFingerprint: now +'_' + $scope.roger_federation.stringId + '_token_fingerprint', + clientGroup: $scope.roger_federation.name, + expiration: expiration + }).then(res => { + $scope.roger_federation.tokens.push({ + token: res.token, + expiration: expiration + }) + growl.info("Token generated! Don't forget to save the policy to keep your changes!"); + }).catch(err => { + growl.error("Error with token request", err); + }) + } + + $scope.getDisplayDate = function(milliseconds) { + return milliseconds === -1 ? -1 : new Date(milliseconds) + } + + $scope.deleteToken = function(token) { + $scope.roger_federation.tokens = $scope.roger_federation.tokens.filter(t => t !== token); + }; +} \ No newline at end of file diff --git a/src/federation-hub-ui/src/main/webapp/modules/workflows/ca_controller.js b/src/federation-hub-ui/src/main/webapp/modules/workflows/ca_controller.js index ebe120ad..90d09cc0 100644 --- a/src/federation-hub-ui/src/main/webapp/modules/workflows/ca_controller.js +++ b/src/federation-hub-ui/src/main/webapp/modules/workflows/ca_controller.js @@ -28,6 +28,10 @@ function caController($rootScope, $scope, $state, $http, $stateParams, $modalIns }; $scope.deleteGroupCa = function(uid) { + if (!confirm("Are you sure you want to delete this CA?")) { + return; + } + $scope.knownCas = $scope.knownCas.filter(ca => ca.uid !== uid); if (JointPaper.paper && JointPaper.paper._views) { var nodeKeys = Object.keys(JointPaper.paper._views); diff --git a/src/federation-hub-ui/src/main/webapp/modules/workflows/joint_paper_factory.js b/src/federation-hub-ui/src/main/webapp/modules/workflows/joint_paper_factory.js index d23917b9..29d99ad1 100644 --- a/src/federation-hub-ui/src/main/webapp/modules/workflows/joint_paper_factory.js +++ b/src/federation-hub-ui/src/main/webapp/modules/workflows/joint_paper_factory.js @@ -211,6 +211,7 @@ angular.module('roger_federation.Workflows') joint.shapes.bpmn.icons.dataStore = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/PjwhRE9DVFlQRSBzdmcgIFBVQkxJQyAnLS8vVzNDLy9EVEQgU1ZHIDEuMS8vRU4nICAnaHR0cDovL3d3dy53My5vcmcvR3JhcGhpY3MvU1ZHLzEuMS9EVEQvc3ZnMTEuZHRkJz48c3ZnIGVuYWJsZS1iYWNrZ3JvdW5kPSJuZXcgMCAwIDI0IDI0IiBoZWlnaHQ9IjI0cHgiIGlkPSJMYXllcl8xIiB2ZXJzaW9uPSIxLjEiIHZpZXdCb3g9IjAgMCAyNCAyNCIgd2lkdGg9IjI0cHgiIHhtbDpzcGFjZT0icHJlc2VydmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxwYXRoICBkPSJtIDAuOTA3NzQ0NTUsNC4wMTI4NTI1IDAsMTYuMjM1MDExNSBjIDAuNzQwODE4OTUsMy42MDc3ODEgMjEuNDgzNzUxNDUsMy42MDc3ODEgMjIuMjI0NTcwNDUsMCBsIDAsLTE2LjIzNTAxMTUgYyAtMC43NDA4MTksLTMuNjA3NzgwMTIgLTIxLjQ4Mzc1MTUsLTMuNjA3NzgwMTIgLTIyLjIyNDU3MDQ1LDAgMC43NDA4MTg5NSwzLjYwNzc4MDEgMjEuNDgzNzUxNDUsMy42MDc3ODAxIDIyLjIyNDU3MDQ1LDAgTSAwLjkwNzc0NDU1LDYuNTM4Mjk4OCBjIDAuNzQwODE4OTUsMy42MDc3ODAyIDIxLjQ4Mzc1MTQ1LDMuNjA3NzgwMiAyMi4yMjQ1NzA0NSwwIE0gMC45MDc3NDQ1NSw5LjA2Mzc0NDkgYyAwLjc0MDgxODk1LDMuNjA3NzgwMSAyMS40ODM3NTE0NSwzLjYwNzc4MDEgMjIuMjI0NTcwNDUsMCINCiAgICAgc3R5bGU9ImZpbGw6I2ZmZmZmZjtzdHJva2U6IzAwMDAwMDtzdHJva2Utd2lkdGg6MC44OTE5NzE0MSIgLz48L3N2Zz4="; joint.shapes.bpmn.icons.outgoing = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAgAAAAIACAYAAAD0eNT6AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAASdAAAEnQB3mYfeAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABMjSURBVHic7d07khxZcoZRD0yb0ahxB6NQ5I5a6CWwm5whuQO+2djCCLMoaqQ4ixjLUTJhBaAemekeEfdeP0dClVklXPs/VBYqtsvlEgBAL5/OPgAAOJ4AAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIZ+OPsAoI9t2/46Iv7q7DuAiO1yuZx9w5u2bfuXiPi7iPjpcrn8+ex7gJxt2z5HxN+ffQcw8HcAtm3754j41xcfiwAAKDLkzwBcx//fXnzqx4j4w7ZtvznpJABYynAB8Mr434gAACgyVAC8M/43IgAACgwTAHeM/40IAICkIQJg27Z/ivvG/0YEAEDC6QFwHf9/f+JLRQAAPOnUAEiM/40IAIAnnBYABeN/IwIA4EGnBEDh+N+IAAB4wOEBsG3b76N2/G9EAADc6dAAuI7/f+z4V4gAALjDYQFwwPjfiAAA+MAhAXDg+N+IAAB4x+4BcML434gAAHjDrgGwbdvv4pzxvxEBAPCK3QLgOv7/udfrP0AEAMA3dgmAgcb/RgQAwAvlATDg+N+IAAC4Kg2Agcf/RgQAQBQGwLZt/xhjj/+NCACgvZIAuI7/f1W81kFEAACtpQNgwvG/EQEAtJUKgInH/0YEANDS0wGwwPjfiAAA2nkqABYa/xsRAEArDwfAtm3/EGuN/40IAKCNhwLgOv7/vdMtIxABALRwdwA0GP8bEQDA8u4KgEbjfyMCAFjahwHQcPxvRAAAy3o3ALZt+yV6jv+NCABgSW8GwHX8/+fAW0YlAgBYzqsBYPy/IwIAWMp3AWD83yQCAFjGVwFg/D8kAgBYwpcA2Lbt5zD+9xABAEzvU8SX8f/15FtmIgIAmNon4/80EQDAtD5FxG/PPmJiIgCAKX26XC6/RMTnsw+ZmAgAYDqfIiJEQJoIAGAqX/4XgAhIEwEATOOr3wMgAtJEAABT+O43AYqANBEAwPBefRaACEgTAQAM7c2nAYqANBEAwLDeDIAIEVBABAAwpHcDIEIEFBABAAznwwCIEAEFRAAAQ7krACJEQAERAMAw7g6ACBFQQAQAMISHAiBCBBQQAQCc7uEAiBABBUQAAKd6KgAiREABEQDAaX7IfPHlcvll27aIiJ9rzmnnx4iIbdt+ulwufz77GDjAnyLif88+gtP9JiL+9uwjutsul0v+Rbbt1xABGX+MCBEALO/6Xc8/xPUfQJzn6bcAXvJ2QJq3A4DlGf+xlARAhAgoIAKAZRn/8ZQFQIQIKCACgOUY/zGVBkCECCggAoBlGP9xlQdAhAgoIAKA6Rn/se0SABEioIAIAKZl/Me3WwBEiIACIgCYjvGfw64BECECCogAYBrGfx67B0CECCggAoDhGf+5HBIAESKggAgAhmX853NYAESIgAIiABiO8Z/ToQEQIQIKiABgGMZ/XocHQIQIKCACgNMZ/7mdEgARIqCACABOY/znd1oARIiAAiIAOJzxX8OpARAhAgqIAOAwxn8dpwdAhAgoIAKA3Rn/tQwRABEioIAIAHZj/NczTABEiIACIgAoZ/zXNFQARIiAAiIAKGP81zVcAESIgAIiAEgz/msbMgAiREABEQA8zfivb9gAiBABBUQA8DDj38PQARAhAgqIAOBuxr+P4QMgQgQUEAHAh4x/L1MEQIQIKCACgDcZ/36mCYAIEVBABADfMf49TRUAESKggAgAvjD+fU0XABEioIAIAIx/c1MGQIQIKCACoDHjz7QBECECCogAaMj4EzF5AESIgAIiABox/txMHwARIqCACIAGjD8vLREAESKggAiAhRl/vrVMAESIgAIiABZk/HnNUgEQIQIKiABYiPHnLcsFQIQIKCACYAHGn/csGQARIqCACICJGX8+smwARIiAAiIAJmT8ucfSARAhAgqIAJiI8S/xOSL+/+wj9rZ8AESIgAIiACZg/Et8vm7G8loEQIQIKCACYGDGv0Sb8Y9oFAARIqCACIABGf8SrcY/olkARIiAAiIABmL8S7Qb/4iGARAhAgqIABiA8S/RcvwjmgZAhAgoIALgRMa/RNvxj2gcABEioIAIgBMY/xKtxz+ieQBEiIACIgAOZPxLtB//CAEQESKggAiAAxj/Esb/SgBciYA0EQA7Mv4ljP8LAuAFEZAmAmAHxr+E8f+GAPiGCEgTAVDI+Jcw/q8QAK8QAWkiAAoY/xLG/w0C4A0iIE0EQILxL2H83yEA3iEC0kQAPMH4lzD+HxAAHxABaSIAHmD8Sxj/OwiAO4iANBEAdzD+JYz/nQTAnURAmgiAdxj/Esb/AQLgASIgTQTAK4x/CeP/IAHwIBGQJgLgBeNfwvg/QQA8QQSkiQAI41/E+D9JADxJBKSJAFoz/iWMf4IASBABaSKAlox/CeOfJACSRECaCKAV41/C+BcQAAVEQJoIoAXjX8L4FxEARURAmghgaca/hPEvJAAKiYA0EcCSjH8J419MABQTAWkigKUY/xLGfwcCYAciIE0EsATjX8L470QA7EQEpIkApmb8Sxj/HQmAHYmANBHAlIx/CeO/MwGwMxGQJgKYivEvYfwPIAAOIALSRABTMP4ljP9BBMBBRECaCGBoxr+E8T+QADiQCEgTAQzJ+Jcw/gcTAAcTAWkigKEY/xLG/wQC4AQiIE0EMATjX8L4n0QAnEQEpIkATmX8Sxj/EwmAE4mANBHAKYx/CeN/MgFwMhGQJgI4lPEvYfwHIAAGIALSRACHMP4ljP8gBMAgRECaCGBXxr+E8R+IABiICEgTAezC+Jcw/oMRAIMRAWkigFLGv4TxH5AAGJAISBMBlDD+JYz/oATAoERAmgggxfiXMP4DEwADEwFpIoCnGP8Sxn9wAmBwIiBNBPAQ41/C+E9AAExABKSJAO5i/EsY/0kIgEmIgDQRwLuMfwnjPxEBMBERkCYCeJXxL2H8JyMAJiMC0kQAXzH+JYz/hATAhERAmgggIox/EeM/KQEwKRGQJgKaM/4ljP/EBMDERECaCGjK+Jcw/pMTAJMTAWkioBnjX8L4L0AALEAEpImAJox/CeO/CAGwCBGQJgIWZ/xLGP+FCICFiIA0EbAo41/C+C9GACxGBKSJgMUY/xLGf0ECYEEiIE0ELML4lzD+ixIAixIBaSJgcsa/hPFfmABYmAhIEwGTMv4ljP/iBMDiRECaCJiM8S9h/BsQAA2IgDQRMAnjX8L4NyEAmhABaSJgcMa/hPFvRAA0IgLSRMCgjH8J49+MAGhGBKSJgMEY/xLGvyEB0JAISBMBgzD+JYx/UwKgKRGQJgJOZvxLGP/GBEBjIiBNBJzE+Jcw/s0JgOZEQJoIOJjxL2H8EQCIgAIi4CDGv4TxJyIEAFciIE0E7Mz4lzD+fCEA+EIEpImAnRj/EsafrwgAviIC0kRAMeNfwvjzHQHAd0RAmggoYvxLGH9eJQB4lQhIEwFJxr+E8edNAoA3iYA0EfAk41/C+PMuAcC7RECaCHiQ8S9h/PmQAOBDIiBNBNzJ+Jcw/txFAHAXEZAmAj5g/EsYf+4mALibCEgTAW8w/iWMPw8RADxEBKSJgG8Y/xLGn4cJAB4mAtJEwJXxL2H8eYoA4CkiIK19BBj/EsafpwkAniYC0tpGgPEvYfxJEQCkiIC0dhFg/EsYf9IEAGkiIK1NBBj/EsafEgKAEiIgbfkIMP4ljD9lBABlREDashFg/EsYf0oJAEqJgLTlIsD4lzD+lBMAlBMBactEgPEvYfzZhQBgFyIgbfoIMP4ljD+7EQDsRgSkTRsBxr+E8WdXAoBdiYC06SLA+Jcw/uxOALA7EZA2TQQY/xLGn0MIAA4hAtKGjwDjX8L4cxgBwGFEQNqwEWD8Sxh/DiUAOJQISBsuAox/CePP4QQAhxMBacNEgPEvYfw5hQDgFCIg7fQIMP4ljD+nEQCcRgSknRYBxr+E8edUAoBTiYC0wyPA+Jcw/pxOAHA6EZB2WAQY/xLGnyEIAIYgAtJ2jwDjX8L4MwwBwDBEQNpuEWD8Sxh/hiIAGIoISCuPAONfwvgzHAHAcERAWlkEGP8Sxp8hCQCGJALS0hFg/EsYf4YlABiWCEh7OgKMfwnjz9AEAEMTAWkPR4DxL2H8GZ4AYHgiIO3uCDD+JYw/UxAATEEEpH0YAca/hPFnGgKAaYiAtDcjwPiXMP5MRQAwFRGQ9l0EGP8Sxp/p/HD2AfCoy+Xyy7ZtERE/n33LpH6MiNi27afrx8Y/x/gzJQHAlERA2o9v/JnHGH+mtV0ul7NvgKdt2/ZriADOYfwXtm3b/0XEb8++Y09+BoCp+ZkATmL8mZ4AYHoigIMZf5YgAFiCCOAgxp9lCACWIQLYmfFnKQKApYgAdmL8WY4AYDkigGLGnyUJAJYkAihi/FmWAGBZIoAk48/SBABLEwE8yfizPAHA8kQADzL+tOBZALTg2QHcyfhz8zki/ubsI/bkWQC04tkBvMP404q3AGjF2wG8wfjTjgCgHRHAN4w/LQkAWhIBXBl/2hIAtCUC2jP+tCYAaE0EtGX8aU8A0J4IaMf4QwgAiAgR0IjxhysBAFciYHnGH14QAPCCCFiW8YdvCAD4hghYjvGHVwgAeIUIWIbxhzcIAHiDCJie8Yd3CAB4hwiYlvGHDwgA+IAImI7xhzsIALiDCJiG8Yc7CQC4kwgYnvGHBwgAeIAIGJbxhwcJAHiQCBiO8YcnCAB4gggYhvGHJwkAeJIIOJ3xhwQBAAki4DTGH5IEACSJgMMZfyggAKCACDiM8YciAgCKiIDdGX8oJACgkAjYjfGHYgIAiomAcsYfdiAAYAcioIzxh50IANiJCEgz/rAjAQA7EgFPM/6wMwEAOxMBDzP+cAABAAcQAXcz/nAQAQAHEQEfMv5wIAEABxIBbzL+cDABAAcTAd8x/nACAQAnEAFfGH84iQCAk4gA4w9nEgBwosYRYPzhZAIATtYwAow/DEAAwAAaRYDxh0EIABhEgwgw/jAQAQADWTgCjD8MRgDAYBaMAOMPAxIAMKCFIsD4w6AEAAxqgQgw/jAwAQADmzgCjD8MTgDA4CaMAOMPExAAMIGJIsD4wyQEAExigggw/jARAQATGTgCjD9MRgDAZAaMAOMPExIAMKGBIsD4w6QEAExqgAgw/jAxAQATOzECjD9MTgDA5E6IAOMPCxAAsIADI8D4wyIEACzigAgw/rAQAQAL2TECjD8sRgDAYnaIAOMPCxIAsKDCCDD+sCgBAIsqiADjDwsTALCwRAQYf1icAIDFPREBxh8aEADQwAMRYPyhCQEATdwRAcYfGhEA0Mg7EWD8oRkBAM28EgHGHxraLpfL2TcAJ9i27deIL0EANCMAAKAhbwEAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQkAAAgIYEAAA0JAAAoCEBAAANCQAAaEgAAEBDAgAAGhIAANCQAACAhgQAADQkAACgIQEAAA0JAABoSAAAQEMCAAAaEgAA0JAAAICGBAAANCQAAKAhAQAADQkAAGhIAABAQwIAABoSAADQ0F8AiCXRTxmjtiwAAAAASUVORK5CYII=' joint.shapes.bpmn.icons.federation = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAWAgMAAACnE7QbAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuOWwzfk4AAAAJUExURQAAAP///wAAAHPGg3EAAAACdFJOUwAAdpPNOAAAACdJREFUCNdjYGBatYqBgYkBDIinQCwHOI9xFapcA4iNkLvWtIJ0GwDTEwWTtBTeUwAAAABJRU5ErkJggg=="; + joint.shapes.bpmn.icons.tokenIcon = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAEsBAMAAAAfmfjxAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAIVBMVEVHcEwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAt9G3DAAAACnRSTlMAVxs6eOKWsfPMDMDuHwAACHdJREFUeNrtXU1zFEUYnpn9Ym9JRErntASjuKetpFSYUywUizlFRKmaE2AMuqcEUYs9SQJY5hQRK9ScyMeyu/MrBbHIfPX0+8508r696ee6u8k+2+/H83T3dFuWgYGBgYGBgYGBgYGBgQEF5pdeYV5zEuevPgii/zC+ufqOriycc8+jBIb3ezrS+Go7yuKZdlQ+DaJcjDc6OtGo/RgJMZrRh8cHQVSA8fe68PgskuAvPXi8H0lxQ4dE+TIC4BF/Ju9GIDyagrh6g4e8eXwUgfE3Zx71CIE7jPtggCEy5tsZ+xEKI66l63KExNc8edjbWCLRLZZEBmge0YRjcH2c/10f766tfrH22z/5r37DsGLlBdYftxffvn72lzwm/CrXe9kveXMu5eB/yr7nJf9WOLmSMxkxYJ/vmRZyI9edO9cyzYQXj2b6+20IzXw6l3Y4D8j4DtwHsyrBTTiPV+kU8B0SH6Vr68nomrAtWT/L3v9h8v3rbIi4WCl4mWcvSTZ1kDhPxuIyEyILeLtUC+Of2WNCJPGlnpaoc2MeFbhZKt4T2myFXarDfXjC37NIdyee6lsl/QsHNd+Or0l1SqbWJjOZ9RTzwRav7l4rr/8GrFpJu+yAJIeEPrb8khmSGhLy2IrXrM0qo0ldt2LhMe6hf4WAT0/0KimmS3x6YlhpPsRmo7fsiuna5zIv1KhYQGPpvs9FMJYqO7GiRzvBFYC/x/mra6u9wjbU4ZEi14t/+R8EWx5aPJKkDYys2kC0vB6LLcok8YCR1RcviBzF1iEhkSOxdAD1UMvCujckFFqwAE9MF43EaUYnt+qwvnypcEEkZDDj2AApJSc5aX0obEX7DHJ9E+ifXqMnqnx02d4HpYibIrIiShK6bA9AXTm9QeVQ+HqPiEcN1EXs9KrcUNhJqGYgmqAu0patrXfJ164aIJ/azRBZFxUDqrLVBcWEnyHyuyhCqZYXXFCuZzdxHYiyncqSDEAuN8wQ2RNRHVNX30NYjRa93SOuvw4sSbelRM4Qy0YbNrkWSYm0iGVjE2ZS5SNSJ24kLVhEyJP9KEZfEPfDDsxFCnVAAPKZx98Pi1VrtiG+EHKl6YgerI95MokS40ozk+3CDFEjQ2RZ+JdoWrsPC4jMbsesv+/Srlv1YSnqbEs3lp6h1SgDYNFMZ/tmgWUhIRIKZLnMWc0UdKQOqWaU9OPUHu1RkUYgUY3bUIXkyvZi1mlVI1jq2bLdsVyI3JK9M743614hU4p5FAdOxAmLZxO5EJH/97d7lke94gky7kSs+psxedKzNCdiOecefLt7RVSgNSJS3GloJ03VLcfahsjJdnYEEZKGGCgjQtzZQ2WTOMSicaBs81uLlkhf2WxUm9aP+Mpmoxq0DtFVNhvVpV3X9ZStj3u000FdZf+eeILujLKA6NPufVBXa0LaSeymqn7sEK9P26pUI3Dp6wQMyY6qoV0nIXIkfytGRIN6M8pAUbHpUm8P8hU1Ep9WocTXoqrV35D6sR5Fse2Q7wVsqilbsG1fJ9NIDtQM7AoREUvNc0Uu/Tb/gZLnikL6rdiuimw/mmak2q4V359UIbpbDB58a6konB550YpHxVBBolE+UxlUT5Iaiyeo/epPP7dZPC12sXqiuiwecm1GlRV4wCDXE88m7VT+KdYJicRKTskC7EXUript7sa9ipFFe8ZAq2JsNSMWKZJIklJFx+Vy8Fn8ibYS2jW+u440RRIHIG3hP77A5phDt+x5O+miR3wwSvJZyZ0q47lMSqRV7RBMP+LwxHHqm5TozfWITfHdrnRUrM/mbNa2dNc7dEBKFIpjjCxklvhsIquWferoabmSxed0F3yIOAM+B571q9z9sBCxOSbMzj13HNjYkofMz5ASWahyFHy/6DE4MneID65PGB3wLbyi4x6yYlGn+kXhdRzSNLGDsjX7OBAKb0wYSnI3eUYudVcvuvxlUuj2aqnkuk47IF7hLRYzcB7EA2IV32IzEeaJnQ5J4gFpyi7gEVzolrmVj3pAXOklKd/lJErtWhWZefxmXVC8bqd+bOdcwO5OmBbo5prh/cWjj1y4GjC8pceH3sLz56+fzy4tzZ69+zz3ZWKVlX3+tiRKznwfo1kvCeJMR0RWMagDK8+sl8GQOrDyzHoZUFcs9OV0bC9EtJXwYHDZ5oIKHi879EQGCnhwuAuxPiU8xGYdjicceBSYdSh4XHRcPbI2LBbwqqbHHA8eVlBN77K5aL5ZiceznsUFbnkWk1U+NEBmPda+z979/9bTx7urcxYntFBjsPP6I/MXluY7FjegLNW4Z3EFzqwfsuWBNOvrfIngIqvDlgfOrO/xHZDGtEQWyqwP+UYWzqwf8B0QnFm/xZcIyqxP+PLAWaotvkRwZn2ZL5HwNETWw9kU5vgS8TSttTiz/lIfHsVmfawPEYlZn9GFh8ysr+tCRGbWN3Uh4utra3FmfaIJEblZ70xHZLHWVjizvqIFkYbWhhBn1rUQKRCzroVIAZl1HUTKQPNpLJxZ3+dPBGbWNRApoe4uHTkNxF6kePpPAAHMukYipTkFs9YQs66NSIGvrDMXKYiVdd4iBbH+yVqkYFbWWYsUzMr64ZREFmuRgltZZyxScCvrjEWKWVnX0ayblfUTBGqfGeOVdVyuczZWmG3XI8Y8itrInkYr64XzDjqtrCcOy9MqkjBTWoy3XiLF74xWRLpTsLIuc+z7WhGx9V9Zl4qUiV5ECkRKTysinpaqHSdSVrQiUtdyEz9uGsWIFCNSjkuk7BiRYkSKESlGpBiRYkSKmUkxIsWIFCNSTpFIGepF5DSIlOUpESmMV9ZxIkWv1Z4CkaKX/BWLFM2clVikbOlGpDsVNUssUka68bCcpVwsWgYGBgYGBgYGBgYGBgYG9PgXizVBvXkUh8MAAAAASUVORK5CYII="; joint.shapes.bpmn.icons.eventGateway = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAMAAACdt4HsAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAYdEVYdFNvZnR3YXJlAHBhaW50Lm5ldCA0LjAuOWwzfk4AAAB1UExURf///wAAAAEBAQsLCw0NDRERERcXFxgYGBwcHCMjIyQkJCkpKSoqKisrKy8vLzAwMDExMTMzMzU1NTk5OTs7O0JCQkNDQ0pKSktLS1FRUVJSUlRUVFlZWVpaWl5eXmFhYWNjY2lpaXNzc3x8fIeHh7q6utvb22ODD/wAAAABdFJOUwBA5thmAAAD10lEQVRYw5VXW3viOgzUbC4kbeiyZcPCNtTF9pn//xPPg3xLSKDrB39tsGXNjCVLIhuD3vuvr+vpdP368t5T/mnQXzvMRnf9vg26vw1WRvPX8VuH79OW6vVtHN9eq/Rh/9QN+l/RZ5OA0/spIvr12ARHXTbY5Tp6O+hv4wMLPOh2Tz03q6Af1MRh0wJ3ANA7itBddyWBu6t+7QFgxw3yawD4pAjNigyNoQg/AaBek4NnAGg9hbZeV6G2FPoWAM53FtT0QKELvve3rMKtD0gchUN0dDYcABwZHEG7kIHetghH8wgAboW/Y/yxsWsYbYVi0ZxJ9ioP94+U1luyp8rdc0FgR7VTEsRpOC3Xoaewm6/zAKpguMoSKZ+lZnRVcLUC4NPnAYBVaBUX5yns/LFSjFY1ywoMVCEyucrHDwDoCknCKvZ5MXsAXtiUuOgaADj8N2LOS+TLZx69wjJlnISw/GC0tOcMsFHCfApBL6yBJl29rvBcr16TuGQD1IznqgODOmDDio954Ac20/9WXeiDC05JyQ5oVqhm6nUA8MKsRM2wUWQC4MUBmLL4WaKw5xjDUcIWJ163sAN6yiX4E9y9C9ccCQH1RdgnOYywVVHU/XY1YfRQAfXPVmnz4pM7t8DPVtJTaScRkVuC7cPftmB0M+3yMxwjHoANpxqgYphV42Ez65p0dyrAhHkCXkUM8Coikshc9aALHIi8AibMIzCK/AHeU6zYDQM+UiDyDvwR+Q2MwYDOAcNlw8Ate1fsWhiQSw6JFQQt7wxcgHcJc7zo7hkCeQOmMJs5ieGWPUFQkjiXUYN3FUOJoJTRFZfqIYYSgU+XuJxvjzGUCG6LrTmYtjGEACrRaCII12t6hmGJYEoXMycU+whDadYWvsfIYpM85JCdlTIXp5zbAg0TKSEAi6RqAdx4j+AyT6rpHA3AkCnjYtTDbNTFQ1QDFQuT4WWzKZeww2qlypyYrL4WLj81TtimZGpXDZj8SLSMxyaX+/JxpTP3I6T08nH1ZX35SZ6RaNiuRWsAZ/Jjlnw9lMfjVg04ryWP6m1ZSoxaNekzet4uZc+xFtwlTgoQQ7TwuMiKy+avB9uyzKvXy7y6LPOWd5XVrNDc3Reau1mhWd0d4WIN6LQkxUtZ6oar1bpYS7oNgnae8SEGUA8/T6efQ6q9bSql14jWGvBZuf+xLD7umMSLthZTW+5uJ08Ruhes8HfX8uxjy2PMNI6TMbHl2T9peXLTtV9runR7ZR63bf6QW9Wi7UuN7OFp7xl7sxUVYj/3vPW9rLe+F/f99jm3qrmR/ecGvlRhe/f/2BV4PKllnhkAAAAASUVORK5CYII="; joint.shapes.bpmn.icons.messageThrow = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAVBAMAAADV4/HZAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC45bDN+TgAAABJQTFRF////AAAAEBAQICAgMDAwQEBA9ql18wAAAAF0Uk5TAEDm2GYAAABVSURBVBjT1Y25EYAwEAOXL2fNkEMHpgQ6gP6bIfAc9tABirQanQ4mG2XIDJU3wIs+eAZR6AqvsIsmuFUXOJXiDk2lSe2WS2Ittnn/jbaBOQx+9J/gAcYZFZNx+7SlAAAAAElFTkSuQmCC"; joint.shapes.bpmn.icons.timer = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGEAAABhCAMAAADx2D+RAAAABGdBTUEAALGPC/xhBQAAAAlwSFlzAAAOwgAADsIBFShKgAAAABh0RVh0U29mdHdhcmUAcGFpbnQubmV0IDQuMC45bDN+TgAAAFpQTFRF////AAAABwcHCAgICgoKDw8PEBAQERERHBwcHR0dHh4eHx8fICAgKioqLy8vMDAwNzc3Ozs7Pz8/QEBAT09PUFBQWlpaX19fYGBgb29vcHBwf39/gICAj4+PemnMMAAAAAF0Uk5TAEDm2GYAAAPzSURBVGjetVrpgqMgDNbdxT10cQbpUofy/q+5tfUAkkCgNn86I5ivuUNo0xTR7UHNG+g2jW1I3fR1HveY+UHD9QT2Q5um4TVRLi2Hpmr+U8ul4c38FxrL9d+WUqHRMfsKdaf7p1RKYBDdKwIIbZ8r9v7P+qeT9WJEFhDmWPIQFtKiyhpd8JIK1iKEO6lyTQUAH9EiRGiazwCjVIKJgWDbIojYeASC88CXbd/YEIEBFIzWDUEfOWnZ1a8P8hCeiuyK51AEj9n6MbMgumiTAd4BEfrd3xgQRyCL9cni7jOCYHZNOo+hyDntBQA83QRB2IR1j7/2iJTp0HMQ4KmDwGOtDZRkwu0ymUBwLaJa3awqmzaKD5EwxYCvKay+bIV7fnoq+j2BKY7AnIFomhL3IQylCkM5asxulqTFFgMoRWgwht4TtmTkRk18y3A18qft6TdO9s3E1U9sfSrqGtIIA8qrqPsxhMVgfYTv8MrtEdC5Aj8CEX6vPtvd6pS0YpvPeIvzHuisOWbK68ajsMcxMQbvyIw5Rjwy16/WBx7bEWKLFiSDvJIePYew6CYH3pkTRcTBBLwzNSADmqDj8RULkkE+oGeFePQQSGSbEwIa3cbvpSoRNsX2LIBkQCPNpvHeMUVNrcvsc17bq96hpH1j530rFsDMLiNRz8ZGIAL6RAT+5r4OgQxo2pnsjsAp0PtrUw1CfzuI0fxfEqObB33sCA45t1rW+QVONCzKyxYgGLDzwkBoChC6zHAmjdDbg7iHPCCKx+MDIHAO207x50zQl5h5qTE9hXEWwqILxUAYKrNGQhRN5hdRhQCt4kgEVYsQiYL7nSiucaQoPV3jXMWICxFlThSS9hU1sQqJYNetWgSNRPVUPqU1A9IZ9kTfeq3RWVCYutB9IoRbVzMGfnrUF17PVdDGDrymjuhzuuA01yNmmQqqNl6ZR4/hHCOIf+xWItXBXP4Cy0bVsd5traBmtfQIt9hniXx+PO9fjrUj40q0Ap8Qzj9QXrr+poJqnmN/FNRYw6RDb4z3Xyl1OGqqkexSNWDkaI+U9JyvT1p1wFsqJKYEf1YZLlu0K0x2dF3Yk+iou4471Q6bXs3pYOlyM+NQcgMBiLwmQTR2MUN4wxHtziQ2gczuReYORe5B1OYBAogrambklmbddGVecQh/SCehQhEE/TDcyL6mEbx7oPiV7wVXTSGEOf8uK4L4xUD4E1xAcpKjTFxEQoTwipNZfw194IwQ4jtsfn0Et8PbDxs8hK+Bfw7PampT2DQtXIcJPfWWFmAnSu/YbXE11EX86zoU82b+DwzxXv60zT2SrnmZnCYlUbY5i+wc/4JC6fO4h1C2lPF/NlQ1aBrowPMAAAAASUVORK5CYII="; @@ -621,19 +622,20 @@ angular.module('roger_federation.Workflows') addStencilToolip(stencil.graphs.artifacts); } else if (diagramType === "Federation") { stencil.load([ - // new joint.shapes.bpmn.Activity({ - // position: { x: 10, y: 10 }, size: { width: 80, height: 80 }, - // roger_federation: { - // name: "", - // type: "Federate", - // stringId: "", - // description: "", - // attributes: [], - // groups: [] - // }, - // graphType: "FederateCell", - // icon: "federation" - // }), + new joint.shapes.bpmn.Activity({ + position: { x: 10, y: 100 }, size: { width: 80, height: 80 }, + roger_federation: { + name: "", + type: "FederationTokenGroup", + stringId: "", + description: "", + interconnected: false, + groupFilters: [], + attributes: [] + }, + graphType: "FederationTokenGroupCell", + icon: "tokenIcon" + }), new joint.shapes.bpmn.Activity({ position: { x: 100, y: 10 }, size: { width: 80, height: 80 }, roger_federation: { diff --git a/src/federation-hub-ui/src/main/webapp/modules/workflows/settings_controller.js b/src/federation-hub-ui/src/main/webapp/modules/workflows/settings_controller.js index 6f361c38..b0e23588 100644 --- a/src/federation-hub-ui/src/main/webapp/modules/workflows/settings_controller.js +++ b/src/federation-hub-ui/src/main/webapp/modules/workflows/settings_controller.js @@ -34,4 +34,15 @@ function settingsController($rootScope, $scope, $http, $stateParams, $timeout, $ }); } + $scope.restartBroker = function() { + growl.success("Federation Hub Broker Process Restarting - Please wait for it to restart before proceeding."); + WorkflowService.restartBroker().then(function(result) { + if (result.status === 200) + growl.success("Federation Hub Broker Process Has Successfully Restarted"); + else + growl.error("Federation Hub Broker Process Restart Failed"); + }, function(error) { + growl.error("Failed to restart SERVER" + error); + }); + } } \ No newline at end of file diff --git a/src/federation-hub-ui/src/main/webapp/modules/workflows/workflow_services.js b/src/federation-hub-ui/src/main/webapp/modules/workflows/workflow_services.js index 6e0f351c..26d110b6 100644 --- a/src/federation-hub-ui/src/main/webapp/modules/workflows/workflow_services.js +++ b/src/federation-hub-ui/src/main/webapp/modules/workflows/workflow_services.js @@ -633,6 +633,32 @@ angular.module('roger_federation.Workflows') }); }; + workflowService.generateJwtToken = function(tokenRequest) { + return $http({ + method: 'POST', + url: ConfigService.getServerBaseUrlStrV2() + 'generateJwtToken', + data: tokenRequest, + headers: { + 'Content-Type': 'application/json' + } + }).then(function(res) { + return res.data; + }, function(reason) { + throw reason; + }); + }; + + workflowService.isAllowFlowIndicators = function() { + return $http.get( + ConfigService.getServerBaseUrlStrV2() + 'allowFlowIndicators/').then( + function(res) { + return res.data; + }, + function(reason) { + throw reason; + }); + }; + workflowService.getSelfCa = function() { return $http.get( ConfigService.getServerBaseUrlStrV2() + 'getSelfCaFile/').then( @@ -644,6 +670,17 @@ angular.module('roger_federation.Workflows') }); }; + workflowService.restartBroker = function() { + return $http.get( + ConfigService.getServerBaseUrlStrV2() + 'restartBroker/').then( + function(res) { + return res; + }, + function(reason) { + throw reason; + }); + }; + workflowService.getDataFlowStats = function() { return $http.get( ConfigService.getServerBaseUrlStrV2() + 'getBrokerMetrics/').then( diff --git a/src/federation-hub-ui/src/main/webapp/modules/workflows/workflows_controller.js b/src/federation-hub-ui/src/main/webapp/modules/workflows/workflows_controller.js index 36776b69..f1776ea7 100644 --- a/src/federation-hub-ui/src/main/webapp/modules/workflows/workflows_controller.js +++ b/src/federation-hub-ui/src/main/webapp/modules/workflows/workflows_controller.js @@ -16,6 +16,8 @@ function workflowsController($scope, $rootScope, $window, $state, $stateParams, $state.tids = {} $state.cache = {} + console.log('workflowsController') + function translateIds(){ if (JointPaper.paper && JointPaper.paper._views) { var nodeKeys = Object.keys(JointPaper.paper._views); @@ -116,9 +118,16 @@ function workflowsController($scope, $rootScope, $window, $state, $stateParams, } function AnimateDataFlow(){ - if($state.fkdi == ""){ - $state.fkdi = $interval(pollDataFlowStats, 5000); - } + WorkflowService.isAllowFlowIndicators().then(function(allowFlowIndicators) { + if (allowFlowIndicators) { + console.log('Allow Flow Indicators') + if($state.fkdi == ""){ + $state.fkdi = $interval(pollDataFlowStats, 5000); + } + } else { + console.log('Disable Flow Indicators') + } + }) } function pollActiveConnections() { @@ -167,7 +176,7 @@ function workflowsController($scope, $rootScope, $window, $state, $stateParams, cellView.resize() } - if (cellView.model.attributes.graphType === "GroupCell") { + if (cellView.model.attributes.graphType === "GroupCell" || cellView.model.attributes.graphType === "FederationTokenGroupCell") { let linkedActiveConnections = [] activeConnections.forEach(activeConnection => { @@ -515,6 +524,8 @@ $scope.saveGraph = function() { $state.go('workflows.editor.addFederateGroup'); } else if (['FederationOutgoing'].indexOf(propertiesType) !== -1) { $state.go('workflows.editor.addFederationOutgoing'); + } else if (['FederationTokenGroup'].indexOf(propertiesType) !== -1) { + $state.go('workflows.editor.addFederationTokenGroup'); } } } diff --git a/src/federation-hub-ui/src/main/webapp/scripts/app.js b/src/federation-hub-ui/src/main/webapp/scripts/app.js index 82c07e45..fc1d0e27 100644 --- a/src/federation-hub-ui/src/main/webapp/scripts/app.js +++ b/src/federation-hub-ui/src/main/webapp/scripts/app.js @@ -371,6 +371,15 @@ angular controller: 'AddFederationOutgoingController' }); + modalStateProvider.state('workflows.editor.addFederationTokenGroup', { + backdrop: 'static', + keyboard: false, + size: 'lg', + url: '/addFederationTokenGroup', + templateUrl: "views/workflows/add_federation_token_group.html", + controller: 'AddFederationTokenGroupController' + }); + modalStateProvider.state('workflows.editor.connections', { backdrop: 'static', keyboard: false, diff --git a/src/federation-hub-ui/src/main/webapp/views/ca/ca.html b/src/federation-hub-ui/src/main/webapp/views/ca/ca.html index 182e62d3..d5bcd30d 100644 --- a/src/federation-hub-ui/src/main/webapp/views/ca/ca.html +++ b/src/federation-hub-ui/src/main/webapp/views/ca/ca.html @@ -15,7 +15,7 @@

Remove Saved CA's

{{ca.uid}} - + diff --git a/src/federation-hub-ui/src/main/webapp/views/settings.html b/src/federation-hub-ui/src/main/webapp/views/settings.html index 1acfab40..ddda1f69 100644 --- a/src/federation-hub-ui/src/main/webapp/views/settings.html +++ b/src/federation-hub-ui/src/main/webapp/views/settings.html @@ -5,3 +5,7 @@

Settings

+
+ + + diff --git a/src/federation-hub-ui/src/main/webapp/views/workflows/add_federation_token_group.html b/src/federation-hub-ui/src/main/webapp/views/workflows/add_federation_token_group.html new file mode 100644 index 00000000..1a831ec5 --- /dev/null +++ b/src/federation-hub-ui/src/main/webapp/views/workflows/add_federation_token_group.html @@ -0,0 +1,87 @@ + + + + + diff --git a/src/gradle.properties b/src/gradle.properties index 9e6ed147..656f4880 100644 --- a/src/gradle.properties +++ b/src/gradle.properties @@ -11,36 +11,21 @@ dom4j_version = 2.1.3 # NEW_VERSION httpcomponents_version = 5.1.4 httpcomponents_httpmime_version=4.5.14 -# slf4j_version = 1.7.32 -# NEW_VERSION -slf4j_version = 2.0.7 + +slf4j_version = 2.0.13 #logback_version = 1.2.11 # NEW_VERSION -logback_version = 1.4.12 -#log4j_api_version = 2.17.1 -# NEW_VERSION -log4j_api_version = 2.19.0 +logback_version = 1.5.6 + +log4j_api_version = 2.23.1 logback_jackson_version = 0.1.5 janino_version= 3.1.10 -# Switched versions of of postgres to address xray reported vulnerability -#postgres_version = 42.2.5 -postgres_version = 42.7.2 -# Previous 5.3.21 released 06/2022 -# Current 5.3.28 released 06/2023 -#spring_version = 5.3.28 -# NEW_VERSION -spring_version = 6.0.17 - -# Current 5.3.8.RELEASE released 04/2021 -# Candidate 5.3.13.RELEASE released 12/2021 -# Candidate 5.5.6 released 04/2022 -# Candidate 5.6.3 released 04/2022 -#spring_security_version = 5.7.2 -# Version recommended by xray -# spring_security_version = 5.7.5 -# NEW_VERSION -spring_security_version = 6.1.7 +postgres_version = 42.7.3 + +spring_version = 6.0.22 + +spring_security_version = 6.1.9 spring_security_oauth2_authorization_server_version = 1.1.3 @@ -48,37 +33,15 @@ spring_security_oauth2_authorization_server_version = 1.1.3 # Spring suggests com.nimbusds:nimbus-jose-jwt:9.22 spring_security_jwt_version = 1.1.1.RELEASE -# Current 2.4.2 released 01/2021 -# Candidate 2.4.13 released 11/2021 -# Candidate 2.5.13 released 04/2022 -# Candidate 2.6.7 released 04/2022 -# spring_boot_version = 2.7.1 -# NEW_VERSION -spring_boot_version = 3.0.13 +spring_boot_version = 3.1.12 -# Renamed io.awspring.cloud.spring-cloud-starter-aws -# Current 2.2.3.RELEASE released 06/2020 -# Candidate 2.2.6.RELEASE released 02/2021 -# Candidate 2.3.4 released 02/2022 Code needs updating -# Candidate 2.4.1 released 04/2022 -spring_cloud_starter_version = 2.4.1 +spring_cloud_starter_version = 2.4.4 -# Current 2.4.3 released 01/2021 And missing from github release listing -# Candidate 2.4.15 released 11/2021 -# Candidate 2.5.11 released 04/2022 -# Candidate 2.6.4 released 04/2022 -spring_data_commons_version = 2.7.1 +spring_data_commons_version = 2.7.18 # Technically spring-data-jpa version -# Current 2.4.3 released 01/2021 And missing from gitlab release listing -# Candidate 2.4.15 released 11/2021 -# Candidate 2.5.11 released 04/2022 -# Candidate 2.6.4 released 04/2022 -#spring_data_version = 2.7.1 -# NEW_VERSION -spring_data_version = 3.0.8 +spring_data_version = 3.0.12 -#micrometer_cloudwatch_version = 1.5.5 # NEW_VERSION micrometer_version= 1.10.9 @@ -207,9 +170,7 @@ gson_version = 2.9.1 # Candidate 10.0.20 released 04/2022 Build Failures # Current 9.0.62 released 04/2022 -#tomcat_version = 9.0.78 -# NEW_VERSION -tomcat_version = 10.1.16 +tomcat_version = 10.1.25 servlet_api_version = 4.0.1 @@ -277,3 +238,6 @@ okhttp3_version = 4.12.0 # Will try to remove this dependency javax_servlet_version = 2.5 + +# latest is currently 3.0.9 but causes jaxb build conflicts, so using 3.0.7 +javaapiforkml_version = 3.0.7 diff --git a/src/lib/JavaAPIforKml-2.2.2.jar b/src/lib/JavaAPIforKml-2.2.2.jar deleted file mode 100644 index 707d9d98..00000000 Binary files a/src/lib/JavaAPIforKml-2.2.2.jar and /dev/null differ diff --git a/src/settings.gradle b/src/settings.gradle index 4b4b2e69..4da3a28b 100644 --- a/src/settings.gradle +++ b/src/settings.gradle @@ -25,4 +25,5 @@ include 'federation-hub-policy' include 'federation-hub-ui' include 'takserver-protobuf' include 'takserver-tool-ui' +//include 'testing:tak-db-profiler' diff --git a/src/takserver-cluster/README.md b/src/takserver-cluster/README.md index 1a433875..72ce60f1 100644 --- a/src/takserver-cluster/README.md +++ b/src/takserver-cluster/README.md @@ -1,5 +1,10 @@ # Automated Cluster Quickstart +Cluster deployment has been tested on the following targets: + - AWS EKS + - RKE2 + - Minikube + ## Cluster Unimplemented Features - Latest SA - Port changes / Input definitions. It's possible to change these in the cluster (add / remove / modify inputs), but the cluster kubernetes YAML would need to be changed also to reflect the changes. @@ -12,12 +17,14 @@ - Injectors - Plugins - untested -## Prerequisites +## AWS Deployment + +### Prerequisites - AWS: - An AWS commerical account. AWS GovCloud has not been recently tested. - __OPTIONAL:__ A DNS Hosted Zone registered in AWS Route 53 - https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/domain-register.html. This can be used to map a domain name to the Load Balancer, but is not required -- The following software must be installed on the workstation you are using to create the cluster: +- An x86-64 system (deployment from ARM-based Macs has not been tested and is unlikely to work) with the following software must be installed on the workstation you are using to create the cluster: - python 3.7 - pip 3.7 - Kubectl (kubernetes client version 1.21) @@ -29,12 +36,12 @@ - helm - Install as described here: https://helm.sh - eksctl - Required to create and manage AWS EKS clusters. Install as described here: https://eksctl.io -## Install Dependencies and Check Software +### Install Dependencies and Check Software - run `pip3 install -r cluster/scripts/requirements.txt` to install python dependencies - run `kubectl version` to ensure it is available from your user account, and that it is version 1.21. - run `docker run hello-world` to ensure docker is running -## Configuring AWS Credentials +### Configuring AWS Credentials - Set Region : `aws configure set region ` [(List of Regions)](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.RegionsAndAvailabilityZones.html) - Set Access Key : `aws configure set aws_access_key_id ` [(Managing access keys (console))](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_access-keys.html) - Set Secret Key : `aws configure set aws_secret_access_key ` @@ -44,26 +51,66 @@ `aws configure set aws_secret_access_key 318860611555` - View config : `aws configure list` -## Create a CA and X.509 certificates for TAK Server +### Create a CA and X.509 certificates for TAK Server - The installer script will automatically generate certs at cluster/takserver-core/certs/files unless certs are already present. You can import your own certs into cluster/takserver-core/certs/files to use existing ones. However, a naming convention for certs is used so we recommend following the automatic generation process first. -## Set Up Environment Variables +### Set Up Environment Variables - Edit __cluster/cluster-properties__ --- Set and review the options here including the cluster name, number of EC2 nodes, domain name to register (optional). The number of pods per service (messaging, API etc) is defined based on the total number of EC2 nodes. +-- Set the TAK_DEPLOYMENT_TARGET to aws and configure the **Common Properties** and **AWS Properties**. Set and review the options here including the cluster name, number of EC2 nodes, domain name to register (optional). The number of pods per service (messaging, API etc) is defined based on the total number of EC2 nodes. - Source __cluster/cluster-properties__ `source cluster/cluster-properties` -## Build AWS EKS TAK Server Cluster (see note about alternate KOPS method at the bottom of this README) +### Build AWS EKS TAK Server Cluster (see note about alternate KOPS method at the bottom of this README) - Depending on your environment, your python command may be `python` rather than `python3`. `python3 cluster/scripts/build-eks.py` -## Delete EKS TAK Server Cluster __this command will delete your cluster and all data__ +### Delete EKS TAK Server Cluster __this command will delete your cluster and all data__ - Depending on your environment, your python command may be `python` rather than `python3`. - `python cluster/scripts/delete-eks.py` +- If it gets stuck with a message along the lines of "1 pods are unevictable from node ip-1-2-3-4.ec2.internal" you may need to navigate to the AWS CloudFormation console and manually remove the nodegroup. This will unblock that step of the deletion. + -## Notes +### Notes - AWS DNS propagation can take some time. When building the cluster and testing the load balancer, you may experience connection refused or host not found errors. Give them a reasonable amount of time to resolve, 20-30 minutes, before troubleshooting / debugging +## Generic (Minikube and RKE2) Deployment + +### Prerequisites +- Docker Engine 20 +- Python 3.7 +- Python 3.7 Pip +- PyYAML + +Helm, Kubectl, and Minikube will be downloaded automatically to `scripts/setup-bins/` on Linux x86_64, Linux ARM, +Linux ARM64, Mac OSX x86_64 (Older x86-based systems), and Mac OSX ARM4 (M2, M3, etc). + +### Install Dependencies +Install docker, python, and python-pip as is typical for your operating system. To install PyYAML execute +`python3 -m pip install pyyaml`. + +### Set Up Environment Variables + +The file __cluster/cluster-properties__ contains the configuration settings. Set the TAK_DEPLOYMENT_TARGET to +**generic-rke2** or **generic-minikube** depending on your deployment target. Then configure the **Common Properties** +and **Generic Properties**. If using RKE2 you will need to set the TAK_KUBECONFIG_FILE variable to a credentials file. +If you are using Minikube an instance will automatically be brought up and used utilizing the configured +TAK_MINIKUBE_DRIVER as the backend. It is recommended to use a personalized namespace if using a shared cluster to +prevent deployment conflicts! + +### Deploy Generic environment +Executing the following command will deploy the environment: `python3 cluster/scripts/build-generic.py`. This will do +the following: +1. Download the proper versions of kubectl, helm, and if applicable minikube to scripts/setup-bins. +2. Build the docker images. +3. Produce certs in if TAK_CERT_SOURCE_DIR has not been set to a certificate directory. +4. Update the helm configuration files. +5. Publish the docker images to TAK_INSECURE_PUBLISH_REPO. +6. If running minikube and TAK_MINIKUBE_DELETE_EXISTING_INSTANCE is set to true, stop and delete any running Minikube instances. +7. If running minikube, start up a new instance using the configured driver, memory, and CPU settings. +8. Deploy it to the cluster. +9. Display the port mapping details. More advanced ingress is not yet supported. + + ## Useful Commands - Tail Logs `kubectl logs -f ` diff --git a/src/takserver-cluster/build.gradle b/src/takserver-cluster/build.gradle index 250b3e2b..18cc5cf6 100644 --- a/src/takserver-cluster/build.gradle +++ b/src/takserver-cluster/build.gradle @@ -99,7 +99,7 @@ task zipCluster(type: Zip) { dependsOn pullTakserverCore dependsOn pullSchemaManager dependsOn pullUserManager - dependsOn pullTakcl +// dependsOn pullTakcl dependsOn deleteCoreConfig dependsOn copyClusterScripts dependsOn copyDeployments @@ -136,7 +136,7 @@ task zipClusterHelm(type: Zip) { dependsOn pullTakserverCore dependsOn pullSchemaManager dependsOn pullUserManager - dependsOn pullTakcl +// dependsOn pullTakcl dependsOn deleteCoreConfig dependsOn copyClusterScripts dependsOn copyDbConfigurations @@ -197,5 +197,5 @@ task buildHelmCluster { dependsOn copyCertsConfigMap } - - +// TODO: Fix this gradle-detected dependency that probably shouldn't be +tasks.getByName('jar').dependsOn('copyClusterConfig', 'copyClusterProperties', 'moveCoreConfig') diff --git a/src/takserver-cluster/cluster-properties-template b/src/takserver-cluster/cluster-properties-template index 0cd9fa26..8c4a8f7d 100644 --- a/src/takserver-cluster/cluster-properties-template +++ b/src/takserver-cluster/cluster-properties-template @@ -1,25 +1,22 @@ -### ------ TAK CLUSTER PROPERTIES ------ ### +######################## COMMON DEPLOYMENT PROPERTIES ######################### -# Absolute path to the takserver cluster directory. (eg. /Users/user/takserver-cluster-/cluster) -export CLUSTER_HOME_DIR= +# Valid values: 'aws', 'generic-rke2', 'generic-minikube' +export TAK_DEPLOYMENT_TARGET= -# Unique Cluster Name - This will be used to keep naming conventions consistent across resources. No capitals or underscores (eg. bbn-cluster) +# Unique Cluster Name - This will be used to keep naming conventions consistent across resources. No capitals or +# underscores (eg. bbn-cluster). In generic deployments this along with a partial uuid will be used as the namespace export TAK_CLUSTER_NAME= -# Set the AWS zones for the cluster region. At least 2 zones are required. (eg. "us-east-1a,us-east-1b") -export TAK_CLUSTER_ZONES=${ZONES:-","} +# The optional directory that contains the certificate files. This must be less than 1MB and contain an 'admin.pem' admin certificate! +export TAK_CERT_SOURCE_DIR= -# Set the number of cluster nodes +# Set the number of cluster nodes. If using rke2 or minikube setting to zero will result in the bare minimum for +# development to be used. Realistically the number of actual nodes will be around 4x this number export TAK_CLUSTER_NODE_COUNT=20 # Set if takserver plugins should be enabled (0 false, 1 true) export TAK_PLUGINS=0 -### ------ Configure Route 53 Domain Name or Gossip-based DNS ------ ### -# By default, gossip-based DNS is used in the cluster. The domain name must end with k8s.local -# Or, set the Domain Name of the hosted zone you registered with Route53. (eg. yourorg.tak.net) (leave off trailing .) -export TAK_CLUSTER_DOMAIN_NAME=k8s.local - ### ------ DATABASE PROPERTIES ------ ### # Master username for RDS @@ -28,15 +25,6 @@ export TAK_DB_USERNAME=martiuser # Master password for RDS export TAK_DB_PASSWORD= -# RDS DB Instance Size -# Size details: https://aws.amazon.com/rds/instance-types/ -# Postgres compatibility: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html -export TAK_DB_SIZE=db.m5.16xlarge - -# RDS DB Initial Storage -export TAK_DB_ALLOCATED_STORAGE=500 - - ### ------ CERTIFICATE PROPERTIES ------ ### # (Can Leave Blank If Reusing Certs) @@ -53,17 +41,85 @@ export TAK_CITY= # Set Certificate Organizational Unit export TAK_ORGANIZATIONAL_UNIT= -# Absolute path to the takserver configmap file that will override the default cert values (eg. /Users/user/takserver-cluster-/cluster). -# Leave empty if you want to use the defaults. -export CERT_CONFIGMAP_FILE= -### ------ PRESET VARIABLES - DO NOT EDIT ------ ### -export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id) -export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key) -export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) -export AWS_S3_KOPS_STORE_NAME=tak.server-$TAK_CLUSTER_NAME -export TAK_CLUSTER_REGION=$(aws configure get region) -export KOPS_STATE_STORE=s3://$AWS_S3_KOPS_STORE_NAME -# get or refresh ECR token -aws ecr get-login-password --region $TAK_CLUSTER_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$TAK_CLUSTER_REGION.amazonaws.com +######################### AWS DEPLOYMENT PROPERTIES ########################### +# Absolute path to the takserver cluster directory. (eg. /Users/user/takserver-cluster-/cluster) +export CLUSTER_HOME_DIR= + +# Set the AWS zones for the cluster region. At least 2 zones are required. (eg. "us-east-1a,us-east-1b") +export TAK_CLUSTER_ZONES=${ZONES:-","} + +### ------ Configure Route 53 Domain Name or Gossip-based DNS ------ ### +# By default, gossip-based DNS is used in the cluster. The domain name must end with k8s.local +# Or, set the Domain Name of the hosted zone you registered with Route53. (eg. yourorg.tak.net) (leave off trailing .) +export TAK_CLUSTER_DOMAIN_NAME=k8s.local + +# RDS DB Instance Size +# Size details: https://aws.amazon.com/rds/instance-types/ +# Postgres compatibility: https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html +export TAK_DB_SIZE=db.m5.16xlarge + +# RDS DB Initial Storage +export TAK_DB_ALLOCATED_STORAGE=500 + + + +####################### GENERIC DEPLOYMENT PROPERTIES ######################### + +# Currently only tested with insecure docker registries, but secure registries should also work in theory +export TAK_INSECURE_PUBLISH_REPO= + +#### ------ RKE2 Properties ------ #### + +# The configuration file to use to connect to the kubernetes instance +export TAK_KUBECONFIG_FILE= + +# If there are issues applying the ingress rules you may be able to enable this to get around any issues in your environment +export TAK_USE_PORT_EXPOSURE=false + +#### ------ MINIKUBE PROPERTIES ------ #### + +# Minikube is meant for development only. If you don't have enough CPUs the +# services will fail! The number defined here assumes +# TAK_CLUSTER_NODE_COUNT=0 (which defaults to the bare minimum) and TAK_PLUGINS=1 +export TAK_MINIKUBE_DRIVER=docker +export TAK_MINIKUBE_MEMORY=16g +export TAK_MINIKUBE_CPUS=10 + +# Stop and delete any existing minikube instance +export TAK_MINIKUBE_DELETE_EXISTING_INSTANCE=false + + + +### ------------------- PRESET VARIABLES - DO NOT EDIT ------------------- ### + +export TAK_HELM_VERSION=v3.12.3 +export TAK_MINIKUBE_VERSION=v1.31.2 +export TAK_KUBERNETES_VERSION=v1.27.0 + +# Ignore AWS stuff if not necessary so aws installation isn't necessary +if [[ "${TAK_DEPLOYMENT_TARGET}" == "aws" ]];then + echo Activating AWS configuration + export AWS_ACCESS_KEY_ID=$(aws configure get aws_access_key_id) + export AWS_SECRET_ACCESS_KEY=$(aws configure get aws_secret_access_key) + export AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + export AWS_S3_KOPS_STORE_NAME=tak.server-$TAK_CLUSTER_NAME + export TAK_CLUSTER_REGION=$(aws configure get region) + export KOPS_STATE_STORE=s3://$AWS_S3_KOPS_STORE_NAME + # get or refresh ECR token + aws ecr get-login-password --region $TAK_CLUSTER_REGION | docker login --username AWS --password-stdin $AWS_ACCOUNT_ID.dkr.ecr.$TAK_CLUSTER_REGION.amazonaws.com + +elif [[ "${TAK_DEPLOYMENT_TARGET}" == "generic-rke2" ]];then + echo Activating Generic RKE2 configuration + export TAK_KUBERNETES_NAMESPACE=${TAK_CLUSTER_NAME} + export KUBECONFIG=${TAK_KUBECONFIG_FILE} + +elif [[ "${TAK_DEPLOYMENT_TARGET}" == "generic-minikube" ]];then + echo Activating Generic Minikube configuration + export TAK_KUBERNETES_NAMESPACE=${TAK_CLUSTER_NAME} + +else + echo -e "\033[31;5mTAK_DEPLOYMENT_TARGET MUST BE SET TO 'aws', 'generic-rke2', or 'generic-minikube!\033[0m" + exit 1 +fi diff --git a/src/takserver-cluster/deployments/helm/developer-values.yaml b/src/takserver-cluster/deployments/helm/developer-values.yaml index 0a1b98ab..4a673858 100644 --- a/src/takserver-cluster/deployments/helm/developer-values.yaml +++ b/src/takserver-cluster/deployments/helm/developer-values.yaml @@ -73,19 +73,21 @@ takserver: storage: max: 50Gi min: 1Gi +global: + postgresql: + host: takserver-postgresql + port: "5432" + auth: + postgresqlUsername: postgres + postgresqlPassword: postgres + postgresqlDatabase: cot postgresql: enabled: true image: registry: docker.io repository: postgis/postgis - tag: 15-3.3 + tag: 15-3.4 configurationConfigMap: postgres-configuration - postgresqlDatabase: cot - -global: - postgresql: - postgresqlUsername: postgres - postgresqlPassword: postgres ignite: image: diff --git a/src/takserver-cluster/deployments/helm/production-values.yaml b/src/takserver-cluster/deployments/helm/production-values.yaml index 37f12075..84535769 100644 --- a/src/takserver-cluster/deployments/helm/production-values.yaml +++ b/src/takserver-cluster/deployments/helm/production-values.yaml @@ -92,15 +92,19 @@ postgresql: image: registry: docker.io repository: postgis/postgis - tag: 15-3.3 - configurationConfigMap: postgres-configuration - postgresqlDatabase: cot + tag: 15-3.4 + primary: + existingConfigmap: postgres-configuration global: postgresql: - postgresqlUsername: postgres - postgresqlPassword: postgres - + host: takserver-postgresql + port: "5432" + auth: + database: cot + password: postgres + postgresPassword: postgres + username: postgres ignite: image: tag: 2.15.0-jdk11 diff --git a/src/takserver-cluster/deployments/helm/templates/post-install-hook-postgres.yaml b/src/takserver-cluster/deployments/helm/templates/post-install-hook-postgres.yaml index cca65c9c..06386eed 100644 --- a/src/takserver-cluster/deployments/helm/templates/post-install-hook-postgres.yaml +++ b/src/takserver-cluster/deployments/helm/templates/post-install-hook-postgres.yaml @@ -3,7 +3,7 @@ apiVersion: batch/v1 kind: Job metadata: - name: "{{ .Release.Name }}" + name: "{{ .Release.Name }}-db-setup" labels: app.kubernetes.io/managed-by: {{ .Release.Service | quote }} app.kubernetes.io/instance: {{ .Release.Name | quote }} @@ -28,31 +28,24 @@ spec: "proxy.istio.io/config": '{ "holdApplicationUntilProxyStarts": true }' spec: restartPolicy: Never - initContainers: - - name: check-db-ready - image: postgres:15-alpine - command: ['sh', '-c', - 'until pg_isready -h $POSTGRES_HOST -p $POSTGRES_PORT; - do echo waiting for database; sleep 5; done;'] - env: - - name: POSTGRES_DB - value: {{ .Values.postgresql.postgresqlDatabase }} - - name: POSTGRES_USER - value: {{ .Values.global.postgresql.postgresqlUsername }} - - name: POSTGRES_PASSWORD - value: {{ .Values.global.postgresql.postgresqlPassword }} - - name: POSTGRES_HOST - value: takserver-postgresql - - name: POSTGRES_PORT - value: "5432" containers: - name: post-install-job image: "{{ .Values.takserver.takserverDatabaseSetup.image.repository }}:{{ .Values.takserver.takserverDatabaseSetup.image.tag }}" env: - name: 'region' - value: 'placeholder' + value: 'aws-rds-placeholder' - name: 'identifier' - value: 'identifier' + value: 'aws-rds-placeholder' + - name: POSTGRES_DB + value: {{ .Values.global.postgresql.auth.database }} + - name: POSTGRES_USER + value: {{ .Values.global.postgresql.auth.username }} + - name: POSTGRES_PASSWORD + value: {{ .Values.global.postgresql.auth.password }} + - name: POSTGRES_HOST + value: "{{ .Values.global.postgresql.host }}" + - name: POSTGRES_PORT + value: "{{ .Values.global.postgresql.port }}" imagePullSecrets: - name: {{ .Values.imagePullSecret }} {{- end }} diff --git a/src/takserver-cluster/deployments/helm/templates/quotas.yaml b/src/takserver-cluster/deployments/helm/templates/quotas.yaml index bfbd56a0..69a5dc95 100644 --- a/src/takserver-cluster/deployments/helm/templates/quotas.yaml +++ b/src/takserver-cluster/deployments/helm/templates/quotas.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: ResourceQuota metadata: name: storagequota - namespace: takserver + namespace: {{.Values.takserver.namespace}} spec: hard: persistentvolumeclaims: {{ .Values.takserver.limits.persistentVolumeClaims }} @@ -12,11 +12,11 @@ apiVersion: v1 kind: LimitRange metadata: name: storagelimits - namespace: takserver + namespace: {{.Values.takserver.namespace}} spec: limits: - max: storage: {{ .Values.takserver.limits.storage.max }} min: storage: {{ .Values.takserver.limits.storage.min }} - type: PersistentVolumeClaim \ No newline at end of file + type: PersistentVolumeClaim diff --git a/src/takserver-cluster/deployments/helm/templates/takserver-core-deployment.yaml b/src/takserver-cluster/deployments/helm/templates/takserver-core-deployment.yaml index 63e55a2c..8fb93f6e 100644 --- a/src/takserver-cluster/deployments/helm/templates/takserver-core-deployment.yaml +++ b/src/takserver-cluster/deployments/helm/templates/takserver-core-deployment.yaml @@ -19,6 +19,10 @@ spec: env: - name: "spring_profiles_active" value: "k8cluster" + - name: POSTGRES_HOST + value: "{{ .Values.global.postgresql.host }}" + - name: POSTGRES_PORT + value: "{{ .Values.global.postgresql.port }}" resources: requests: cpu: "{{ .Values.takserver.messaging.resources.requests.cpu }}" @@ -87,6 +91,11 @@ spec: containers: - name: takserver-api image: "{{ .Values.takserver.api.image.repository }}:{{ .Values.takserver.api.image.tag }}" + env: + - name: POSTGRES_HOST + value: "{{ .Values.global.postgresql.host }}" + - name: POSTGRES_PORT + value: "{{ .Values.global.postgresql.port }}" resources: requests: cpu: "{{ .Values.takserver.api.resources.requests.cpu }}" @@ -114,8 +123,8 @@ spec: command: - /api-readiness.sh initialDelaySeconds: 180 - periodSeconds: 10 - failureThreshold: 4 + periodSeconds: 30 + failureThreshold: 6 volumeMounts: - name: config #The name(key) value must match pod volumes name(key) value mountPath: /certs-configmap/ @@ -168,8 +177,8 @@ spec: tcpSocket: port: 10800 initialDelaySeconds: 20 - periodSeconds: 10 - failureThreshold: 4 + periodSeconds: 30 + failureThreshold: 6 volumeMounts: - name: config #The name(key) value must match pod volumes name(key) value mountPath: /certs-configmap/ diff --git a/src/takserver-cluster/deployments/helm/templates/takserver-plugins-deployment.yaml b/src/takserver-cluster/deployments/helm/templates/takserver-plugins-deployment.yaml index f0a10b26..854044b5 100644 --- a/src/takserver-cluster/deployments/helm/templates/takserver-plugins-deployment.yaml +++ b/src/takserver-cluster/deployments/helm/templates/takserver-plugins-deployment.yaml @@ -28,18 +28,12 @@ spec: volumeMounts: - name: config #The name(key) value must match pod volumes name(key) value mountPath: /certs-configmap/ - - name: core-config - mountPath: /CoreConfig.xml - subPath: CoreConfig.xml - name: tak-ignite-config mountPath: /TAKIgniteConfig.xml volumes: - name: config configMap: name: {{ .Values.certConfigMapName }} - - name: core-config - configMap: - name: {{ .Values.coreConfigMapName}} - name: tak-ignite-config configMap: name: {{ .Values.takIgniteConfigMapName }} diff --git a/src/takserver-cluster/deployments/ingress-infrastructure/default-ingress-setup.yaml b/src/takserver-cluster/deployments/ingress-infrastructure/default-ingress-setup.yaml new file mode 100644 index 00000000..02e25475 --- /dev/null +++ b/src/takserver-cluster/deployments/ingress-infrastructure/default-ingress-setup.yaml @@ -0,0 +1,63 @@ +apiVersion: v1 +kind: Service +metadata: + name: takserver-api-service +spec: + allocateLoadBalancerNodePorts: true + externalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + internalTrafficPolicy: Cluster + ports: + - name: cert-https + nodePort: 8443 + port: 8443 + protocol: TCP + targetPort: 8443 + - name: federation-truststore + nodePort: 8444 + port: 8444 + protocol: TCP + targetPort: 8444 + - name: https + nodePort: 8446 + port: 8446 + protocol: TCP + targetPort: 8446 + selector: + app: takserver-api + sessionAffinity: None + type: LoadBalancer +--- +apiVersion: v1 +kind: Service +metadata: + name: takserver-messaging-service +spec: + allocateLoadBalancerNodePorts: true + externalTrafficPolicy: Cluster + ipFamilies: + - IPv4 + ipFamilyPolicy: SingleStack + internalTrafficPolicy: Cluster + ports: + - name: tls + nodePort: 8089 + port: 8089 + protocol: TCP + targetPort: 8089 + - name: federation-v1 + nodePort: 9000 + port: 9000 + protocol: TCP + targetPort: 9000 + - name: federation-v2 + nodePort: 9001 + port: 9001 + protocol: TCP + targetPort: 9001 + selector: + app: takserver-messaging + sessionAffinity: None + type: LoadBalancer \ No newline at end of file diff --git a/src/takserver-cluster/docker-files/Dockerfile.ca b/src/takserver-cluster/docker-files/Dockerfile.ca index ec09744a..19b0e04d 100644 --- a/src/takserver-cluster/docker-files/Dockerfile.ca +++ b/src/takserver-cluster/docker-files/Dockerfile.ca @@ -1,17 +1,5 @@ # need java for keytool FROM eclipse-temurin:17-jammy COPY takserver-core/certs/* / -ARG ARG_CA_NAME -ENV CA_NAME=$ARG_CA_NAME -ARG ARG_STATE -ENV STATE=$ARG_STATE -ARG ARG_CITY -ENV CITY=$ARG_CITY -ARG ARG_ORGANIZATIONAL_UNIT -ENV ORGANIZATIONAL_UNIT=$ARG_ORGANIZATIONAL_UNIT -RUN apt update && \ - apt install -y apt-utils && \ - apt install -y openssl && \ - apt install -y vim && \ - ./generateClusterCerts.sh -CMD ["bash"] +RUN apt update && apt install -y apt-utils openssl vim +CMD ["bash", "generateClusterCerts.sh"] diff --git a/src/takserver-cluster/docker-files/Dockerfile.database-setup b/src/takserver-cluster/docker-files/Dockerfile.database-setup index 1a879f3e..e76e88fa 100644 --- a/src/takserver-cluster/docker-files/Dockerfile.database-setup +++ b/src/takserver-cluster/docker-files/Dockerfile.database-setup @@ -1,9 +1,10 @@ FROM eclipse-temurin:17-jammy + +# Netcat is necessary to check if the remote server is open for use. +# This could probably be replaced by a "-wait" parameter or something similar in SchemaManager.jar +RUN apt update -y && apt install -y netcat + COPY takserver-schemamanager/SchemaManager.jar ./ COPY takserver-schemamanager/generic-cluster-database-configuration.sh ./ -COPY CoreConfig.xml opt/tak/CoreConfig.xml -COPY takserver-schemamanager/db-connection-configuration.sh . RUN chmod +x generic-cluster-database-configuration.sh -RUN chmod +x db-connection-configuration.sh -RUN ./db-connection-configuration.sh -CMD ["./generic-cluster-database-configuration.sh"] +CMD ["./generic-cluster-database-configuration.sh"] \ No newline at end of file diff --git a/src/takserver-cluster/docker-files/Dockerfile.takserver-config b/src/takserver-cluster/docker-files/Dockerfile.takserver-config index 8770752a..773bbec9 100644 --- a/src/takserver-cluster/docker-files/Dockerfile.takserver-config +++ b/src/takserver-cluster/docker-files/Dockerfile.takserver-config @@ -1,4 +1,5 @@ ARG TAKSERVER_IMAGE_REPO=docker-devtest-local.artifacts.tak.gov/takserver-cluster/takserver-base ARG TAKSERVER_IMAGE_TAG=core-base FROM ${TAKSERVER_IMAGE_REPO}:${TAKSERVER_IMAGE_TAG} +RUN apt update && apt install -y postgresql-client inetutils-ping socat wget curl vim CMD ["sh", "takserver-config-cluster.sh"] diff --git a/src/takserver-cluster/eks-cluster.yaml b/src/takserver-cluster/eks-cluster.yaml index 728ed947..67f6178a 100644 --- a/src/takserver-cluster/eks-cluster.yaml +++ b/src/takserver-cluster/eks-cluster.yaml @@ -13,6 +13,7 @@ managedNodeGroups: instanceType: c5.4xlarge name: cluster-node-group volumeSize: 100 + volumeEncrypted: true metadata: name: cluster region: us-east-1 diff --git a/src/takserver-cluster/scripts/build-eks.py b/src/takserver-cluster/scripts/build-eks.py index 7e6fbef1..d55456be 100644 --- a/src/takserver-cluster/scripts/build-eks.py +++ b/src/takserver-cluster/scripts/build-eks.py @@ -1,4 +1,6 @@ import argparse +import shutil + import boto3 import time import subprocess @@ -18,6 +20,10 @@ _parser.add_argument('--validate-deployment', action='store_true', help='Validates the helm files using a currently connected k8s instance or minikube') +if 'TAK_DEPLOYMENT_TARGET' in os.environ and os.environ['TAK_DEPLOYMENT_TARGET'] != 'aws': + print(f'TAK_DEPLOYMENT_TARGET is not set to "aws"! Ensure all required variables are set in cluster-properties ' + + 'and it has been sourced prior to running this script!') + exit(1) # AWS CREDS AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID'] @@ -29,22 +35,17 @@ # https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.DBInstanceClass.html#Concepts.DBInstanceClass.Support # https://docs.aws.amazon.com/AmazonRDS/latest/PostgreSQLReleaseNotes/postgresql-versions.html to see supported versions KUBERNETES_SERVER_VERSION = '1.27' -POSTGRES_GROUP_FAMILY='postgres15' -POSTGRES_ENGINE_VERSION='15.3' +POSTGRES_GROUP_FAMILY = 'postgres15' +POSTGRES_ENGINE_VERSION = '15.7' CLUSTER_HOME_DIR = os.environ['CLUSTER_HOME_DIR'] TAK_CLUSTER_NAME = os.environ['TAK_CLUSTER_NAME'] TAK_CLUSTER_ZONES = os.environ['TAK_CLUSTER_ZONES'] TAK_CLUSTER_DOMAIN_NAME = os.environ['TAK_CLUSTER_DOMAIN_NAME'] TAK_CLUSTER_REGION = os.environ['TAK_CLUSTER_REGION'] -TAK_CLUSTER_NODE_COUNT = os.environ['TAK_CLUSTER_NODE_COUNT'] +TAK_CLUSTER_NODE_COUNT = int(os.environ['TAK_CLUSTER_NODE_COUNT']) AWS_ECR_URI = '' TAK_PLUGINS = os.environ['TAK_PLUGINS'] -# How many pods to multiply the count by for all tak services -TAK_POD_MULTIPLIER = 3 -# The percentage of pods to allocate to the messaging service -TAK_POD_MESSAGING_PERCENTAGE = .667 - # RDS DB Config TAK_DB_USERNAME = os.environ['TAK_DB_USERNAME'] TAK_DB_PASSWORD = os.environ['TAK_DB_PASSWORD'] @@ -62,721 +63,797 @@ HELM_DIRECTORY = CLUSTER_HOME_DIR + '/deployments/helm' PROPERTIES_FILE = HELM_DIRECTORY + '/production-values.yaml' -CERT_CONFIGMAP_FILE = os.environ['CERT_CONFIGMAP_FILE'] +TAK_CERT_SOURCE_DIR = os.environ['TAK_CERT_SOURCE_DIR'] + +# These values define how many of each node type will be instantiated for each api node with at least 1 being spun up +TAK_MESSAGING_NODE_MULTIPLIER = 2 +TAK_IGNITE_NODE_MULTIPLIER = 0.4 +TAK_NATS_NODE_MULTIPLIER = 0.4 +TAK_CONFIG_NODE_MULTIPLIER = 0.2 +TAK_LOAD_BALANCER_NODE_MULTIPLIER = 0.2 + +# Required certificates. If the user provides their own certs and these aren't present deployment will halt +REQUIRED_CERTIFICATES = [ + 'takserver.jks', + 'truststore-root.jks', + 'fed-truststore.jks', + 'admin.pem' +] + +# A global object to reflect the pod counts in use +class PodCounts: + api = TAK_CLUSTER_NODE_COUNT + messaging = max(1, int(round(TAK_CLUSTER_NODE_COUNT * TAK_MESSAGING_NODE_MULTIPLIER))) + config = max(1, int(round(TAK_CLUSTER_NODE_COUNT * TAK_CONFIG_NODE_MULTIPLIER))) + plugin = TAK_PLUGINS + loadbalancer = max(1, int(round(TAK_CLUSTER_NODE_COUNT * TAK_LOAD_BALANCER_NODE_MULTIPLIER))) + ignite = max(1, int(round(TAK_CLUSTER_NODE_COUNT * TAK_IGNITE_NODE_MULTIPLIER))) + nats = max(1, int(round(TAK_CLUSTER_NODE_COUNT * TAK_NATS_NODE_MULTIPLIER))) + def load_yaml(filename): - _yaml = [] - - with open(filename) as f: - docs = yaml.load_all(f, Loader=yaml.FullLoader) - for doc in docs: - _yaml.append(doc) + _yaml = [] + + with open(filename) as f: + docs = yaml.load_all(f, Loader=yaml.FullLoader) + for doc in docs: + _yaml.append(doc) + + return _yaml - return _yaml def save_yaml(filename, _yaml): - with open(filename, 'w') as file: - yaml.dump_all(_yaml, file, default_flow_style=False) + with open(filename, 'w') as file: + yaml.dump_all(_yaml, file, default_flow_style=False) + # Run A Command Line Argument, Logging Output As We Go def runCmd(cmd): - p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE) - while True: - out = p.stderr.read(1) - - if out == b'' and p.poll() != None: - break - - # It isn't clear why building the Dockerfile.rds spit out a few bytes that couldn't be decoded. but given - # everything has worked fine for years I'm assuming a bad byte is being generated by the docker command and - # there isn't really a good solution for that (which happened during `pip3 install --upgrade awscli`). - if out != b'' and out != b'\xe2' and out != b'\x94' and out != b'\x81': - sys.stdout.write(out.decode('utf-8')) - sys.stdout.flush() - - return p.poll() + p = subprocess.Popen(cmd, shell=True, stderr=subprocess.PIPE) + while True: + out = p.stderr.read(1) + + if out == b'' and p.poll() != None: + break + + # It isn't clear why building the Dockerfile.rds spit out a few bytes that couldn't be decoded. but given + # everything has worked fine for years I'm assuming a bad byte is being generated by the docker command and + # there isn't really a good solution for that (which happened during `pip3 install --upgrade awscli`). + if out != b'' and out != b'\xe2' and out != b'\x94' and out != b'\x81': + sys.stdout.write(out.decode('utf-8')) + sys.stdout.flush() + + return p.poll() + # Print Out Formatted Json -def printJson(j) : - print (json.dumps(j,sort_keys=True, indent=4, default=str)) +def printJson(j): + print(json.dumps(j, sort_keys=True, indent=4, default=str)) + -# Setup AWS ECR. If Exists, Skip. Otherwise, Create. +# Setup AWS ECR. If Exists, Skip. Otherwise, Create. # Once ECR Is Available, Write URI To The Cluster Properties File (Will Override Any Existing AWS_ECR_URI) def setupECR(): - global AWS_ECR_URI - try: - find_ecr_res = boto3.client('ecr', region_name=TAK_CLUSTER_REGION).describe_repositories(repositoryNames=['tak/server-' + TAK_CLUSTER_NAME]) - printJson(find_ecr_res) - AWS_ECR_URI = find_ecr_res['repositories'][0]['repositoryUri'] - print('\nECR Already Exists At ' + AWS_ECR_URI + ' , Skipping Creation..') - - except botocore.exceptions.ClientError as e: - create_ecr_res = boto3.client('ecr').create_repository(repositoryName='tak/server-' + TAK_CLUSTER_NAME) - AWS_ECR_URI = create_ecr_res['repository']['repositoryUri'] - print('ECR created at ' + AWS_ECR_URI) - - while True: - try: - boto3.client('ecr', region_name=TAK_CLUSTER_REGION).describe_repositories(repositoryNames=['tak/server-' + TAK_CLUSTER_NAME]) - print('ECR is Available') - break - - except botocore.exceptions.ClientError as e: - print('Waiting for ECR to become available. Checking again in 15 seconds') - time.sleep(15) - - try: - ecr_uri_exists = False - - properties_file = fileinput.input(files=CLUSTER_HOME_DIR +'/cluster-properties', inplace=1) - for line in properties_file: - if 'export AWS_ECR_URI=' in line: - line = 'export AWS_ECR_URI=' + AWS_ECR_URI + "\n" - ecr_uri_exists = True - print (line, end ="") - properties_file.close() - - if not ecr_uri_exists: - with open(CLUSTER_HOME_DIR +'/cluster-properties', "a") as properties_file: - properties_file.write('export AWS_ECR_URI=' + AWS_ECR_URI + "\n") - - except: - print('\nEXITING: Could not open ' + CLUSTER_HOME_DIR +'/cluster-properties') - sys.exit(1) + global AWS_ECR_URI + try: + find_ecr_res = boto3.client('ecr', region_name=TAK_CLUSTER_REGION).describe_repositories( + repositoryNames=['tak/server-' + TAK_CLUSTER_NAME]) + printJson(find_ecr_res) + AWS_ECR_URI = find_ecr_res['repositories'][0]['repositoryUri'] + print('\nECR Already Exists At ' + AWS_ECR_URI + ' , Skipping Creation..') + + except botocore.exceptions.ClientError as e: + create_ecr_res = boto3.client('ecr').create_repository(repositoryName='tak/server-' + TAK_CLUSTER_NAME) + AWS_ECR_URI = create_ecr_res['repository']['repositoryUri'] + print('ECR created at ' + AWS_ECR_URI) + + while True: + try: + boto3.client('ecr', region_name=TAK_CLUSTER_REGION).describe_repositories( + repositoryNames=['tak/server-' + TAK_CLUSTER_NAME]) + print('ECR is Available') + break + + except botocore.exceptions.ClientError as e: + print('Waiting for ECR to become available. Checking again in 15 seconds') + time.sleep(15) + + try: + ecr_uri_exists = False + + properties_file = fileinput.input(files=CLUSTER_HOME_DIR + '/cluster-properties', inplace=1) + for line in properties_file: + if 'export AWS_ECR_URI=' in line: + line = 'export AWS_ECR_URI=' + AWS_ECR_URI + "\n" + ecr_uri_exists = True + print(line, end="") + properties_file.close() + + if not ecr_uri_exists: + with open(CLUSTER_HOME_DIR + '/cluster-properties', "a") as properties_file: + properties_file.write('export AWS_ECR_URI=' + AWS_ECR_URI + "\n") + + except: + print('\nEXITING: Could not open ' + CLUSTER_HOME_DIR + '/cluster-properties') + sys.exit(1) def modEksClusterConfig(): - eks_yaml = load_yaml(CLUSTER_CONFIG_FILE) + eks_yaml = load_yaml(CLUSTER_CONFIG_FILE) - for _yaml in eks_yaml: - _yaml['metadata']['name'] = TAK_CLUSTER_NAME - _yaml['metadata']['region'] = TAK_CLUSTER_REGION - _yaml['metadata']['version'] = KUBERNETES_SERVER_VERSION - _yaml['availabilityZones'] = TAK_CLUSTER_ZONES.split(',') + for _yaml in eks_yaml: + _yaml['metadata']['name'] = TAK_CLUSTER_NAME + _yaml['metadata']['region'] = TAK_CLUSTER_REGION + _yaml['metadata']['version'] = KUBERNETES_SERVER_VERSION + _yaml['availabilityZones'] = TAK_CLUSTER_ZONES.split(',') - for entry in _yaml['managedNodeGroups'][0]: - if entry == 'desiredCapacity': - _yaml['managedNodeGroups'][0][entry] = int(TAK_CLUSTER_NODE_COUNT) - - save_yaml(CLUSTER_CONFIG_FILE, eks_yaml) + for entry in _yaml['managedNodeGroups'][0]: + if entry == 'desiredCapacity': + _yaml['managedNodeGroups'][0][entry] = PodCounts.api -def adjustDeploymentsByNumberOfNodes(): - max_tak_pods = int(TAK_CLUSTER_NODE_COUNT) * TAK_POD_MULTIPLIER + save_yaml(CLUSTER_CONFIG_FILE, eks_yaml) - # Negating one to account for configuration pod - total_msg = math.floor(max_tak_pods * TAK_POD_MESSAGING_PERCENTAGE - 1) - total_loadbalancer = int(round(total_msg / 5, 0)) - loadbalancer_yaml = load_yaml(LOAD_BALANCER_DEPLOYMENT_FILE) +def adjustDeploymentsByNumberOfNodes(): + loadbalancer_yaml = load_yaml(LOAD_BALANCER_DEPLOYMENT_FILE) + + for _yaml in loadbalancer_yaml: + if _yaml['kind'] == 'Deployment': + _yaml['spec']['replicas'] = PodCounts.loadbalancer - for _yaml in loadbalancer_yaml: - if _yaml['kind'] == 'Deployment': - _yaml['spec']['replicas'] = total_loadbalancer + save_yaml(LOAD_BALANCER_DEPLOYMENT_FILE, loadbalancer_yaml) - save_yaml(LOAD_BALANCER_DEPLOYMENT_FILE, loadbalancer_yaml) # Setup The Cluster Using Kops. Skip Setup If Cluster Exists And Is Valid. def setupCluster(): - modEksClusterConfig() - adjustDeploymentsByNumberOfNodes() + modEksClusterConfig() + adjustDeploymentsByNumberOfNodes() - if runCmd('eksctl get cluster --name ' + TAK_CLUSTER_NAME) == 0: - print('Cluster Exists, Skipping Setup..\n') - else: - cluster_create_status = runCmd('eksctl create cluster -f ' + CLUSTER_CONFIG_FILE) - runCmd('kubectl create namespace takserver') + if runCmd('eksctl get cluster --name ' + TAK_CLUSTER_NAME) == 0: + print('Cluster Exists, Skipping Setup..\n') + else: + cluster_create_status = runCmd('eksctl create cluster -f ' + CLUSTER_CONFIG_FILE) + runCmd('kubectl create namespace takserver') + + if cluster_create_status != 0: + sys.exit(1) - if cluster_create_status != 0: - sys.exit(1) - # Validate KubeCtl API Is Ready def validateKubeCtl(): - kubectl_valid = False - while not kubectl_valid : - - if runCmd('kubectl cluster-info') == 0: - print ('Kubectl Is Now Valid!') - kubectl_valid = True - - else: - print ("\nKubeCtl Not Ready. Trying Again In 15 Seconds\n") - time.sleep(15) + kubectl_valid = False + while not kubectl_valid: + + if runCmd('kubectl cluster-info') == 0: + print('Kubectl Is Now Valid!') + kubectl_valid = True + + else: + print("\nKubeCtl Not Ready. Trying Again In 15 Seconds\n") + time.sleep(15) # Generate And Return An MD5 Hash def generateDBHash(): - return hashlib.md5((TAK_DB_PASSWORD + TAK_DB_USERNAME).encode('utf-8')).hexdigest() + return hashlib.md5((TAK_DB_PASSWORD + TAK_DB_USERNAME).encode('utf-8')).hexdigest() + # Setup RDS DB Parameter group with 50000 max connections def setupDBParameterGroups(): - try: - find_db_param_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_parameter_groups(DBParameterGroupName='takserver-rds-pg15') - printJson(find_db_param_group_res) - print('\nParameter Group Exists') - except botocore.exceptions.ClientError as e: - create_db_param_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).create_db_parameter_group( - DBParameterGroupName='takserver-rds-pg15', - DBParameterGroupFamily=POSTGRES_GROUP_FAMILY, - Description='Takserver RDS parameter group for ' + POSTGRES_GROUP_FAMILY - ) - - print('\nCreating RDS Parameter Group') - printJson(create_db_param_group_res) - - modify_db_param_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).modify_db_parameter_group( - DBParameterGroupName='takserver-rds-pg15', - Parameters=[ - { - 'ParameterName': 'max_connections', - 'ParameterValue': '50000', - 'ApplyMethod': 'pending-reboot' - }, - ] - ) - - print('\nModifying RDS Parameter Group') - printJson(modify_db_param_group_res) + try: + find_db_param_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_parameter_groups( + DBParameterGroupName='takserver-rds-pg15') + printJson(find_db_param_group_res) + print('\nParameter Group Exists') + except botocore.exceptions.ClientError as e: + create_db_param_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).create_db_parameter_group( + DBParameterGroupName='takserver-rds-pg15', + DBParameterGroupFamily=POSTGRES_GROUP_FAMILY, + Description='Takserver RDS parameter group for ' + POSTGRES_GROUP_FAMILY + ) + + print('\nCreating RDS Parameter Group') + printJson(create_db_param_group_res) + + modify_db_param_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).modify_db_parameter_group( + DBParameterGroupName='takserver-rds-pg15', + Parameters=[ + { + 'ParameterName': 'max_connections', + 'ParameterValue': '50000', + 'ApplyMethod': 'pending-reboot' + }, + ] + ) + + print('\nModifying RDS Parameter Group') + printJson(modify_db_param_group_res) + # Setup A Subnet Group For RDS DB. Skip Creation If Exists def setupSubnetGroups(): - try: - find_db_subnet_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_subnet_groups(DBSubnetGroupName=TAK_CLUSTER_NAME + '-SG') - printJson (find_db_subnet_group_res) - print ('\nSubnet Security Group Already Exists') - - except botocore.exceptions.ClientError as e: - - try: - find_db_subnets_res = boto3.client('ec2', region_name=TAK_CLUSTER_REGION).describe_subnets(Filters=[{'Name': 'tag:aws:cloudformation:stack-name','Values': ['eksctl-' + TAK_CLUSTER_NAME + '-cluster']}]) - - subnets = [] - for subnet in find_db_subnets_res['Subnets']: - subnets.append(subnet['SubnetId']) - - create_db_subnet_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).create_db_subnet_group(DBSubnetGroupName=TAK_CLUSTER_NAME + '-SG',DBSubnetGroupDescription=TAK_CLUSTER_NAME + '-SG',SubnetIds=subnets) - - printJson(create_db_subnet_group_res) - print ('\nSubnet Security Group Created') - - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) + try: + find_db_subnet_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_subnet_groups( + DBSubnetGroupName=TAK_CLUSTER_NAME + '-SG') + printJson(find_db_subnet_group_res) + print('\nSubnet Security Group Already Exists') + + except botocore.exceptions.ClientError as e: + + try: + find_db_subnets_res = boto3.client('ec2', region_name=TAK_CLUSTER_REGION).describe_subnets(Filters=[ + {'Name': 'tag:aws:cloudformation:stack-name', 'Values': ['eksctl-' + TAK_CLUSTER_NAME + '-cluster']}]) + + subnets = [] + for subnet in find_db_subnets_res['Subnets']: + subnets.append(subnet['SubnetId']) + + create_db_subnet_group_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).create_db_subnet_group( + DBSubnetGroupName=TAK_CLUSTER_NAME + '-SG', DBSubnetGroupDescription=TAK_CLUSTER_NAME + '-SG', + SubnetIds=subnets) + + printJson(create_db_subnet_group_res) + print('\nSubnet Security Group Created') + + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + # Find And Return Cluster Security Group -def describeSecurityGroups() : - try: - find_security_groups_res = boto3.client('ec2', region_name=TAK_CLUSTER_REGION).describe_security_groups(Filters=[{'Name': 'tag:aws:cloudformation:stack-name','Values': ['eksctl-' + TAK_CLUSTER_NAME + '-cluster']}]) - - groups = [] +def describeSecurityGroups(): + try: + find_security_groups_res = boto3.client('ec2', region_name=TAK_CLUSTER_REGION).describe_security_groups( + Filters=[ + {'Name': 'tag:aws:cloudformation:stack-name', 'Values': ['eksctl-' + TAK_CLUSTER_NAME + '-cluster']}]) + + groups = [] - for group in find_security_groups_res['SecurityGroups']: - groups.append(group['GroupId']) + for group in find_security_groups_res['SecurityGroups']: + groups.append(group['GroupId']) + + return groups + + except botocore.exceptions.ClientError as e: + print('\nEXITING: Could Not Find Cluster Security Group') + sys.exit(1) - return groups - - except botocore.exceptions.ClientError as e: - print ('\nEXITING: Could Not Find Cluster Security Group') - sys.exit(1) # Create RDS DB Is It Does Not Exist. Modify Core Config With RDS Credentials def setupRDS(): - setupDBParameterGroups() - setupSubnetGroups() - - rds_dns = '' - while not rds_dns : - - try: - find_db_instances_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_instances(DBInstanceIdentifier=TAK_CLUSTER_NAME) - rds_dns = find_db_instances_res['DBInstances'][0]['Endpoint']['Address'] - printJson(find_db_instances_res) - print('\nDatabase Exists At ' + rds_dns) - - except botocore.exceptions.ClientError as e: - - try: - create_db_instance_res = (boto3.client('rds', region_name=TAK_CLUSTER_REGION) - .create_db_instance( - DBName='cot', - DBInstanceIdentifier=TAK_CLUSTER_NAME, - AllocatedStorage=int(TAK_DB_ALLOCATED_STORAGE), - DBInstanceClass=TAK_DB_SIZE, - Engine='postgres', - MasterUsername=TAK_DB_USERNAME, - MasterUserPassword='md5' + generateDBHash(), - VpcSecurityGroupIds=describeSecurityGroups(), - DBSubnetGroupName=TAK_CLUSTER_NAME + '-SG', - EngineVersion=POSTGRES_ENGINE_VERSION, - DBParameterGroupName='takserver-rds-pg15', - PubliclyAccessible=False - )) - printJson(create_db_instance_res) - print('\nDatabase Is Now Creating... ') - - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) - - except KeyError as e: - print ('\nDatabase Does Not Have An Endpoint Yet.. Trying again in 60 Seconds') - time.sleep(60) - - try: - core_config = xml.etree.ElementTree.parse(CLUSTER_HOME_DIR +'/CoreConfig.xml') - xml.etree.ElementTree.register_namespace('', 'http://bbn.com/marti/xml/config') - xml.etree.ElementTree.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance') - - for repository in core_config.getroot().findall('{http://bbn.com/marti/xml/config}repository'): - for connection in repository.findall('{http://bbn.com/marti/xml/config}connection'): - connection.set('url', 'jdbc:postgresql://' + rds_dns + ':5432/cot') - connection.set('username', TAK_DB_USERNAME) - connection.set('password', TAK_DB_PASSWORD) - - core_config.write(CLUSTER_HOME_DIR +'/CoreConfig.xml') - print('\nCoreConfig successfully modified to include RDS credentials') - - except: - print ('\nError writing to CoreConfig.xml...') - sys.exit(1) + setupDBParameterGroups() + setupSubnetGroups() + + rds_dns = '' + while not rds_dns: + + try: + find_db_instances_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_instances( + DBInstanceIdentifier=TAK_CLUSTER_NAME) + rds_dns = find_db_instances_res['DBInstances'][0]['Endpoint']['Address'] + printJson(find_db_instances_res) + print('\nDatabase Exists At ' + rds_dns) + + except botocore.exceptions.ClientError as e: + + try: + create_db_instance_res = (boto3.client('rds', region_name=TAK_CLUSTER_REGION) + .create_db_instance( + DBName='cot', + DBInstanceIdentifier=TAK_CLUSTER_NAME, + AllocatedStorage=int(TAK_DB_ALLOCATED_STORAGE), + DBInstanceClass=TAK_DB_SIZE, + Engine='postgres', + MasterUsername=TAK_DB_USERNAME, + MasterUserPassword='md5' + generateDBHash(), + VpcSecurityGroupIds=describeSecurityGroups(), + DBSubnetGroupName=TAK_CLUSTER_NAME + '-SG', + EngineVersion=POSTGRES_ENGINE_VERSION, + DBParameterGroupName='takserver-rds-pg15', + PubliclyAccessible=False + )) + printJson(create_db_instance_res) + print('\nDatabase Is Now Creating... ') + + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + + except KeyError as e: + print('\nDatabase Does Not Have An Endpoint Yet.. Trying again in 60 Seconds') + time.sleep(60) + + try: + core_config = xml.etree.ElementTree.parse(CLUSTER_HOME_DIR + '/CoreConfig.xml') + xml.etree.ElementTree.register_namespace('', 'http://bbn.com/marti/xml/config') + xml.etree.ElementTree.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance') + + for repository in core_config.getroot().findall('{http://bbn.com/marti/xml/config}repository'): + for connection in repository.findall('{http://bbn.com/marti/xml/config}connection'): + connection.set('url', 'jdbc:postgresql://' + rds_dns + ':5432/cot') + connection.set('username', TAK_DB_USERNAME) + connection.set('password', TAK_DB_PASSWORD) + + core_config.write(CLUSTER_HOME_DIR + '/CoreConfig.xml') + print('\nCoreConfig successfully modified to include RDS credentials') + + except: + print('\nError writing to CoreConfig.xml...') + sys.exit(1) + # Check The Status Of The RDS Setup Pod def checkDBSetupDeploymentStatus(): - deployment_succeeded_code = runCmd("kubectl get pod --field-selector=status.phase=Succeeded | grep takserver-database-setup") - deployment_failed_code = runCmd("kubectl get pod --field-selector=status.phase=Failed | grep takserver-database-setup") - - if deployment_succeeded_code == 0: - print ('\nDatabase Setup Pod Already Exists And Succeeded.. Skipping Setup (delete this pod if you want to run/rerun RDS schema setup)\n') - return 0 - - elif deployment_failed_code == 0: - print ('\nDatabase Setup Pod Already Exists But Failed.. Deleting and Deploying..\n') - return 1 - - else: - print ('\nNo Databse Setup Exists.. Creating\n') - return -1 + deployment_succeeded_code = runCmd( + "kubectl get pod --field-selector=status.phase=Succeeded | grep takserver-database-setup") + deployment_failed_code = runCmd( + "kubectl get pod --field-selector=status.phase=Failed | grep takserver-database-setup") + + if deployment_succeeded_code == 0: + print( + '\nDatabase Setup Pod Already Exists And Succeeded.. Skipping Setup (delete this pod if you want to run/rerun RDS schema setup)\n') + return 0 + + elif deployment_failed_code == 0: + print('\nDatabase Setup Pod Already Exists But Failed.. Deleting and Deploying..\n') + return 1 + + else: + print('\nNo Databse Setup Exists.. Creating\n') + return -1 + def checkDocker(): - check_docker_cmd = 'docker ps' - check_docker_cmd_status = runCmd(check_docker_cmd) - - if check_docker_cmd_status == 0: - print("\nDocker is running.. continuing") - return - else : - if platform == "linux" or platform == "linux2": - check_docker_cmd = 'systemctl restart docker' - runCmd(check_docker_cmd) - - check_docker_cmd = 'docker-machine restart && eval "$(docker-machine env default)"' - runCmd(check_docker_cmd) - elif platform == "darwin": - check_docker_cmd = 'docker-machine restart && eval "$(docker-machine env default)"' - runCmd(check_docker_cmd) - elif platform == "win32" or platform == "cygwin": - print('Windows Host') - - time.sleep(5) - - check_docker_cmd_status = runCmd(check_docker_cmd) - - if check_docker_cmd_status == 0: - print("\nDocker is running.. continuing") - else: - print("\n** Could not programatically start docker. Please do it manually and rerun the script. Run 'docker ps' to verify status") - sys.exit(1) + check_docker_cmd = 'docker ps' + check_docker_cmd_status = runCmd(check_docker_cmd) + + if check_docker_cmd_status == 0: + print("\nDocker is running.. continuing") + return + else: + if platform == "linux" or platform == "linux2": + check_docker_cmd = 'systemctl restart docker' + runCmd(check_docker_cmd) + + check_docker_cmd = 'docker-machine restart && eval "$(docker-machine env default)"' + runCmd(check_docker_cmd) + elif platform == "darwin": + check_docker_cmd = 'docker-machine restart && eval "$(docker-machine env default)"' + runCmd(check_docker_cmd) + elif platform == "win32" or platform == "cygwin": + print('Windows Host') + + time.sleep(5) + + check_docker_cmd_status = runCmd(check_docker_cmd) + + if check_docker_cmd_status == 0: + print("\nDocker is running.. continuing") + else: + print( + "\n** Could not programatically start docker. Please do it manually and rerun the script. Run 'docker ps' to verify status") + sys.exit(1) + # Run The Databse Setup Pod And Wait For It To Finish def deployDatabaseSetupPod(): - db_pod_status_code = checkDBSetupDeploymentStatus() - if db_pod_status_code == 0: - return - - elif db_pod_status_code == 1: - runCmd("kubectl delete pod takserver-database-setup") - - init_DB_cmd = 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':database-setup -f docker-files/Dockerfile.rds . && docker push ' + AWS_ECR_URI + ':database-setup && kubectl run takserver-database-setup --image=' + AWS_ECR_URI + ':database-setup --image-pull-policy Always --restart Never --env="AWS_SECRET_ACCESS_KEY=' + AWS_SECRET_ACCESS_KEY + '" --env="AWS_ACCESS_KEY_ID=' + AWS_ACCESS_KEY_ID + '" --env="region=' + TAK_CLUSTER_REGION + '" --env="identifier=' + TAK_CLUSTER_NAME + '"' - - if runCmd(init_DB_cmd) == 0: - print ('\nDeployed Database Setup Pod\n') - - else: - print('\nUnable to deploy database pod...') - sys.exit(1) - - while True: - db_deployment_failed_code = runCmd("kubectl get pod --field-selector=status.phase=Failed | grep takserver-database-setup") - db_deployment_succeeded_code = runCmd("kubectl get pod --field-selector=status.phase=Succeeded | grep takserver-database-setup") - if db_deployment_succeeded_code == 0: - print ('\nSchemaManager has successfully setup Cot Database\n') - break - - if db_deployment_failed_code == 0: - print ('\nSomething went wrong. Schema manager failed to setup db.\n') - sys.exit(1) - - print ('Schema Manager Still Setting Up DB.. checking again in 30 seconds\n') - time.sleep(30) + db_pod_status_code = checkDBSetupDeploymentStatus() + if db_pod_status_code == 0: + return + + elif db_pod_status_code == 1: + runCmd("kubectl delete pod takserver-database-setup") + + init_DB_cmd = 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':database-setup -f docker-files/Dockerfile.rds . && docker push ' + AWS_ECR_URI + ':database-setup && kubectl run takserver-database-setup --image=' + AWS_ECR_URI + ':database-setup --image-pull-policy Always --restart Never --env="AWS_SECRET_ACCESS_KEY=' + AWS_SECRET_ACCESS_KEY + '" --env="AWS_ACCESS_KEY_ID=' + AWS_ACCESS_KEY_ID + '" --env="region=' + TAK_CLUSTER_REGION + '" --env="identifier=' + TAK_CLUSTER_NAME + '"' + + if runCmd(init_DB_cmd) == 0: + print('\nDeployed Database Setup Pod\n') + + else: + print('\nUnable to deploy database pod...') + sys.exit(1) + + while True: + db_deployment_failed_code = runCmd( + "kubectl get pod --field-selector=status.phase=Failed | grep takserver-database-setup") + db_deployment_succeeded_code = runCmd( + "kubectl get pod --field-selector=status.phase=Succeeded | grep takserver-database-setup") + if db_deployment_succeeded_code == 0: + print('\nSchemaManager has successfully setup Cot Database\n') + break + + if db_deployment_failed_code == 0: + print('\nSomething went wrong. Schema manager failed to setup db.\n') + sys.exit(1) + + print('Schema Manager Still Setting Up DB.. checking again in 30 seconds\n') + time.sleep(30) + # Deploy Ingress def deployIngress(): - deploy_ingress_cmd = 'cd ' + CLUSTER_HOME_DIR + '/deployments/ingress-infrastructure && kubectl config set-context $(kubectl config current-context) --namespace=takserver && kubectl apply -f ingress-setup.yaml' - if runCmd(deploy_ingress_cmd) == 0: - print('Ingress Setup Deployed Successfully\n') - - else: - print('Ingress Setup Already Deployed.. Skipping\n') + deploy_ingress_cmd = 'cd ' + CLUSTER_HOME_DIR + '/deployments/ingress-infrastructure && kubectl config set-context $(kubectl config current-context) --namespace=takserver && kubectl apply -f ingress-setup.yaml' + if runCmd(deploy_ingress_cmd) == 0: + print('Ingress Setup Deployed Successfully\n') + + else: + print('Ingress Setup Already Deployed.. Skipping\n') + # Deploy Load Balancer def deployLoadBalancer(): - cmd = 'cd ' + CLUSTER_HOME_DIR + ' && kubectl create -f ' + CLUSTER_HOME_DIR + '/deployments/ingress-infrastructure/load-balancer-deployment.yaml' - cmd_status = runCmd(cmd) - - if cmd_status == 0: - print("\nLoad balancer deployed") - else: - print("\nLoad balancer was not deployed") + cmd = 'cd ' + CLUSTER_HOME_DIR + ' && kubectl create -f ' + CLUSTER_HOME_DIR + '/deployments/ingress-infrastructure/load-balancer-deployment.yaml' + cmd_status = runCmd(cmd) + + if cmd_status == 0: + print("\nLoad balancer deployed") + else: + print("\nLoad balancer was not deployed") + +def validateInputData(): + if TAK_CERT_SOURCE_DIR != '': + if not os.path.isdir(TAK_CERT_SOURCE_DIR): + print(f"The specified value for TAK_CERT_SOURCE_DIR {TAK_CERT_SOURCE_DIR} does not exist!", file=sys.stderr) + exit(1) + + for cert in REQUIRED_CERTIFICATES: + if not os.path.isfile(os.path.join(TAK_CERT_SOURCE_DIR, cert)): + print(f'Although the certificate directory {TAK_CERT_SOURCE_DIR} exists it does not contain ' + f'the required certificate {cert}!', file=sys.stderr) + exit(1) # Generate Cluster Certificaties If They Dont Exist def generateTakseverCertificates(): - if not os.path.isdir(CLUSTER_HOME_DIR + '/takserver-core/certs/files') or not os.listdir(CLUSTER_HOME_DIR + '/takserver-core/certs/files'): - if not TAK_CA_NAME or not TAK_CITY or not TAK_STATE or not TAK_ORGANIZATIONAL_UNIT: - printJson('No certs found and cert metadata not defined in cluster-properties. please update cluster-properties, resource the file, and try rebuilding') - sys.exit(1) + if TAK_CERT_SOURCE_DIR != '': + shutil.copytree(TAK_CERT_SOURCE_DIR, CLUSTER_HOME_DIR + '/takserver-core/certs/files') + + if not os.path.isdir(CLUSTER_HOME_DIR + '/takserver-core/certs/files') or not len(os.listdir( + CLUSTER_HOME_DIR + '/takserver-core/certs/files')) > 0: + if not TAK_CA_NAME or not TAK_CITY or not TAK_STATE or not TAK_ORGANIZATIONAL_UNIT: + printJson( + 'No certs found and cert metadata not defined in cluster-properties. please update cluster-properties, resource the file, and try rebuilding') + sys.exit(1) + + runCmd( + 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ca-setup -f docker-files/Dockerfile.ca . && docker run -it --name ca-setup-container --env CA_NAME=' + TAK_CA_NAME + ' --env STATE=' + TAK_STATE + ' --env CITY=' + TAK_CITY + ' --env ORGANIZATIONAL_UNIT=' + TAK_ORGANIZATIONAL_UNIT + ' ca-setup > /dev/null && docker cp ca-setup-container:/files ' + CLUSTER_HOME_DIR + '/takserver-core/certs/ && docker rm ca-setup-container') + print( + '\nNew certificates with default password: "atakatak" generated at: \n\n' + CLUSTER_HOME_DIR + '/takserver-core/certs/files.') - runCmd('cd ' + CLUSTER_HOME_DIR + ' && docker build -t ca-setup --build-arg ARG_CA_NAME=' + TAK_CA_NAME + ' --build-arg ARG_STATE=' + TAK_STATE + ' --build-arg ARG_CITY=' + TAK_CITY + ' --build-arg ARG_ORGANIZATIONAL_UNIT=' + TAK_ORGANIZATIONAL_UNIT + ' -f docker-files/Dockerfile.ca . && docker create -it --name ca-setup-container ca-setup && docker cp ca-setup-container:/files ' + CLUSTER_HOME_DIR + '/takserver-core/certs/ && docker rm ca-setup-container') - print('\nNew certificates with default password: "atakatak" generated at: \n\n' + CLUSTER_HOME_DIR + '/takserver-core/certs/files.') - - else: - print('\nEXISTING CERTIFICATES FOUND AT \n\n' + CLUSTER_HOME_DIR + '/takserver-core/certs/files\n\nNOT GENERATING NEW ONES...') + else: + print( + '\nEXISTING CERTIFICATES FOUND AT \n\n' + CLUSTER_HOME_DIR + '/takserver-core/certs/files\n\nNOT GENERATING NEW ONES...') - config_map_cmd = 'kubectl create configmap cert-migration --from-file="' + CLUSTER_HOME_DIR + '/takserver-core/certs/files' + '" --dry-run=client -o yaml >' + CLUSTER_HOME_DIR + '/deployments/helm/templates/cert-migration.yaml' - runCmd(config_map_cmd) + config_map_cmd = 'kubectl create configmap cert-migration --from-file="' + CLUSTER_HOME_DIR + '/takserver-core/certs/files' + '" --dry-run=client -o yaml >' + CLUSTER_HOME_DIR + '/deployments/helm/templates/cert-migration.yaml' + runCmd(config_map_cmd) def addCoreConfigMap(): - fp = os.path.join(CLUSTER_HOME_DIR, 'CoreConfig.xml') - if not os.path.isfile(fp): - printJson('No CoreConfig.xml found. It should be located in the root of the cluster directory!') - sys.exit(1) + fp = os.path.join(CLUSTER_HOME_DIR, 'CoreConfig.xml') + if not os.path.isfile(fp): + printJson('No CoreConfig.xml found. It should be located in the root of the cluster directory!') + sys.exit(1) + + print('Loading CoreConfig.xml found in cluster root into the cluster ConfigMap.') + config_map_cmd = 'kubectl create configmap core-config --from-file="' + fp + '" --dry-run=client -o yaml >' + CLUSTER_HOME_DIR + '/deployments/helm/templates/core-config.yaml' + runCmd(config_map_cmd) - print('Loading CoreConfig.xml found in cluster root into the cluster ConfigMap.') - config_map_cmd = 'kubectl create configmap core-config --from-file="' + fp + '" --dry-run=client -o yaml >' + CLUSTER_HOME_DIR + '/deployments/helm/templates/core-config.yaml' - runCmd(config_map_cmd) def addIgniteConfigMap(): - fp = os.path.join(CLUSTER_HOME_DIR, 'TAKIgniteConfig.xml') - if not os.path.isfile(fp): - printJson('No TAKIgniteConfig.xml found. It should be located in the root of the cluster directory!') - sys.exit(1) + fp = os.path.join(CLUSTER_HOME_DIR, 'TAKIgniteConfig.xml') + if not os.path.isfile(fp): + printJson('No TAKIgniteConfig.xml found. It should be located in the root of the cluster directory!') + sys.exit(1) + + print('Loading TAKIgniteConfig.xml found in cluster root into the cluster ConfigMap.') + config_map_cmd = 'kubectl create configmap tak-ignite-config --from-file="' + fp + '" --dry-run=client -o yaml >' + CLUSTER_HOME_DIR + '/deployments/helm/templates/tak-ignite-config.yaml' + runCmd(config_map_cmd) - print('Loading TAKIgniteConfig.xml found in cluster root into the cluster ConfigMap.') - config_map_cmd = 'kubectl create configmap tak-ignite-config --from-file="' + fp + '" --dry-run=client -o yaml >' + CLUSTER_HOME_DIR + '/deployments/helm/templates/tak-ignite-config.yaml' - runCmd(config_map_cmd) # Deploy Takserver Core def publishTakserverCoreImages(): - base_cmd = ('cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':core-base -f docker-files/Dockerfile.takserver-base . && docker push ' + AWS_ECR_URI + ':core-base') - - cmd_status = runCmd(base_cmd) + base_cmd = ( + 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':core-base -f docker-files/Dockerfile.takserver-base . && docker push ' + AWS_ECR_URI + ':core-base') + + cmd_status = runCmd(base_cmd) - if cmd_status == 0: - build_dependent_dockerfiles_cmd = ('cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':messaging-provisioned -f docker-files/Dockerfile.takserver-messaging --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' .' - + ' && docker build -t ' + AWS_ECR_URI + ':api-provisioned -f docker-files/Dockerfile.takserver-api --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' .' - + ' && docker build -t ' + AWS_ECR_URI + ':config-provisioned -f docker-files/Dockerfile.takserver-config --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' .' - + ' && docker push ' + AWS_ECR_URI + ' --all-tags') + if cmd_status == 0: + build_dependent_dockerfiles_cmd = ( + 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':messaging-provisioned -f docker-files/Dockerfile.takserver-messaging --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' .' + + ' && docker build -t ' + AWS_ECR_URI + ':api-provisioned -f docker-files/Dockerfile.takserver-api --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' .' + + ' && docker build -t ' + AWS_ECR_URI + ':config-provisioned -f docker-files/Dockerfile.takserver-config --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' .' + + ' && docker push ' + AWS_ECR_URI + ' --all-tags') - build_dependent_dockerfiles_cmd_status = runCmd(build_dependent_dockerfiles_cmd) + build_dependent_dockerfiles_cmd_status = runCmd(build_dependent_dockerfiles_cmd) + + if build_dependent_dockerfiles_cmd_status == 0: + print("\nTakserver Base, API, & Messaging images were published") + else: + print("\nTakserver Base, API, & Messaging images were NOT published") + else: + print("\nTakserver Base image was not published") - if build_dependent_dockerfiles_cmd_status == 0: - print("\nTakserver Base, API, & Messaging images were published") - else: - print("\nTakserver Base, API, & Messaging images were NOT published") - else: - print("\nTakserver Base image was not published") # Deploy Takserver Plugins def publishTakserverPluginImages(): - cmd = 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':plugins-provisioned -f docker-files/Dockerfile.takserver-plugins --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' . && docker push ' + AWS_ECR_URI + ' --all-tags' - cmd_status = runCmd(cmd) + cmd = 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':plugins-provisioned -f docker-files/Dockerfile.takserver-plugins --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' . && docker push ' + AWS_ECR_URI + ' --all-tags' + cmd_status = runCmd(cmd) - if cmd_status == 0: - print("\nTakserver Plugins deployed") - else: - print("\nTakserver Services was not deployed") + if cmd_status == 0: + print("\nTakserver Plugins deployed") + else: + print("\nTakserver Services was not deployed") def publishIntegrationTestImages(): - cmd = 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':integrationtests-provisioned -f docker-files/Dockerfile.takserver-integrationtests --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' . && docker push ' + AWS_ECR_URI + ' --all-tags' - cmd_status = runCmd(cmd) + cmd = 'cd ' + CLUSTER_HOME_DIR + ' && docker build -t ' + AWS_ECR_URI + ':integrationtests-provisioned -f docker-files/Dockerfile.takserver-integrationtests --build-arg TAKSERVER_IMAGE_REPO=' + AWS_ECR_URI + ' . && docker push ' + AWS_ECR_URI + ' --all-tags' + cmd_status = runCmd(cmd) - if cmd_status == 0: - print("\nTakserver integreation tests deployed") - else: - print("\nTakserver integration tests was not deployed") + if cmd_status == 0: + print("\nTakserver integreation tests deployed") + else: + print("\nTakserver integration tests was not deployed") # Find Cluster Load Balancer.. Wait Until It's Found AND Active. Return Its DNS def getLoadBalancerDNS(): - dns = '' - load_balancer_active = False - - while not load_balancer_active: - try: - load_balancers = boto3.client('elbv2', region_name=TAK_CLUSTER_REGION).describe_load_balancers() - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) - - for load_balancer in load_balancers['LoadBalancers']: - try: - cluster_load_balancer = boto3.client('elbv2', region_name=TAK_CLUSTER_REGION).describe_tags(ResourceArns=[load_balancer['LoadBalancerArn']]) - - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) - - for tag_description in cluster_load_balancer['TagDescriptions']: - for tag in tag_description['Tags']: - tag_key = tag['Key'] - if tag_key == 'kubernetes.io/cluster/' + TAK_CLUSTER_NAME: - dns = load_balancer['DNSName'] - if load_balancer['State']['Code'] == 'active': - load_balancer_active = True - printJson(load_balancer) - print('\nLoad Balancer is now Active at ' + dns) - - if not load_balancer_active: - print('Load Balancer not Active yet. Checking again in 60 seconds') - time.sleep(60) - - return dns + dns = '' + load_balancer_active = False + + while not load_balancer_active: + try: + load_balancers = boto3.client('elbv2', region_name=TAK_CLUSTER_REGION).describe_load_balancers() + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + + for load_balancer in load_balancers['LoadBalancers']: + try: + cluster_load_balancer = boto3.client('elbv2', region_name=TAK_CLUSTER_REGION).describe_tags( + ResourceArns=[load_balancer['LoadBalancerArn']]) + + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + + for tag_description in cluster_load_balancer['TagDescriptions']: + for tag in tag_description['Tags']: + tag_key = tag['Key'] + if tag_key == 'kubernetes.io/cluster/' + TAK_CLUSTER_NAME: + dns = load_balancer['DNSName'] + if load_balancer['State']['Code'] == 'active': + load_balancer_active = True + printJson(load_balancer) + print('\nLoad Balancer is now Active at ' + dns) + + if not load_balancer_active: + print('Load Balancer not Active yet. Checking again in 60 seconds') + time.sleep(60) + + return dns + # Get The Cluster Hosted Zone - Return Its ID def getHostedZoneId(): - zone_id = '' - try: - zones = boto3.client('route53', region_name=TAK_CLUSTER_REGION).list_hosted_zones() - - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) - - for zone in zones['HostedZones']: - if zone['Name'] == TAK_CLUSTER_DOMAIN_NAME + '.': - zone_id = zone['Id'] - printJson(zone) - print('\nFound Hosted Zone') - break - - return zone_id + zone_id = '' + try: + zones = boto3.client('route53', region_name=TAK_CLUSTER_REGION).list_hosted_zones() + + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + + for zone in zones['HostedZones']: + if zone['Name'] == TAK_CLUSTER_DOMAIN_NAME + '.': + zone_id = zone['Id'] + printJson(zone) + print('\nFound Hosted Zone') + break + + return zone_id + # Create a CNAME For the Load Balancers DNS def createCNAMEForLoadBalancerDNS(zone, dns): - if not dns: - print('\nCould not find load balancer DNS') - sys.exit(1) - - if not zone: - print('\nCould not find hosted zone matching ' + TAK_CLUSTER_DOMAIN_NAME) - sys.exit(1) - - try: - create_resource_record_sets_res = boto3.client('route53', region_name=TAK_CLUSTER_REGION).change_resource_record_sets( - HostedZoneId=zone, - ChangeBatch={ - 'Changes': [ - { - 'Action': 'UPSERT', - 'ResourceRecordSet': { - 'Name': TAK_CLUSTER_NAME + '.' + TAK_CLUSTER_DOMAIN_NAME, - 'Type': 'CNAME', - 'TTL': 5, - 'ResourceRecords': [{'Value': dns}] - } - }, - ] - } - ) - - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) - - printJson(create_resource_record_sets_res) - print('\nUpserted CNAME Record Set') + if not dns: + print('\nCould not find load balancer DNS') + sys.exit(1) + + if not zone: + print('\nCould not find hosted zone matching ' + TAK_CLUSTER_DOMAIN_NAME) + sys.exit(1) + + try: + create_resource_record_sets_res = boto3.client('route53', + region_name=TAK_CLUSTER_REGION).change_resource_record_sets( + HostedZoneId=zone, + ChangeBatch={ + 'Changes': [ + { + 'Action': 'UPSERT', + 'ResourceRecordSet': { + 'Name': TAK_CLUSTER_NAME + '.' + TAK_CLUSTER_DOMAIN_NAME, + 'Type': 'CNAME', + 'TTL': 5, + 'ResourceRecords': [{'Value': dns}] + } + }, + ] + } + ) + + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + + printJson(create_resource_record_sets_res) + print('\nUpserted CNAME Record Set') + # Test That CNAME Resolves def testTakseverCNAME(zone): - dns_is_ready = False - domain_name = '' - while not dns_is_ready: - try: - test_dns_answer_res = boto3.client('route53', region_name=TAK_CLUSTER_REGION).test_dns_answer( - HostedZoneId=zone, - RecordName=TAK_CLUSTER_NAME + '.' + TAK_CLUSTER_DOMAIN_NAME, - RecordType='CNAME' - ) - if test_dns_answer_res['ResponseCode'] != 'NOERROR': - print('\nCNAME status is ' + test_dns_answer_res['ResponseCode'] + ', checking again in 30 seconds') - time.sleep(30) - - else: - dns_is_ready = True - printJson(test_dns_answer_res) - print('\nTakserver DNS is now active ' + test_dns_answer_res['RecordName'] ) - domain_name = test_dns_answer_res['RecordName'] - - except botocore.exceptions.ClientError as e: - printJson(e.response['Error']) - sys.exit(1) - - return domain_name + dns_is_ready = False + domain_name = '' + while not dns_is_ready: + try: + test_dns_answer_res = boto3.client('route53', region_name=TAK_CLUSTER_REGION).test_dns_answer( + HostedZoneId=zone, + RecordName=TAK_CLUSTER_NAME + '.' + TAK_CLUSTER_DOMAIN_NAME, + RecordType='CNAME' + ) + if test_dns_answer_res['ResponseCode'] != 'NOERROR': + print('\nCNAME status is ' + test_dns_answer_res['ResponseCode'] + ', checking again in 30 seconds') + time.sleep(30) + + else: + dns_is_ready = True + printJson(test_dns_answer_res) + print('\nTakserver DNS is now active ' + test_dns_answer_res['RecordName']) + domain_name = test_dns_answer_res['RecordName'] + + except botocore.exceptions.ClientError as e: + printJson(e.response['Error']) + sys.exit(1) + + return domain_name + # Print Takserver Entrypoints Defined In CoreConfig def printTakserverEntryPoints(domain_name): - try: - core_config = xml.etree.ElementTree.parse(CLUSTER_HOME_DIR + '/CoreConfig.xml') - xml.etree.ElementTree.register_namespace('', 'http://bbn.com/marti/xml/config') - xml.etree.ElementTree.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance') - - for network in core_config.getroot().findall('{http://bbn.com/marti/xml/config}network'): - for input in network.findall('{http://bbn.com/marti/xml/config}input'): - print ('\nClients can now connect to Taksever at ' + domain_name + ':' + input.get('port') + ' using protocol ' - + input.get('protocol') + ' for ' + input.get('_name')) - - for connector in network.findall('{http://bbn.com/marti/xml/config}connector'): - http = 'https://' if "https" in connector.get('_name') else 'http://' - print ('\nTakserver can now be reached at ' + http - + domain_name + ':' + connector.get('port') + ' for ' + connector.get('_name')) - - except: - print ('\nError reading entrypoints from CoreConfig.xml...') - sys.exit(1) + try: + core_config = xml.etree.ElementTree.parse(CLUSTER_HOME_DIR + '/CoreConfig.xml') + xml.etree.ElementTree.register_namespace('', 'http://bbn.com/marti/xml/config') + xml.etree.ElementTree.register_namespace('xsi', 'http://www.w3.org/2001/XMLSchema-instance') -def validate_helm_deployment(): - if subprocess.run(['kubectl', 'cluster-info']).returncode != 0: - if subprocess.run(['which', 'minikube']).returncode == 0: - if subprocess.run(['minikube', 'status']).returncode != 0: - subprocess.run(['minikube', 'stop']) - subprocess.run(['minikube', 'delete']) - subprocess.run([ - 'minikube', 'start', '--memory=2048m', '--cpus=2', - '--kubernetes-version=v1.21.14', '--insecure-registry=127.0.0.1:5000' - ], check=True) + for network in core_config.getroot().findall('{http://bbn.com/marti/xml/config}network'): + for input in network.findall('{http://bbn.com/marti/xml/config}input'): + print('\nClients can now connect to Taksever at ' + domain_name + ':' + input.get( + 'port') + ' using protocol ' + + input.get('protocol') + ' for ' + input.get('_name')) - else: - raise Exception('Kubectl is not currently connected to a cluster and no minikube installation found!') + for connector in network.findall('{http://bbn.com/marti/xml/config}connector'): + http = 'https://' if "https" in connector.get('_name') else 'http://' + print('\nTakserver can now be reached at ' + http + + domain_name + ':' + connector.get('port') + ' for ' + connector.get('_name')) - installHelmChart(True) + except: + print('\nError reading entrypoints from CoreConfig.xml...') + sys.exit(1) -def installHelmChart(dry_run=False): - max_tak_pods = int(TAK_CLUSTER_NODE_COUNT) * TAK_POD_MULTIPLIER - # Negating one to account for configuration pod - total_msg = math.floor(max_tak_pods * TAK_POD_MESSAGING_PERCENTAGE - 1) +def validate_helm_deployment(): + if subprocess.run(['kubectl', 'cluster-info']).returncode != 0: + if subprocess.run(['which', 'minikube']).returncode == 0: + if subprocess.run(['minikube', 'status']).returncode != 0: + subprocess.run(['minikube', 'stop']) + subprocess.run(['minikube', 'delete']) + subprocess.run([ + 'minikube', 'start', '--memory=2048m', '--cpus=2', + '--kubernetes-version=v1.21.14', '--insecure-registry=127.0.0.1:5000' + ], check=True) + + else: + raise Exception('Kubectl is not currently connected to a cluster and no minikube installation found!') - # If plugins are enabled negate one more pod from the msg process to account for it - if TAK_PLUGINS == '1': - total_msg = total_msg - 1 + installHelmChart(True) - # Allocate the remaining pods to api minus one for the config service - total_api = max_tak_pods - total_msg - 1 - total_ignite = max(1, int(round(total_msg / 5, 0))) - total_nats = max(1, int(round(total_msg / 5, 0))) - password = runCmd("aws ecr get-login-password") - configmap = 'cert-migration' +def installHelmChart(dry_run=False): + password = runCmd("aws ecr get-login-password") + + find_db_instances_res = boto3.client('rds', region_name=TAK_CLUSTER_REGION).describe_db_instances( + DBInstanceIdentifier=TAK_CLUSTER_NAME) + rds_dns = find_db_instances_res['DBInstances'][0]['Endpoint']['Address'] - if dry_run: - print('Skipping kubectl configuration for dry-run...') - else: - if CERT_CONFIGMAP_FILE != '': - runCmd('kubectl create -f cert-migration-replacement.yaml -n takserver') - configmap = 'cert-migration-replacement' + configmap = 'cert-migration' - runCmd('kubectl delete secret -n takserver reg-cred --ignore-not-found') - runCmd('kubectl create secret -n takserver docker-registry reg-cred \ + if dry_run: + print('Skipping kubectl configuration for dry-run...') + else: + runCmd('kubectl delete secret -n takserver reg-cred --ignore-not-found') + runCmd('kubectl create secret -n takserver docker-registry reg-cred \ --docker-server=$AWS_ACCOUNT_ID.dkr.ecr.$TAK_CLUSTER_REGION.amazonaws.com \ --docker-username=AWS \ --docker-password=' + str(password) + '\ --namespace=takserver') - runCmd('helm dependency update ' + HELM_DIRECTORY) - runCmd(('helm upgrade' + (' --dry-run' if dry_run else '') - + ' --install -n takserver takserver ' + HELM_DIRECTORY - + ' -f ' + PROPERTIES_FILE - + ' --set certConfigMapName=' + configmap - + ' --set takserver.plugins.enabled=' + str(TAK_PLUGINS == '1') - + ' --set takserver.config.replicas=1' - + ' --set takserver.messaging.replicas=' + str(total_msg) - + ' --set takserver.api.replicas=' + str(total_api) - + ' --set takserver.config.image.repository=' + AWS_ECR_URI - + ' --set takserver.messaging.image.repository=' + AWS_ECR_URI - + ' --set takserver.api.image.repository=' + AWS_ECR_URI - + ' --set takserver.plugins.image.repository=' + AWS_ECR_URI - + ' --set takserver.integrationtests.image.repository=' + AWS_ECR_URI - + ' --set ignite.replicaCount=' + str(total_ignite) - + ' --set nats.cluster.replicas=' + str(total_nats) - + ' --set stan.cluster.replicas=' + str(total_nats))) + runCmd('helm dependency update ' + HELM_DIRECTORY) + runCmd(('helm upgrade' + (' --dry-run' if dry_run else '') + + ' --install -n takserver takserver ' + HELM_DIRECTORY + + ' -f ' + PROPERTIES_FILE + + ' --set certConfigMapName=' + configmap + + ' --set takserver.plugins.enabled=' + str(PodCounts.plugin) + + ' --set takserver.config.replicas=' + str(PodCounts.config) + + ' --set takserver.messaging.replicas=' + str(PodCounts.messaging) + + ' --set takserver.api.replicas=' + str(PodCounts.api) + + ' --set takserver.config.image.repository=' + AWS_ECR_URI + + ' --set takserver.messaging.image.repository=' + AWS_ECR_URI + + ' --set takserver.api.image.repository=' + AWS_ECR_URI + + ' --set takserver.plugins.image.repository=' + AWS_ECR_URI + + ' --set takserver.integrationtests.image.repository=' + AWS_ECR_URI + + ' --set ignite.replicaCount=' + str(PodCounts.ignite) + + ' --set nats.cluster.replicas=' + str(PodCounts.nats) + + ' --set stan.cluster.replicas=' + str(PodCounts.nats) + + ' --set global.postgresql.host=' + rds_dns + + ' --set global.postgresql.port=5432')) if __name__ == '__main__': - args = _parser.parse_args() - - if args.validate_deployment: - validate_helm_deployment() - - else: - print("\n---------- Running AWS ECR Setup Commands ----------") - setupECR() - print("\n---------- Running AWS Cluster Initialization Commands ----------") - setupCluster() - print("\n---------- Running KubeCtl Validation Commands ----------") - validateKubeCtl() - print("\n---------- Running AWS RDS Setup Commands ----------") - setupRDS() - print("\n---------- Running Docker Check ----------") - checkDocker() - print("\n---------- Running AWS RDS Deployment Commands ----------") - deployDatabaseSetupPod() - print("\n---------- Running nginx Ingress Setup and Deployment Commands ----------") - deployIngress() - print("\n---------- Running Docker Check ----------") - checkDocker() - print("\n---------- Running AWS Load Balancer Service Commands ----------") - deployLoadBalancer() - print("\n---------- Running Takserver Certificate Generation Commands ----------") - generateTakseverCertificates() - print("\n---------- Adding CoreConfigMap ----------") - addCoreConfigMap() - print("\n---------- Adding IgniteConfigMap ----------") - addIgniteConfigMap() - print("\n---------- Publishing Takserver Core Docker Images ----------") - publishTakserverCoreImages() - if TAK_PLUGINS == '1': - print("\n---------- Publishing Takserver Plugin Docker Images ----------") - publishTakserverPluginImages() - else: - print("\n---------- Plugins Disabled, Plugin Docker Image Publications ----------") - if os.path.exists('docker-files/Dockerfile.takserver-integrationtests'): - print("\n---------- Publishing Takserver Integration Tests Docker Images ----------") - publishIntegrationTestImages() - print("\n---------- Deploying services to cluster ----------") - installHelmChart() - print("\n---------- Running AWS Get Load Balancer DNS Commands ----------") - dns = getLoadBalancerDNS() - - if TAK_CLUSTER_DOMAIN_NAME != None and TAK_CLUSTER_DOMAIN_NAME != '': - print("\n---------- Running AWS Get Hosted Zone ID Commands ----------") - zone = getHostedZoneId() - print("\n---------- Running AWS Create CNAME Record Set Commands ----------") - createCNAMEForLoadBalancerDNS(zone, dns) - print("\n---------- Running AWS Test CNAME DNS Commands ----------") - domain_name = testTakseverCNAME(zone) - print("\n---------- Running TAK Entrypoint Commands ----------") - printTakserverEntryPoints(domain_name) - else: - print("\n---------- Running TAK Entrypoint Commands ----------") - printTakserverEntryPoints(dns) + args = _parser.parse_args() + + if args.validate_deployment: + validate_helm_deployment() + + else: + print("\n---------- Validating Input Data ----------") + validateInputData() + print("\n---------- Running AWS ECR Setup Commands ----------") + setupECR() + print("\n---------- Running AWS Cluster Initialization Commands ----------") + setupCluster() + print("\n---------- Running KubeCtl Validation Commands ----------") + validateKubeCtl() + print("\n---------- Running AWS RDS Setup Commands ----------") + setupRDS() + print("\n---------- Running Docker Check ----------") + checkDocker() + print("\n---------- Running AWS RDS Deployment Commands ----------") + deployDatabaseSetupPod() + print("\n---------- Running nginx Ingress Setup and Deployment Commands ----------") + deployIngress() + print("\n---------- Running Docker Check ----------") + checkDocker() + print("\n---------- Running AWS Load Balancer Service Commands ----------") + deployLoadBalancer() + print("\n---------- Running Takserver Certificate Generation Commands ----------") + generateTakseverCertificates() + print("\n---------- Adding CoreConfigMap ----------") + addCoreConfigMap() + print("\n---------- Adding IgniteConfigMap ----------") + addIgniteConfigMap() + print("\n---------- Publishing Takserver Core Docker Images ----------") + publishTakserverCoreImages() + if TAK_PLUGINS == '1': + print("\n---------- Publishing Takserver Plugin Docker Images ----------") + publishTakserverPluginImages() + else: + print("\n---------- Plugins Disabled, Plugin Docker Image Publications ----------") + if os.path.exists('docker-files/Dockerfile.takserver-integrationtests'): + print("\n---------- Publishing Takserver Integration Tests Docker Images ----------") + publishIntegrationTestImages() + print("\n---------- Deploying services to cluster ----------") + installHelmChart() + print("\n---------- Running AWS Get Load Balancer DNS Commands ----------") + dns = getLoadBalancerDNS() + + if TAK_CLUSTER_DOMAIN_NAME != None and TAK_CLUSTER_DOMAIN_NAME != '': + print("\n---------- Running AWS Get Hosted Zone ID Commands ----------") + zone = getHostedZoneId() + print("\n---------- Running AWS Create CNAME Record Set Commands ----------") + createCNAMEForLoadBalancerDNS(zone, dns) + print("\n---------- Running AWS Test CNAME DNS Commands ----------") + domain_name = testTakseverCNAME(zone) + print("\n---------- Running TAK Entrypoint Commands ----------") + printTakserverEntryPoints(domain_name) + else: + print("\n---------- Running TAK Entrypoint Commands ----------") + printTakserverEntryPoints(dns) diff --git a/src/takserver-cluster/scripts/build-generic.py b/src/takserver-cluster/scripts/build-generic.py new file mode 100644 index 00000000..7498e72c --- /dev/null +++ b/src/takserver-cluster/scripts/build-generic.py @@ -0,0 +1,1048 @@ +#!/usr/bin/env python3 + +import argparse +import datetime +import json +import os +import pathlib +import platform +import shutil +import stat +import subprocess +import sys +import time +import urllib.request +import xml.etree.ElementTree as ET +from enum import Enum +from typing import Optional, Union, List, Dict + +import yaml + +# URLs to fetch deployment dependencies +KUBCTL_URL = 'https://dl.k8s.io/release/{version}/bin/{os}/{arch}/kubectl' +MINIKUBE_URL = 'https://github.com/kubernetes/minikube/releases/download/{version}/minikube-{os}-{arch}' +HELM_URL = 'https://get.helm.sh/helm-{version}-{os}-{arch}.tar.gz' + +# These values define how many of each node type will be instantiated for each api node with at least 1 being spun up +# TODO: Utilize ignite/nats scaling +TAK_MESSAGING_NODE_MULTIPLIER = 2 +TAK_IGNITE_NODE_MULTIPLIER = 0.4 +TAK_NATS_NODE_MULTIPLIER = 0.4 +TAK_CONFIG_NODE_MULTIPLIER = 0.2 +TAK_LOAD_BALANCER_NODE_MULTIPLIER = 0.2 +TAK_API_NODE_MULTIPLIER = 1 +TAK_PLUGIN_NODE_MULTIPLIER = 0 +TAK_DATABASE_SETUP_MULTIPLIER = 0 + +# Required certificates. If the user provides their own certs and these aren't present deployment will halt +REQUIRED_CERTIFICATES = [ + 'takserver.jks', + 'truststore-root.jks', + 'fed-truststore.jks', + 'admin.pem' +] + +# If certs are not provided this container will be temporarily deployed locally to generate them +CA_CONTAINER_NAME = 'tak-ca-setup' + +SCRIPT_DIR = os.path.dirname(os.path.realpath(__file__)) +CLUSTER_ROOT = os.path.dirname(SCRIPT_DIR.rstrip('/')) + +# The file where the command execution details will be logged to +COMMAND_LOG_FILE = os.path.join(CLUSTER_ROOT, os.path.realpath(__file__).split(os.sep)[-1]).rstrip('.py') + '.log' +COMMAND_LOG_FILE_RELATIVE_PATH = COMMAND_LOG_FILE.replace(CLUSTER_ROOT + '/', '') +DEFAULT_INGRESS_FILE = os.path.join(CLUSTER_ROOT, 'deployments/ingress-infrastructure/default-ingress-setup.yaml') + +_parser = argparse.ArgumentParser('TAKServer AWS Cluster Deployer') + + +def halt_on_failure(msg: str, error_code: int = 1, silence_failure: bool = False): + """ + Halts deployment with the provided message being made clearly visible + :param msg: The message to display + :param error_code: The error code to halt with. Defaults to 1. + :param silence_failure: Do not display the error to the user. + """ + if not silence_failure: + print(f'\033[31;5mA FATAL ERROR OCCURRED!!\033[0m', file=sys.stderr) + print(f'\033[31m{msg}\033[0m', file=sys.stderr) + exit(error_code) + + +def parse_env_var(env_var: str, value_type: type, required: bool) -> Union[None, str, int]: + """ + Convenience method for parsing env vars + :param env_var: The env var tag + :param value_type: The value's expected type + :param required: Whether the env var is required + """ + try: + return value_type(os.environ[env_var]) + except KeyError: + if required: + halt_on_failure(f'Environment variable {env_var} must be set!') + else: + return None + except ValueError: + halt_on_failure(f'Environment variable {env_var} must be a {value_type.__name__}!') + + +def env_int_req(env_var: str) -> int: + """ + Parses required integer env var + :param env_var: The env var key + :return: THe integer value + """ + return parse_env_var(env_var, int, True) + + +def env_int_opt(env_var: str) -> Optional[int]: + """ + Parses optional integer env var + :param env_var: The env var key + :return: The Integer value if present, or None if it is not + """ + return parse_env_var(env_var, int, False) + + +def env_str_req(env_var: str) -> str: + """ + Parses required str env var + :param env_var: The env var key + :return: The str value + """ + return parse_env_var(env_var, str, True) + + +def env_bool_req(env_var: str) -> bool: + """ + Parses required boolean env var + :param env_var: The env var key + :return: The boolean value + """ + value_str = env_str_req(env_var).lower() + if (value_str == 'true' or value_str == 'yes' or value_str == '1' or value_str == 't' or value_str == 'y'): + return True + if (value_str == 'false' or value_str == 'no' or value_str == '0' or value_str == 'f' or value_str == 'n'): + return False + halt_on_failure(f"Unexpected boolean value '{value_str}'! Expected true, false, yes, or no!") + + +def env_enum_req(env_var: str, enum_class: Enum): + """ + Parses an enum value from an env var + :param env_var: The env var key + :param enum_class: The expected enum type + :return: The enum value + """ + value_str = env_str_req(env_var) + label_dict = {target.value: target for target in list(enum_class)} + if value_str in label_dict: + return label_dict[value_str] + else: + halt_on_failure( + f'Environment variable {env_var} must be a one of the following values: [' + + ','.join(label_dict.keys()) + ']!') + + +def env_str_opt(env_var: str) -> Optional[str]: + """ + Parses optional str env var + :param env_var: The env var key + :return: The str value if present, None if it is not + """ + return parse_env_var(env_var, str, False) + + +def pretty_print_dict_list(dict_list: List[Dict], headers: List[str] = None): + if headers is None: + headers = sorted(dict_list[0].keys()) + + column_lengths = dict() + for header in headers: + column_lengths[header] = len(header) + + for row in dict_list: + column_lengths[header] = max(column_lengths[header], len(str(row[header]))) + + header_columns = list() + for header in headers: + header_columns.append(header.ljust(column_lengths[header], ' ')) + rows = [' '.join(header_columns)] + + for row_data in dict_list: + row_columns = list() + for header in headers: + row_columns.append(str(row_data[header]).ljust(column_lengths[header], ' ')) + rows.append(' '.join(row_columns)) + + for row in rows: + print(row) + + +class DeploymentTarget(Enum): + """ + The deployment targets + """ + AWS = 'aws' + RKE2 = 'generic-rke2' + MINIKUBE = 'generic-minikube' + + @classmethod + def from_label(cls, label: str) -> Union[None, 'DeploymentTarget']: + label_dict = {target.value: target for target in set(DeploymentTarget)} + return label_dict[label] if label in label_dict else None + + +class CertConfiguration: + """ + An object to store the certificate configuration + """ + + def __init__(self): + if 'TAK_CERT_SOURCE_DIR' in os.environ and os.environ['TAK_CERT_SOURCE_DIR'] != '': + # If the cert dir is set use that + self.src_dir = os.path.abspath(os.environ['TAK_CERT_SOURCE_DIR']) + # Validate the directory exists + if not os.path.exists(self.src_dir): + halt_on_failure(f'Could not find TAK_CERT_SOURCE_DIR {self.src_dir}!') + # Validate the admin cert exists + if not os.path.exists(os.path.join(self.src_dir, 'admin.pem')): + halt_on_failure(f'The TAK_CERT_SOURCE_DIR must contain an "admin.pem" cert for administration!') + self.ca_name = None + self.ca_state = None + self.ca_city = None + self.ca_organization = None + else: + # Otherwise we will need to generate the certificates + self.src_dir = None + self.ca_name = env_str_req('TAK_CA_NAME') + self.ca_state = env_str_req('TAK_STATE') + self.ca_city = env_str_req('TAK_CITY') + self.ca_organization = env_str_req('TAK_ORGANIZATIONAL_UNIT') + + +class Configuration: + """ + An object to store the configuration options + """ + + def __init__(self): + self.deployment_target: DeploymentTarget = env_enum_req('TAK_DEPLOYMENT_TARGET', DeploymentTarget) + self.tak_cluster_name = env_str_req('TAK_CLUSTER_NAME') + self.tak_node_count = env_int_req('TAK_CLUSTER_NODE_COUNT') + self.tak_enable_plugins = env_int_req('TAK_PLUGINS') + self.tak_db_username = env_str_req('TAK_DB_USERNAME') + self.tak_db_password = env_str_req('TAK_DB_PASSWORD') + self.tak_cert_config = CertConfiguration() + self.kubernetes_version = env_str_req('TAK_KUBERNETES_VERSION') + self.kubernetes_namespace = env_str_req('TAK_KUBERNETES_NAMESPACE') + self.helm_version = env_str_req('TAK_HELM_VERSION') + self.minikube_version = env_str_req('TAK_MINIKUBE_VERSION') + self.insecure_publish_repo = env_str_req('TAK_INSECURE_PUBLISH_REPO') + self.kubeconfig = (env_str_req('TAK_KUBECONFIG_FILE') if self.deployment_target == DeploymentTarget.RKE2 + else env_str_opt('TAK_KUBECONFIG_FILE')) + self.use_port_exposure = env_bool_req('TAK_USE_PORT_EXPOSURE') + self.certs = CertConfiguration() + self.minikube_driver = env_str_req('TAK_MINIKUBE_DRIVER') + self.minikube_memory = env_str_req('TAK_MINIKUBE_MEMORY') + self.minikube_cpus = env_str_req('TAK_MINIKUBE_CPUS') + self.minikube_delete_existing_instance = env_bool_req("TAK_MINIKUBE_DELETE_EXISTING_INSTANCE") + + +# Kubectl Releases: https://kubernetes.io/docs/tasks/tools/ +class SoftwareLoadout(Enum): + """ + An object used to configure the environment-specific deployment dependencies + """ + LINUX_X86_64 = ('Linux x86_64', 'linux', 'x86_64') + LINUX_ARM_64 = ('Linux ARM64', 'linux', 'arm64') + LINUX_ARM = ('Linux ARM', 'linux', 'arm') + DARWIN_X86_64 = ('Mac OS x86_64', 'darwin', 'x86_64') + DARWIN_ARM_64 = ('Mac OS M2+', 'darwin', 'arm64') + + def __init__(self, pretty_label: str, python_platform: str, python_arch: str): + self._pretty_label = pretty_label + self._python_platform = python_platform + self._python_arch = python_arch + self._platform = python_platform # Same as what is reported by python + if self._python_arch == 'x86_64': + self._arch = 'amd64' + elif self._python_arch == 'arm64' or self._python_arch == 'arm': + self._arch = self._python_arch + else: + halt_on_failure(f'Deployment from {python_platform} - {python_arch} is currently not supported!') + self.bin_dir = os.path.join(SCRIPT_DIR, 'setup-bins') + self._kubectl_path = None + self._helm_path = None + self._minikube_path = None + + @property + def kubectl_path(self): + return self._kubectl_path + + @property + def helm_path(self): + return self._helm_path + + @property + def minikube_path(self): + return self._minikube_path + + @classmethod + def this_system(cls) -> 'SoftwareLoadout': + """ + Gets the configuration for the host system + :return: the SoftwareLoadout for this system + """ + system_platform = sys.platform + platform_machine = platform.machine() + loadout_identifiers = list() + for loadout in SoftwareLoadout: + if system_platform == loadout._python_platform and platform_machine == loadout._python_arch: + return loadout + loadout_identifiers.append(loadout._pretty_label) + + err_msg = (f'Could not autodetect supported platform! Supported Platforms:{", ".join(loadout_identifiers)}') + halt_on_failure(err_msg) + + def _download_binary(self, name: str, version: str, fetch_url: str) -> str: + """ + Downloads a binary file + :param name: The name of the binary + :param version: The version of the binary + :param fetch_url: The URL for the binary + :return: The path to the downloaded binary + """ + target = os.path.join(self.bin_dir, f'{name}-{version}-{self._platform}-{self._arch}') + fetch_url = fetch_url.format(version=version, os=self._platform, arch=self._arch) + if not os.path.exists(target): + print(f'Fetching {name} {version} from {fetch_url}...') + urllib.request.urlretrieve(fetch_url, target) + os.chmod(target, os.stat(target).st_mode | stat.S_IEXEC) + + path = os.path.join(self.bin_dir, name) + if os.path.exists(path): + os.remove(path) + os.symlink(target, path) + return path + + def install_software(self, config: Configuration) -> 'SoftwareLoadout': + """ + Installs the software needed to execute the provided deployment configuration + :param config: The configuration to fetch necessary software for + :return: This software loadout instance + """ + if not os.path.exists(self.bin_dir): + os.mkdir(self.bin_dir) + + self._kubectl_path = self._download_binary('kubectl', config.kubernetes_version, KUBCTL_URL) + + if config.deployment_target == DeploymentTarget.MINIKUBE: + self._minikube_path = self._download_binary('minikube', config.minikube_version, MINIKUBE_URL) + + helm_target = os.path.join(self.bin_dir, f'helm-{config.helm_version}-{self._platform}-{self._arch}') + fetch_url = HELM_URL.format(version=config.helm_version, os=self._platform, arch=self._arch) + + print(f'Fetching helm {config.helm_version} from {fetch_url}...') + if not os.path.exists(helm_target): + target_zip = os.path.join(self.bin_dir, f'helm-{config.helm_version}-{self._platform}-{self._arch}.tar.gz') + urllib.request.urlretrieve(fetch_url, target_zip) + shutil.unpack_archive(target_zip, self.bin_dir) + shutil.move(os.path.join(self.bin_dir, f'{self._platform}-{self._arch}', 'helm'), helm_target) + os.chmod(helm_target, os.stat(helm_target).st_mode | stat.S_IEXEC) + os.remove(target_zip) + shutil.rmtree(os.path.join(self.bin_dir, f'{self._platform}-{self._arch}')) + + self._helm_path = os.path.join(self.bin_dir, 'helm') + + if os.path.exists(self._helm_path): + os.remove(self._helm_path) + os.symlink(helm_target, self._helm_path) + + print() + return self + + +class CommandRunner: + """ + A helper class to run general, kubectl, minikube, or helm commands while logging the output to a shared log file + and handling failure as specified + """ + + def __init__(self, configuration: Configuration, software_loadout: SoftwareLoadout): + """ + :param configuration: The configuration to apply the commands to + :param software_loadout: the SoftwareLoadout to use to execute the commands + """ + self._configuration = configuration + self._kubectl_path = software_loadout.kubectl_path + self._minikube_path = software_loadout.minikube_path + self._helm_path = software_loadout.helm_path + + self._log_file = open(COMMAND_LOG_FILE, 'a') + + timestamp = datetime.datetime.now().strftime('%Y-%m-%dT%H:%M:%S') + self._log_file.write(f'Starting deployment at {timestamp}\n') + self._log_file.flush() + + @property + def configuration(self): + """ + :return: The Configuration + """ + return self._configuration + + def run_cmd(self, cmd, stdout_filepath: str = None, cwd: str = CLUSTER_ROOT, env: Dict[str, str] = None, + keep_running_on_failure: bool = False, silence_failure: bool = False) -> str: + """ + Runs a generic command + :param cmd: The command to run + :param stdout_filepath: An optional file to send the output to + :param cwd: The directory to run the command in + :param env: environment parameters to use with the command + :param keep_running_on_failure: If true, do not halt execution + :param silence_failure: If true, do not report the error as a fatal error + :return: The output of the command as a str + """ + + self._log_file.write(f'Executing command [{" ".join(cmd)}]\n') + process = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, cwd=cwd, env=env) + output = process.stdout + + # Write to file if specified + if stdout_filepath is not None: + with open(stdout_filepath, 'wb') as target: + target.write(output) + + # Write to standard log + self._log_file.write(output.decode()) + self._log_file.flush() + + if process.returncode != 0: + self._log_file.write(f'Command failed with error code {process.returncode}!\n') + self._log_file.flush() + + if not keep_running_on_failure: + if cmd[0].startswith(CLUSTER_ROOT): + cmd[0] = cmd[0].replace(CLUSTER_ROOT + '/', '') + halt_on_failure(f'An error occurred while executing command \'{" ".join(cmd)}\'! ' + + f'See the end of {COMMAND_LOG_FILE_RELATIVE_PATH} for details.', + silence_failure=silence_failure) + + self._log_file.write(f'Command finished successfully\n') + self._log_file.flush() + + return None if output is None else output.decode() + + def kubectl(self, cmd: List[str], stdout_filepath: str = None, cwd=None, keep_running_on_failure: bool = False, + silence_failure: bool = False) -> str: + """ + Runs a kubectl command + :param cmd: The command to run + :param stdout_filepath: The optional filepath to write the output to + :param cwd: The optional directory to run the command in + :param keep_running_on_failure: If true, do not halt execution + :param silence_failure: If true, do not report the error as a fatal error + :return: The output of the command as a str + """ + if cmd[0] == 'kubectl': + cmd.pop(0) + cmd.insert(0, self._kubectl_path) + if self.configuration.deployment_target == DeploymentTarget.RKE2: + env = {'KUBECONFIG': self._configuration.kubeconfig} + else: + env = None + return self.run_cmd(cmd, stdout_filepath, cwd, env, keep_running_on_failure=keep_running_on_failure, + silence_failure=silence_failure) + + def minikube(self, cmd: List[str], stdout_filepath: str = None, cwd=None, keep_running_on_failure: bool = False, + silence_failure: bool = False) -> str: + """ + Runs a minikube command + :param cmd: The command to run + :param stdout_filepath: The optional filepath to write the output to + :param cwd: The optional directory to run the command in + :param keep_running_on_failure: If true, do not halt execution + :param silence_failure: If true, do not report the error as a fatal error + :return: The output of the command as a str + """ + if cmd[0] == 'minikube': + cmd.pop(0) + cmd.insert(0, self._minikube_path) + return self.run_cmd(cmd, stdout_filepath, cwd, None, keep_running_on_failure=keep_running_on_failure, + silence_failure=silence_failure) + + def helm(self, cmd: List[str], stdout_filepath: str = None, cwd=None, keep_running_on_failure: bool = False, + silence_failure: bool = False) -> str: + """ + Runs a helm command + :param cmd: The command to run + :param stdout_filepath: The optional filepath to write the output to + :param cwd: The optional directory to run the command in + :param keep_running_on_failure: If true, do not halt execution + :param silence_failure: If true, do not report the error as a fatal error + :return: The output of the command as a str + """ + if cmd[0] == 'helm': + cmd.pop(0) + cmd.insert(0, self._helm_path) + if self.configuration.deployment_target == DeploymentTarget.RKE2: + env = {'KUBECONFIG': self._configuration.kubeconfig} + else: + env = None + return self.run_cmd(cmd, stdout_filepath, cwd, env, keep_running_on_failure=keep_running_on_failure, + silence_failure=silence_failure) + + +class DockerImages(Enum): + """ + The docker images that may be used as part of a deployment + """ + Base = ('docker-files/Dockerfile.takserver-base', 'takserver-base') + Config = ('docker-files/Dockerfile.takserver-config', 'takserver-config') + Messaging = ('docker-files/Dockerfile.takserver-messaging', 'takserver-messaging') + Api = ('docker-files/Dockerfile.takserver-api', 'takserver-api') + DatabaseSetup = ('docker-files/Dockerfile.database-setup', 'takserver-database-setup') + CaSetup = ('docker-files/Dockerfile.ca', 'takserver-ca-setup') + Plugins = ('docker-files/Dockerfile.takserver-plugins', 'takserver-plugins') + + def __init__(self, dockerfile: str, base_image_name: str): + """ + :param dockerfile: The local path to the dockerfile + :param base_image_name: The name of the resulting image, without the tag or repository information + """ + self._dockerfile = dockerfile + self._base_image_name = base_image_name + self._repository = None + self._tag = None + self._enabled = True + + @property + def repository(self) -> str: + """ + :return: The repository the image has been published to, if it has been published. + """ + return self._repository + + @property + def tag(self) -> str: + """ + :return: The tag that has been applied to the image, if it has been published + """ + return self._tag + + @property + def enabled(self) -> bool: + """ + :return: Whether the image is enabled + """ + return self._enabled + + def disable(self): + self._enabled = False + + def build(self, cmd_runner: CommandRunner, image_tag: str): + """ + Builds the image and updates the repository and tag information + :param cmd_runner: The command runner to use to build the image + :param image_tag: The tag to apply to the image + """ + if self._enabled: + image_repo = cmd_runner.configuration.insecure_publish_repo + full_tag = f'{image_repo}/{self._base_image_name}:{image_tag}' + print(f'Building docker image {full_tag}...') + + cmd = ['docker', 'build', '-t', f'{full_tag}', '-f', self._dockerfile, + '--build-arg', f'TAKSERVER_IMAGE_REPO={image_repo}/takserver-base', '--build-arg', + f'TAKSERVER_IMAGE_TAG={image_tag}', '.'] + cmd_runner.run_cmd(cmd) + self._repository = f'{image_repo}/{self._base_image_name}' + self._tag = image_tag + + else: + print(f'Skipping building of docker image for {self._base_image_name}') + + def publish(self, cmd_runner: CommandRunner): + """ + Publishes the built image + :param cmd_runner: The command runner to use to publish the image + :return: + """ + # TODO: Add support for self signed and authenticated repositories + if self._enabled: + if self._repository is None or self._tag is None: + halt_on_failure("Images must be built before they can be published!") + + repository_and_tag = f'{self._repository}:{self._tag}' + print(f'Publishing docker image {repository_and_tag}') + cmd_runner.run_cmd(['docker', 'push', repository_and_tag]) + + else: + print(f'Skipping publishing of docker image for {self._base_image_name}') + + +class DeploymentContainers(Enum): + """ + The deployed container types + """ + Config = ('config', DockerImages.Config, TAK_CONFIG_NODE_MULTIPLIER) + Messaging = ('messaging', DockerImages.Messaging, TAK_MESSAGING_NODE_MULTIPLIER) + Api = ('api', DockerImages.Api, TAK_API_NODE_MULTIPLIER) + Plugins = ('plugins', DockerImages.Plugins, TAK_PLUGIN_NODE_MULTIPLIER) + DatabaseSetup = ('takserverDatabaseSetup', DockerImages.DatabaseSetup, TAK_DATABASE_SETUP_MULTIPLIER) + + def __init__(self, config_name: str, docker_image: DockerImages, replica_multiplier: int): + """ + :param config_name: The name of the configuration in the yaml files + :param docker_image: The docker image object used by the container + :param replica_multiplier: The multiplier for now many instances should be deployed based on the takserver node + count. At least one will always be deployed. + """ + self._config_name = config_name + self._docker_image = docker_image + self._replica_multiplier = replica_multiplier + + def update_helm_config(self, deployment_values: Dict, tak_cluster_node_count: int): + """ + Updates the helm configuration to have the desired resource allocations for this container + :param deployment_values: The helm configuration + :param tak_cluster_node_count: The defined TAK cluster node count. Standard multipliers for each node type will + be multiplied by this to get the desired replica count. + """ + container = deployment_values['takserver'][self._config_name] + + if self._docker_image.enabled: + container['enabled'] = True + + container['image']['repository'] = self._docker_image.repository + container['image']['tag'] = self._docker_image.tag + + if tak_cluster_node_count == 0: + print(f'Setting {self.name} replicas to 1') + container['replicas'] = 1 + if 'resources' not in container: + container['resources'] = dict() + if 'requests' not in container['resources']: + container['resources']['requests'] = dict() + if 'limits' not in container['resources']: + container['resources']['limits'] = dict() + container['resources']['requests']['cpu'] = 2 + container['resources']['limits']['cpu'] = 2 + container['resources']['requests']['memory'] = '2Gi' + container['resources']['limits']['memory'] = '2Gi' + else: + replicas = max(1, int(round(tak_cluster_node_count * self._replica_multiplier))) + print(f'Setting {self.name} replicas to {replicas}') + container['replicas'] = replicas + + else: + container['enabled'] = False + + +class ClusterDeployer: + """ + Used to deploy a generic cluster instance + """ + + def __init__(self, configuration: Configuration, software_loadout: SoftwareLoadout): + """ + :param configuration: The configuration to deploy + :param software_loadout: The local applications set up to be used for deployment + """ + self._configuration = configuration + self._cmd_runner = CommandRunner(configuration, software_loadout) + + def build_docker_images(self): + """ + Builds all enabled docker images + """ + + if self._configuration.tak_enable_plugins <= 0: + DockerImages.Plugins.disable() + + for image in DockerImages: + image.build(self._cmd_runner, self._configuration.kubernetes_namespace) + + def publish_docker_images(self): + """ + Publishes all enabled docker images. If they haven't been built this will fail! + """ + for image in DockerImages: + image.publish(self._cmd_runner) + + def prep_certs(self): + """ + Prepares the certificates for this configuration. If none are provided a container will be brought up to create + them + """ + + def validate_required_certs(cert_dir): + for cert in REQUIRED_CERTIFICATES: + if not os.path.isfile(os.path.join(cert_dir, cert)): + halt_on_failure(f'Although the certificate directory {cert_dir} exists it does not contain the ' + + f'required certificate {cert}!') + + config = self._configuration.tak_cert_config + default_cert_dir = os.path.join(CLUSTER_ROOT, 'takserver-core', 'certs', 'files') + + if config.src_dir is not None: + print(f'Using certificates located in {config.src_dir}') + source_dir = config.src_dir + + elif (os.path.isdir(default_cert_dir) and os.path.isfile(os.path.join(default_cert_dir, 'admin.pem'))): + print(f'Using certificates prepopulated in {default_cert_dir}') + source_dir = default_cert_dir + + else: + print(f'Creating new certificates with default password "atakatak" in {default_cert_dir}') + self._cmd_runner.run_cmd([ + 'docker', 'run', '-it', f'--name={CA_CONTAINER_NAME}', + f'--env=CA_NAME={config.ca_name}', + f'--env=STATE={config.ca_state}', + f'--env=CITY={config.ca_city}', + f'--env=ORGANIZATIONAL_UNIT={config.ca_organization}', + f"{DockerImages.CaSetup.repository}:{DockerImages.CaSetup.tag}"]) + self._cmd_runner.run_cmd(['docker', 'cp', f'{CA_CONTAINER_NAME}:/files', + os.path.join(CLUSTER_ROOT, 'takserver-core', 'certs')]) + self._cmd_runner.run_cmd(['docker', 'rm', CA_CONTAINER_NAME]) + source_dir = default_cert_dir + + validate_required_certs(source_dir) + self._cmd_runner.kubectl(['create', 'configmap', 'cert-migration', + f'--from-file={source_dir}', '--dry-run=client', '--output=yaml'], + stdout_filepath=os.path.join(CLUSTER_ROOT, + 'deployments/helm/templates/cert-migration.yaml')) + + def update_configuration_files(self): + """ + Updates all deployment configuration files with relevant configuration details + """ + db_url = f'jdbc:postgresql://takserver-postgresql:5432/cot' + db_username = self._configuration.tak_db_username + db_password = self._configuration.tak_db_password + namespace = self._configuration.kubernetes_namespace + + print('Updating Helm deployment values file...') + # Update the helm deployment values + config_filepath = os.path.join(CLUSTER_ROOT, 'deployments/helm/production-values.yaml') + deployment_values = yaml.load(open(config_filepath), Loader=yaml.SafeLoader) + deployment_values['takserver']['namespace'] = namespace + deployment_values['postgresql']['enabled'] = True + pg_auth = deployment_values['global']['postgresql']['auth'] + pg_auth['username'] = db_username + pg_auth['password'] = db_password + pg_auth['postgresPassword'] = db_password + for container in DeploymentContainers: + container.update_helm_config(deployment_values, self._configuration.tak_node_count) + yaml.dump(deployment_values, open(config_filepath, 'w'), Dumper=yaml.SafeDumper) + + print('Updating CoreConfig.xml...') + cc_path = os.path.join(CLUSTER_ROOT, 'CoreConfig.xml') + core_config = open(cc_path).read() + core_config = core_config.replace('DB_URL_PLACEHOLDER', db_url) + core_config = core_config.replace('DB_USERNAME_PLACEHOLDER', db_username) + core_config = core_config.replace('DB_PASSWORD_PLACEHOLDER', db_password) + open(cc_path, 'w').write(core_config) + + print('Updating TAKIgniteConfig.xml...') + tic_path = os.path.join(CLUSTER_ROOT, 'TAKIgniteConfig.xml') + tic_tree = ET.parse(tic_path) + tic_tree.getroot().attrib['igniteClusterNamespace'] = namespace + tic_tree.write('TAKIgniteConfig.xml') + + def deploy(self): + """ + Deploys the configuration to the k8s cluster + """ + kubectl = self._cmd_runner.kubectl + helm = self._cmd_runner.helm + namespace = self._configuration.kubernetes_namespace + + if self._configuration.deployment_target == DeploymentTarget.MINIKUBE: + minikube = self._cmd_runner.minikube + # TODO: Add check for core count + if self._configuration.minikube_delete_existing_instance: + print("Halting existing Minikube instance...") + minikube(['stop'], keep_running_on_failure=True, silence_failure=True) + minikube(['delete'], keep_running_on_failure=True, silence_failure=True) + print("Starting Minikube...") + minikube(['start', '--apiserver-port', '9210', + f'--kubernetes-version={self._configuration.kubernetes_version}', + f'--driver={self._configuration.minikube_driver}', + f'--cpus={self._configuration.minikube_cpus}', + f'--memory={self._configuration.minikube_memory}', + f'--insecure-registry={self._configuration.insecure_publish_repo}', + '--extra-config=apiserver.service-node-port-range=8000-65535']) + minikube(['addons', 'enable', 'ingress']) + + # update_core_config + print('Deploying...') + kubectl(['create', 'configmap', 'tak-ignite-config', + f'--from-file={os.path.join(CLUSTER_ROOT, "TAKIgniteConfig.xml")}', + '--dry-run=client', '--output=yaml'], + stdout_filepath=os.path.join(CLUSTER_ROOT, + 'deployments/helm/templates/tak-ignite-config.yaml')) + + kubectl(['create', 'configmap', 'core-config', + f'--from-file={os.path.join(CLUSTER_ROOT, "CoreConfig.xml")}', + '--dry-run=client', '--output=yaml'], + stdout_filepath=os.path.join(CLUSTER_ROOT, 'deployments/helm/templates/core-config.yaml')) + + # DO NOT run helm commands within the deployments/helm directory as it may result in additional files being + # placed in there and deployment failures! + exec_dir = str(pathlib.Path(SCRIPT_DIR).parent) + helm(['dep', 'update', 'deployments/helm'], cwd=exec_dir) + + helm(['upgrade', '--install', + f'--namespace={namespace}', '--create-namespace', + '--values=deployments/helm/production-values.yaml', 'takserver', './deployments/helm'], cwd=exec_dir) + + def try_get_address(self, service_name: str) -> str: + # TODO: Add checks so that if multiple nodes are detected for a service it makes it known + if self._configuration.deployment_target == DeploymentTarget.MINIKUBE: + node_address = subprocess.check_output(['minikube', 'ip']).decode().strip() + + else: + try: + namespace = self._configuration.kubernetes_namespace + + node_name = json.loads(subprocess.check_output( + ['kubectl', '-n', namespace, 'get', 'pod', f'--selector=app={service_name}', '-o', + 'json']).decode().strip() + )['items'][0]['spec']['nodeName'] + node_details = json.loads(subprocess.check_output( + ['kubectl', 'get', 'node', '-A', '-o', 'json', f'--field-selector=metadata.name={node_name}']) + .strip()) + node_address = list(filter(lambda x: x['type'] == 'InternalIP', + node_details['items'][0]['status']['addresses']))[0]['address'] + except Exception as e: + node_address = None + + return node_address + + def apply_port_exposures(self): + """ + Deploys the configuration to the k8s cluster + """ + kubectl = self._cmd_runner.kubectl + namespace = self._configuration.kubernetes_namespace + + if self._configuration.deployment_target == DeploymentTarget.MINIKUBE: + node_address = subprocess.check_output(['minikube', 'ip']).decode().strip() + + else: + try: + api_node_name = json.loads(subprocess.check_output( + ['kubectl', '-n', namespace, 'get', 'pod', '--selector=app=takserver-api', '-o', 'json']).strip() + )['items'][0]['spec']['nodeName'] + node_details = json.loads(subprocess.check_output( + ['kubectl', 'get', 'node', '-A', '-o', 'json', f'--field-selector=metadata.name={api_node_name}']) + .strip()) + node_address = list(filter(lambda x: x['type'] == 'InternalIP', + node_details['items'][0]['status']['addresses']))[0]['address'] + except: + node_address = None + + def expose_port(node_name: str, service_identifier, protocol: str, port: int, host: str = None) -> str: + def try_patching(target_port: int): + kubectl(['patch', 'service', service_identifier, + f'--namespace={namespace}', '--type=json', + '--patch=[{"op": "replace", "path": "/spec/ports/0/nodePort", "value":' + str( + target_port) + '}]' + ], silence_failure=True) + + kubectl(['expose', 'deployment', node_name, + f'--namespace={namespace}', + f'--protocol={protocol}', f'--port={str(port)}', f'--target-port={str(port)}', + '--type=LoadBalancer', + f'--name={service_identifier}']) + + try: + try_patching(port) + except: + target_port = 30000 + port + print(f'Failed to patch port to {str(port)}. Trying {str(target_port)}') + try: + try_patching(target_port) + except: + print('Cannot patch port. Using automatically assigned port.') + + external_port = kubectl([ + 'get', 'service', f'--namespace={namespace}', '--output', + 'go-template={{range.spec.ports}}{{if .nodePort}}{{.nodePort}}{{end}}{{end}}', service_identifier]) + + if host is None: + return f"{service_identifier}-{protocol.lower()} -> {external_port}" + else: + return f"{service_identifier}-{protocol.lower()} -> {host}:{external_port}" + + urls = list() + urls.append(expose_port('takserver-api', 'cert-https', 'TCP', 8443, node_address)) + urls.append(expose_port('takserver-api', 'federation-truststore', 'TCP', 8444, node_address)) + urls.append(expose_port('takserver-api', 'https', 'TCP', 8446, node_address)) + urls.append(expose_port('takserver-messaging', 'tls', 'TCP', 8089, node_address)) + urls.append(expose_port('takserver-messaging', 'federation-v1', 'TCP', 9000, node_address)) + urls.append(expose_port('takserver-messaging', 'federation-v2', 'TCP', 9001, node_address)) + + def apply_ingress_rules(self): + """ + Deploys the configuration to the k8s cluster + """ + + if self._configuration.use_port_exposure: + print('Applying port exposures instead of direct ingress...') + return self.apply_port_exposures() + + kubectl = self._cmd_runner.kubectl + namespace = self._configuration.kubernetes_namespace + + try: + api_node_name = json.loads(subprocess.check_output( + ['kubectl', '-n', namespace, 'get', 'pod', '--selector=app=takserver-api', '-o', 'json']).strip() + )['items'][0]['spec']['nodeName'] + node_details = json.loads(subprocess.check_output( + ['kubectl', 'get', 'node', '-A', '-o', 'json', f'--field-selector=metadata.name={api_node_name}']) + .strip()) + node_address = list(filter(lambda x: x['type'] == 'InternalIP', + node_details['items'][0]['status']['addresses']))[0]['address'] + except: + node_address = None + + # Update the namespace for the ingress rules + data = yaml.load_all(open(DEFAULT_INGRESS_FILE), yaml.FullLoader) + target = list() + for section in data: + section['metadata']['namespace'] = namespace + target.append(section) + + yaml.dump_all(target, open(DEFAULT_INGRESS_FILE, 'w')) + + # Apply the ingress rules + kubectl(['apply', '--namespace', namespace, '-f', DEFAULT_INGRESS_FILE]) + + def display_external_access(self): + # TODO: Validate no services have multiple IP addresses + namespace = self._configuration.kubernetes_namespace + + service_json = json.loads( + subprocess.check_output(['kubectl', 'get', 'services', '-n', namespace, '-o', 'json']).strip()) + + service_list = list(filter( + lambda x: + x['metadata']['name'] == 'takserver-api-service' or + x['metadata']['name'] == 'takserver-messaging-service' + , service_json['items'])) + + endpoint_details = list() + + for service in service_list: + if ('status' in service and 'loadBalancer' in service['status'] and + 'ingress' in service['status']['loadBalancer'] and + len(service['status']['loadBalancer']['ingress']) > 0): + ip = service['status']['loadBalancer']['ingress'][0]['ip'] + else: + try: + ip = self.try_get_address(service['spec']['selector']['app']) + except: + ip = 'Unknown' + + if service['spec'] is not None and len(service['spec']['ports']) > 0: + for port in service['spec']['ports']: + endpoint_details.append({ + 'SERVICE': service['metadata']['name'], + 'ENDPOINT': port['name'], + 'ADDRESS': ip, + 'PORT': port['targetPort'] + }) + + print(f"Deployment Namespace: {namespace}") + print('\nService Endpoints:') + + pretty_print_dict_list(endpoint_details, ['SERVICE', 'ENDPOINT', 'ADDRESS', 'PORT']) + + print() + print('If no address is defined or the ports do not seem to work you may have to check with your system ' + 'administrator to see how your ingress is configured. You can also try setting TAK_USE_PORT_EXPOSURE ' + + 'to true to see if that resolves your issues.') + print() + print('If this is a locally hosted system you may have to adjust your firewall rules to allow external access.') + + def start(self): + """ + Starts deployment of the system + """ + self.build_docker_images() + print() + self.prep_certs() + print() + self.update_configuration_files() + print() + self.publish_docker_images() + print() + self.deploy() + print() + self.apply_ingress_rules() + print() + self.display_external_access() + + def uninstall(self): + """ + Removes the deployed artifacts from the cluster. The docker images are not removed, and a command to delete the + database will be printed to the command line to remove it as well. + """ + print('Uninstalling takserver...') + ns = self._configuration.kubernetes_namespace + + # Chart hooks can't be deleted as part of the chart so delete the db setup hook manually + # -- https://helm.sh/docs/topics/charts_hooks/#hook-deletion-policies + self._cmd_runner.kubectl(['delete', 'pods', f"--selector=job-name=takserver-db-setup", f'--namespace={ns}'], + keep_running_on_failure=True) + self._cmd_runner.kubectl(['delete', 'job.batch/takserver-db-setup', '-n', ns], keep_running_on_failure=True) + + # Remove the helm chart + self._cmd_runner.helm(['uninstall', f'--namespace={ns}', 'takserver'], keep_running_on_failure=True) + + print('Removing service definitions...') + self._cmd_runner.kubectl(['delete', 'svc', f'{ns}-https-8443', '-n', ns], keep_running_on_failure=True) + self._cmd_runner.kubectl(['delete', 'svc', f'{ns}-https-8444', '-n', ns], keep_running_on_failure=True) + self._cmd_runner.kubectl(['delete', 'svc', f'{ns}-https-8446', '-n', ns], keep_running_on_failure=True) + self._cmd_runner.kubectl(['delete', 'svc', f'{ns}-streaming-8089', '-n', ns], keep_running_on_failure=True) + self._cmd_runner.kubectl(['delete', 'svc', f'{ns}-fed-9000', '-n', ns], keep_running_on_failure=True) + self._cmd_runner.kubectl(['delete', 'svc', f'{ns}-fed-9001', '-n', ns], keep_running_on_failure=True) + + print('If you would like to delete the database please execute "kubectl delete pvc -n ' + + ns + ' data-takserver-postgresql-0"') + + if self._configuration.deployment_target == DeploymentTarget.MINIKUBE: + print('Run "scripts/setup-bins/minikube delete" if you would like to delete the host minikube instance ' + + ' including the database.') + + result = None + while result != '': + result = self._cmd_runner.kubectl([f'--namespace={ns}', 'get', 'pods', '--output=name']) + time.sleep(2) + + +def main(): + print(f'Starting deployment of TAKServer. Execution logs will be saved to {COMMAND_LOG_FILE_RELATIVE_PATH}.\n') + # Get the configuration + configuration = Configuration() + # Install the software necessary to set up the cluster + software_loadout = SoftwareLoadout.this_system().install_software(configuration) + # Deploy the cluster + + if len(sys.argv) >= 2: + if sys.argv[1] == 'uninstall' or sys.argv[1] == '-u': + ClusterDeployer(configuration, software_loadout).uninstall() + elif sys.argv[1] == 'status' or sys.argv[1] == '-s': + ClusterDeployer(configuration, software_loadout).display_external_access() + else: + print(f"Unexpected argument {sys.argv[1]}!") + exit(1) + else: + ClusterDeployer(configuration, software_loadout).start() + + +if __name__ == '__main__': + main() diff --git a/src/takserver-cluster/scripts/delete-eks.py b/src/takserver-cluster/scripts/delete-eks.py index 7215479a..a02e1a9a 100644 --- a/src/takserver-cluster/scripts/delete-eks.py +++ b/src/takserver-cluster/scripts/delete-eks.py @@ -10,6 +10,13 @@ import hashlib import xml.etree.ElementTree + +if 'TAK_DEPLOYMENT_TARGET' in os.environ and os.environ['TAK_DEPLOYMENT_TARGET'] != 'aws': + print(f'TAK_DEPLOYMENT_TARGET is not set to "aws"! Ensure all required variables are set in cluster-properties ' + + 'and it has been sourced prior to running this script!') + exit(1) + + # AWS CREDS AWS_ACCESS_KEY_ID = os.environ['AWS_ACCESS_KEY_ID'] AWS_SECRET_ACCESS_KEY = os.environ['AWS_SECRET_ACCESS_KEY'] diff --git a/src/takserver-cluster/scripts/delete-generic.py b/src/takserver-cluster/scripts/delete-generic.py new file mode 100644 index 00000000..e62ae9c7 --- /dev/null +++ b/src/takserver-cluster/scripts/delete-generic.py @@ -0,0 +1,9 @@ +#!/usr/bin/env python3 +# A convenience script to match with current deletion scripts + +import os +import subprocess + +if __name__ == '__main__': + script_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'build-generic.py') + subprocess.check_call(['python3', script_dir, 'uninstall']) diff --git a/src/takserver-common/build.gradle b/src/takserver-common/build.gradle index 635c5757..f797dc80 100644 --- a/src/takserver-common/build.gradle +++ b/src/takserver-common/build.gradle @@ -103,6 +103,7 @@ dependencies { implementation group: 'org.springframework.security', name: 'spring-security-core', version: spring_security_version api group: 'com.beust', name: 'jcommander', version: jcommander_version + implementation group: 'uk.m0nom', name: 'javaapiforkml', version: javaapiforkml_version } sourceSets { diff --git a/src/takserver-common/src/main/java/com/bbn/marti/remote/CoreConfig.java b/src/takserver-common/src/main/java/com/bbn/marti/remote/CoreConfig.java index 6469fedb..f04a4414 100644 --- a/src/takserver-common/src/main/java/com/bbn/marti/remote/CoreConfig.java +++ b/src/takserver-common/src/main/java/com/bbn/marti/remote/CoreConfig.java @@ -20,6 +20,7 @@ import com.bbn.marti.config.Input; import com.bbn.marti.config.Qos; import com.bbn.marti.config.Repository; +import com.bbn.marti.config.Subscription; import com.bbn.marti.config.Tls; import com.bbn.marti.config.Vbm; import com.bbn.marti.remote.groups.NetworkInputAddResult; @@ -54,7 +55,15 @@ public interface CoreConfig { void setAndSaveStoreForwardChatEnabled(boolean storeForwardChatEnabled); void setAndSaveVbmConfiguration(Vbm vbm); - + + void setAndSaveServerId(String serverId); + + void setAndSaveEnterpriseSyncSizeLimit(int uploadSizeLimit); + + void addStaticSubscriptionAndSave(@NotNull Subscription.Static newStaticSubscription); + + void removeStaticSubscriptionAndSave(@NotNull String subscriptionIdentifier); + boolean isContactApiFilter(); Set getContactApiWriteOnlyGroups(); diff --git a/src/takserver-common/src/main/java/com/bbn/marti/remote/FederationManager.java b/src/takserver-common/src/main/java/com/bbn/marti/remote/FederationManager.java index ecc88e13..91a5cd3b 100644 --- a/src/takserver-common/src/main/java/com/bbn/marti/remote/FederationManager.java +++ b/src/takserver-common/src/main/java/com/bbn/marti/remote/FederationManager.java @@ -62,7 +62,7 @@ public interface FederationManager { void enableOutgoing(String name); // will throw exception if output with same name already exists - void addOutgoingConnection(String name, String host, int port, int reconnect, int maxRetries, boolean unlimitedRetries, boolean enable, int protocolVersion, String fallback); + void addOutgoingConnection(String name, String host, int port, int reconnect, int maxRetries, boolean unlimitedRetries, boolean enable, int protocolVersion, String fallback, String token); void updateOutgoingConnection(Federation.FederationOutgoing original, Federation.FederationOutgoing update); void removeOutgoing(String name); diff --git a/src/takserver-common/src/main/java/com/bbn/marti/remote/config/CoreConfigFacade.java b/src/takserver-common/src/main/java/com/bbn/marti/remote/config/CoreConfigFacade.java index e5c5b435..add32af4 100644 --- a/src/takserver-common/src/main/java/com/bbn/marti/remote/config/CoreConfigFacade.java +++ b/src/takserver-common/src/main/java/com/bbn/marti/remote/config/CoreConfigFacade.java @@ -145,6 +145,26 @@ public void setAndSaveVbmConfiguration(Vbm vbm) { notifyConfigDirty(); } + public void setAndSaveServerId(String serverId) { + coreConfig.setAndSaveServerId(serverId); + notifyConfigDirty(); + } + + public void setAndSaveEnterpriseSyncSizeLimit(int uploadSizeLimit) { + coreConfig.setAndSaveEnterpriseSyncSizeLimit(uploadSizeLimit); + notifyConfigDirty(); + } + + public void addStaticSubscriptionAndSave(@NotNull Subscription.Static newStaticSubscription) { + coreConfig.addStaticSubscriptionAndSave(newStaticSubscription); + notifyConfigDirty(); + } + + public void removeStaticSubscriptionAndSave(@NotNull String subscriptionIdentifier) { + coreConfig.removeStaticSubscriptionAndSave(subscriptionIdentifier); + notifyConfigDirty(); + } + public boolean isContactApiFilter() { return coreConfig.isContactApiFilter(); } diff --git a/src/takserver-common/src/main/java/com/bbn/marti/remote/config/DistributedConfiguration.java b/src/takserver-common/src/main/java/com/bbn/marti/remote/config/DistributedConfiguration.java index b623da25..17015125 100644 --- a/src/takserver-common/src/main/java/com/bbn/marti/remote/config/DistributedConfiguration.java +++ b/src/takserver-common/src/main/java/com/bbn/marti/remote/config/DistributedConfiguration.java @@ -551,6 +551,18 @@ public void setAndSaveVbmConfiguration(Vbm vbm) { saveChangesAndUpdateCache(); } + @Override + public void setAndSaveServerId(String serverId) { + getRemoteConfiguration().getNetwork().setServerId(serverId); + saveChangesAndUpdateCache(); + } + + @Override + public void setAndSaveEnterpriseSyncSizeLimit(int uploadSizeLimt) { + getRemoteConfiguration().getNetwork().setEnterpriseSyncSizeLimitMB(uploadSizeLimt); + saveChangesAndUpdateCache(); + } + @Override public boolean isContactApiFilter() { return getRemoteConfiguration().getFilter().getContactApi() != null; diff --git a/src/takserver-common/src/main/java/com/bbn/marti/remote/sync/MissionMetadata.java b/src/takserver-common/src/main/java/com/bbn/marti/remote/sync/MissionMetadata.java index 6a514325..8f357fcc 100644 --- a/src/takserver-common/src/main/java/com/bbn/marti/remote/sync/MissionMetadata.java +++ b/src/takserver-common/src/main/java/com/bbn/marti/remote/sync/MissionMetadata.java @@ -1,5 +1,7 @@ package com.bbn.marti.remote.sync; +import java.util.UUID; + /* * * value class holding succinct metadata about a mission @@ -12,6 +14,7 @@ public class MissionMetadata { private String chatRoom; private String tool; private String description; + private UUID guid; public String getName() { return name; } @@ -42,9 +45,15 @@ public String getDescription() { public void setDescription(String description) { this.description = description; } + public UUID getGuid() { + return guid; + } + public void setGuid(UUID guid) { + this.guid = guid; + } @Override public String toString() { return "MissionMetadata [name=" + name + ", creatorUid=" + creatorUid + ", chatRoom=" + chatRoom + ", tool=" - + tool + ", description=" + description + "]"; + + tool + ", description=" + description + ", guid=" + guid + "]"; } } diff --git a/src/takserver-common/src/main/java/com/bbn/marti/remote/util/RemoteUtil.java b/src/takserver-common/src/main/java/com/bbn/marti/remote/util/RemoteUtil.java index 03e1a04d..93877a90 100644 --- a/src/takserver-common/src/main/java/com/bbn/marti/remote/util/RemoteUtil.java +++ b/src/takserver-common/src/main/java/com/bbn/marti/remote/util/RemoteUtil.java @@ -8,10 +8,9 @@ import java.util.Collection; import java.util.NavigableSet; import java.util.Set; +import java.util.UUID; import java.util.concurrent.ConcurrentSkipListSet; -import jakarta.xml.bind.DatatypeConverter; - import org.apache.commons.lang3.StringUtils; import org.apache.ignite.Ignite; import org.jetbrains.annotations.NotNull; @@ -32,6 +31,8 @@ import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import jakarta.xml.bind.DatatypeConverter; + /* * * Constants and utility functions that are shared between TAK Server microservices diff --git a/src/takserver-common/src/main/java/de/micromata/opengis/kml/v_2_2_0/SchemaData.java b/src/takserver-common/src/main/java/de/micromata/opengis/kml/v_2_2_0/SchemaData.java new file mode 100644 index 00000000..29fa691f --- /dev/null +++ b/src/takserver-common/src/main/java/de/micromata/opengis/kml/v_2_2_0/SchemaData.java @@ -0,0 +1,210 @@ +// +// Source code recreated from a .class file by IntelliJ IDEA +// (powered by FernFlower decompiler) +// + +package de.micromata.opengis.kml.v_2_2_0; + +import de.micromata.opengis.kml.v_2_2_0.annotations.Obvious; +import de.micromata.opengis.kml.v_2_2_0.gx.SimpleArrayData; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; +import jakarta.xml.bind.annotation.XmlAccessType; +import jakarta.xml.bind.annotation.XmlAccessorType; +import jakarta.xml.bind.annotation.XmlAttribute; +import jakarta.xml.bind.annotation.XmlElement; +import jakarta.xml.bind.annotation.XmlRootElement; +import jakarta.xml.bind.annotation.XmlSchemaType; +import jakarta.xml.bind.annotation.XmlType; + +@XmlAccessorType(XmlAccessType.FIELD) +@XmlType( + name = "SchemaDataType", + propOrder = {"simpleData", "schemaDataExtension"} +) +@XmlRootElement( + name = "SchemaData", + namespace = "http://www.opengis.net/kml/2.2" +) +public class SchemaData extends AbstractObject implements Cloneable { + @XmlElement( + name = "SimpleData" + ) + protected List simpleData; + @XmlElement( + name = "SimpleArrayData", + namespace = "http://www.google.com/kml/ext/2.2" + ) + protected List schemaDataExtension; + @XmlAttribute( + name = "schemaUrl" + ) + @XmlSchemaType( + name = "anyURI" + ) + protected String schemaUrl; + + public SchemaData() { + } + + public List getSimpleData() { + if (this.simpleData == null) { + this.simpleData = new ArrayList(); + } + + return this.simpleData; + } + + public List getSchemaDataExtension() { + if (this.schemaDataExtension == null) { + this.schemaDataExtension = new ArrayList(); + } + + return this.schemaDataExtension; + } + + public String getSchemaUrl() { + return this.schemaUrl; + } + + public void setSchemaUrl(String var1) { + this.schemaUrl = var1; + } + + public int hashCode() { + int var2 = super.hashCode(); + var2 = 31 * var2 + (this.simpleData == null ? 0 : this.simpleData.hashCode()); + var2 = 31 * var2 + (this.schemaDataExtension == null ? 0 : this.schemaDataExtension.hashCode()); + var2 = 31 * var2 + (this.schemaUrl == null ? 0 : this.schemaUrl.hashCode()); + return var2; + } + + public boolean equals(Object var1) { + if (this == var1) { + return true; + } else if (var1 == null) { + return false; + } else if (!super.equals(var1)) { + return false; + } else if (!(var1 instanceof SchemaData)) { + return false; + } else { + SchemaData var2 = (SchemaData)var1; + if (this.simpleData == null) { + if (var2.simpleData != null) { + return false; + } + } else if (!this.simpleData.equals(var2.simpleData)) { + return false; + } + + if (this.schemaDataExtension == null) { + if (var2.schemaDataExtension != null) { + return false; + } + } else if (!this.schemaDataExtension.equals(var2.schemaDataExtension)) { + return false; + } + + if (this.schemaUrl == null) { + if (var2.schemaUrl != null) { + return false; + } + } else if (!this.schemaUrl.equals(var2.schemaUrl)) { + return false; + } + + return true; + } + } + + public SimpleData createAndAddSimpleData(String var1) { + SimpleData var2 = new SimpleData(var1); + this.getSimpleData().add(var2); + return var2; + } + + public void setSimpleData(List var1) { + this.simpleData = var1; + } + + public SchemaData addToSimpleData(SimpleData var1) { + this.getSimpleData().add(var1); + return this; + } + + public void setSchemaDataExtension(List var1) { + this.schemaDataExtension = var1; + } + + public SchemaData addToSchemaDataExtension(SimpleArrayData var1) { + this.getSchemaDataExtension().add(var1); + return this; + } + + @Obvious + public void setObjectSimpleExtension(List var1) { + super.setObjectSimpleExtension(var1); + } + + @Obvious + public SchemaData addToObjectSimpleExtension(Object var1) { + super.getObjectSimpleExtension().add(var1); + return this; + } + + public SchemaData withSimpleData(List var1) { + this.setSimpleData(var1); + return this; + } + + public SchemaData withSchemaDataExtension(List var1) { + this.setSchemaDataExtension(var1); + return this; + } + + public SchemaData withSchemaUrl(String var1) { + this.setSchemaUrl(var1); + return this; + } + + @Obvious + public SchemaData withObjectSimpleExtension(List var1) { + super.withObjectSimpleExtension(var1); + return this; + } + + @Obvious + public SchemaData withId(String var1) { + super.withId(var1); + return this; + } + + @Obvious + public SchemaData withTargetId(String var1) { + super.withTargetId(var1); + return this; + } + + public SchemaData clone() { + SchemaData var1 = (SchemaData)super.clone(); + var1.simpleData = new ArrayList(this.getSimpleData().size()); + Iterator var2 = this.simpleData.iterator(); + + while(var2.hasNext()) { + SimpleData var3 = (SimpleData)var2.next(); + var1.simpleData.add(var3.clone()); + } + + var1.schemaDataExtension = new ArrayList(this.getSchemaDataExtension().size()); + var2 = this.schemaDataExtension.iterator(); + + while(var2.hasNext()) { + SimpleArrayData var4 = (SimpleArrayData)var2.next(); + var1.schemaDataExtension.add(var4); + } + + return var1; + } +} diff --git a/src/takserver-common/src/main/java/de/micromata/opengis/kml/v_2_2_0/package-info.java b/src/takserver-common/src/main/java/de/micromata/opengis/kml/v_2_2_0/package-info.java new file mode 100644 index 00000000..98f2441c --- /dev/null +++ b/src/takserver-common/src/main/java/de/micromata/opengis/kml/v_2_2_0/package-info.java @@ -0,0 +1,9 @@ +@XmlSchema(namespace = "http://www.opengis.net/kml/2.2", + xmlns = {@XmlNs(prefix = "", + namespaceURI = "http://www.opengis.net/kml/2.2")}, + elementFormDefault = XmlNsForm.QUALIFIED) +package de.micromata.opengis.kml.v_2_2_0; + +import jakarta.xml.bind.annotation.XmlNs; +import jakarta.xml.bind.annotation.XmlNsForm; +import jakarta.xml.bind.annotation.XmlSchema; \ No newline at end of file diff --git a/src/takserver-common/src/main/java/tak/server/PluginManager.java b/src/takserver-common/src/main/java/tak/server/PluginManager.java index cc6ce56c..c10e0e25 100644 --- a/src/takserver-common/src/main/java/tak/server/PluginManager.java +++ b/src/takserver-common/src/main/java/tak/server/PluginManager.java @@ -67,7 +67,7 @@ public interface PluginManager { * Request data from a plugin. Implementation of this function is entirely delegated to the plugin. * */ - PluginResponse requestDataFromPlugin(String pluginClassName, Map allRequestParams, String contentType); + PluginResponse requestDataFromPlugin(String pluginClassName, Map allRequestParams, String accept); /* * Delete data from a plugin. Implementation of this function is entirely delegated to the plugin. diff --git a/src/takserver-common/src/main/java/tak/server/ignite/IgniteConfigurationHolder.java b/src/takserver-common/src/main/java/tak/server/ignite/IgniteConfigurationHolder.java index 06f9db68..140ed7c4 100644 --- a/src/takserver-common/src/main/java/tak/server/ignite/IgniteConfigurationHolder.java +++ b/src/takserver-common/src/main/java/tak/server/ignite/IgniteConfigurationHolder.java @@ -275,7 +275,7 @@ public IgniteConfiguration getIgniteConfiguration(String igniteProfile, TAKIgnit // spec: ipFinder.setServiceName("takserver-ignite"); - ipFinder.setNamespace("takserver"); + ipFinder.setNamespace(takIgniteConfiguration.getIgniteClusterNamespace()); // default is // /** Kubernetes API server URL in a string form. */ diff --git a/src/takserver-common/src/main/xsd/CoreConfig.xsd b/src/takserver-common/src/main/xsd/CoreConfig.xsd index 0659f064..2c2676bd 100644 --- a/src/takserver-common/src/main/xsd/CoreConfig.xsd +++ b/src/takserver-common/src/main/xsd/CoreConfig.xsd @@ -94,12 +94,12 @@ - + Set the timeout for file uploads in milliseconds (sync/upload endpoint). Default is 600000 ms (5 minutes). - + Set the timeout for file downloads in milliseconds (sync/upload endpoint). Default is 600000 ms (5 minutes). @@ -224,6 +224,8 @@ + + @@ -507,6 +509,11 @@ + + + Enable http access log (/opt/tak/logs/takserver-api-access.log) + + @@ -944,6 +951,7 @@ + diff --git a/src/takserver-common/src/main/xsd/TAKIgniteConfig.xsd b/src/takserver-common/src/main/xsd/TAKIgniteConfig.xsd index 7fe3d688..b67dc274 100644 --- a/src/takserver-common/src/main/xsd/TAKIgniteConfig.xsd +++ b/src/takserver-common/src/main/xsd/TAKIgniteConfig.xsd @@ -33,6 +33,11 @@ Explicity set the off-heap cache max size (bytes). -1 means autodetect. + + + The namespace used in a cluster deployment. Default is takserver + + Explicity set the off-heap cache initial size (bytes). -1 means autodetect diff --git a/src/takserver-common/src/main/xsd/UserAuthenticationFile.xsd b/src/takserver-common/src/main/xsd/UserAuthenticationFile.xsd index a5addbae..b147e972 100644 --- a/src/takserver-common/src/main/xsd/UserAuthenticationFile.xsd +++ b/src/takserver-common/src/main/xsd/UserAuthenticationFile.xsd @@ -35,6 +35,9 @@ + + + diff --git a/src/takserver-core/build.gradle b/src/takserver-core/build.gradle index 02f5dd21..eadbcf0c 100644 --- a/src/takserver-core/build.gradle +++ b/src/takserver-core/build.gradle @@ -150,7 +150,9 @@ dependencies { implementation "io.netty:netty-tcnative-boringssl-static:$netty_tcnative_version:windows-x86_64" implementation "io.netty.incubator:netty-incubator-codec-native-quic:$netty_quic_version:linux-x86_64" + implementation "io.netty.incubator:netty-incubator-codec-native-quic:$netty_quic_version:linux-aarch_64" implementation "io.netty.incubator:netty-incubator-codec-native-quic:$netty_quic_version:osx-x86_64" + implementation "io.netty.incubator:netty-incubator-codec-native-quic:$netty_quic_version:osx-aarch_64" implementation "io.netty.incubator:netty-incubator-codec-native-quic:$netty_quic_version:windows-x86_64" implementation group: 'io.netty', name: 'netty-tcnative-classes', version: netty_tcnative_version @@ -290,7 +292,7 @@ dependencies { //implementation group: 'org.springframework.security', name: 'spring-security-oauth2-authorization-server', version: '1.1.2' - + implementation group: 'uk.m0nom', name: 'javaapiforkml', version: javaapiforkml_version } clean { diff --git a/src/takserver-core/docker/hardened/full/CoreConfig.example.docker-hardened-full.xml b/src/takserver-core/example/CoreConfig.example.docker-hardened-full.xml similarity index 100% rename from src/takserver-core/docker/hardened/full/CoreConfig.example.docker-hardened-full.xml rename to src/takserver-core/example/CoreConfig.example.docker-hardened-full.xml diff --git a/src/takserver-core/scripts/API/takserver-api b/src/takserver-core/scripts/API/takserver-api index 940bd676..8c5d3888 100644 --- a/src/takserver-core/scripts/API/takserver-api +++ b/src/takserver-core/scripts/API/takserver-api @@ -33,7 +33,7 @@ TAK_HOME=/opt/tak case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${TAK_HOME} && ./takserver-api.sh &> /opt/tak/logs/takserver-api-console.log &" + su tak -c "cd ${TAK_HOME} && ./takserver-api.sh > /opt/tak/logs/takserver-api-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/takserver-core/scripts/api-readiness.sh b/src/takserver-core/scripts/api-readiness.sh index 841141fd..ed13819f 100755 --- a/src/takserver-core/scripts/api-readiness.sh +++ b/src/takserver-core/scripts/api-readiness.sh @@ -1,2 +1,16 @@ #!/bin/sh -cat '/logs/takserver-api.log' | grep -q 'Tomcat started on port(s)' \ No newline at end of file +cat '/logs/takserver-api.log' | grep -q 'Tomcat started on port(s)' + +nc -zw3 $POSTGRES_HOST $POSTGRES_PORT +RVAL=$? + +while [ $RVAL != 0 ];do + nc -zw3 $POSTGRES_HOST $POSTGRES_PORT + RVAL=$? + sleep 1 +# init_time=true +done + +#if [ "$init_time" = "true" ];then +# sleep 60 +#fi diff --git a/src/takserver-core/scripts/certs/makeCert.sh b/src/takserver-core/scripts/certs/makeCert.sh index 8b4ee6a6..1aac6244 100755 --- a/src/takserver-core/scripts/certs/makeCert.sh +++ b/src/takserver-core/scripts/certs/makeCert.sh @@ -72,6 +72,7 @@ else fi CRYPTO_SETTINGS="" +FIPS_SETTINGS="" openssl list -providers 2>&1 | grep "\(invalid command\|unknown option\)" >/dev/null if [ $? -ne 0 ] ; then @@ -85,7 +86,7 @@ do echo "$var" if [ "$var" == "-fips" ] || [ "$var" == "--fips" ];then fips=true - CRYPTO_SETTINGS='-macalg SHA256 -aes256 -descert -keypbe AES-256-CBC -certpbe AES-256-CBC' + FIPS_SETTINGS='-macalg SHA256 -aes256 -descert -keypbe AES-256-CBC -certpbe AES-256-CBC' fi done @@ -114,17 +115,17 @@ cat ca-trusted.pem >> "${SNAME}"-trusted.pem # now make pkcs12 and jks keystore files if [[ "$1" == "server" || "$1" == "client" || "$1" == "dbclient" ]]; then - openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}".pem -inkey "${SNAME}".key -out "${SNAME}".p12 -name "${SNAME}" -CAfile ca.pem -passin pass:${PASS} -passout pass:${PASS} + openssl pkcs12 ${CRYPTO_SETTINGS} ${FIPS_SETTINGS} -export -in "${SNAME}".pem -inkey "${SNAME}".key -out "${SNAME}".p12 -name "${SNAME}" -CAfile ca.pem -passin pass:${PASS} -passout pass:${PASS} keytool -importkeystore -deststorepass "${PASS}" -destkeypass "${PASS}" -destkeystore "${SNAME}".jks -srckeystore "${SNAME}".p12 -srcstoretype PKCS12 -srcstorepass "${PASS}" -alias "${SNAME}" else # a CA if [ "$fips" = true ];then - openssl pkcs12 -legacy -export -in "${SNAME}"-trusted.pem -out truststore-"${SNAME}"-legacy.p12 -nokeys -passout pass:${CAPASS} + openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}"-trusted.pem -out truststore-"${SNAME}"-legacy.p12 -nokeys -passout pass:${CAPASS} fi - openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}"-trusted.pem -out truststore-"${SNAME}".p12 -nokeys -passout pass:${CAPASS} + openssl pkcs12 ${CRYPTO_SETTINGS} ${FIPS_SETTINGS} ${FIPS_SETTINGS} -export -in "${SNAME}"-trusted.pem -out truststore-"${SNAME}".p12 -nokeys -passout pass:${CAPASS} keytool -import -trustcacerts -file "${SNAME}".pem -keystore truststore-"${SNAME}".jks -storepass "${CAPASS}" -noprompt # include a CA signing keystore; NOT FOR DISTRIBUTION TO CLIENTS - openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}".pem -inkey "${SNAME}".key -out "${SNAME}"-signing.p12 -name "${SNAME}" -passin pass:${CAPASS} -passout pass:${CAPASS} + openssl pkcs12 ${CRYPTO_SETTINGS} ${FIPS_SETTINGS} -export -in "${SNAME}".pem -inkey "${SNAME}".key -out "${SNAME}"-signing.p12 -name "${SNAME}" -passin pass:${CAPASS} -passout pass:${CAPASS} keytool -importkeystore -deststorepass "${CAPASS}" -destkeypass "${CAPASS}" -destkeystore "${SNAME}"-signing.jks -srckeystore "${SNAME}"-signing.p12 -srcstoretype PKCS12 -srcstorepass "${CAPASS}" -alias "${SNAME}" ## create empty crl diff --git a/src/takserver-core/scripts/certs/makeRootCa.sh b/src/takserver-core/scripts/certs/makeRootCa.sh index 2966f1d4..a5df2536 100755 --- a/src/takserver-core/scripts/certs/makeRootCa.sh +++ b/src/takserver-core/scripts/certs/makeRootCa.sh @@ -32,7 +32,7 @@ if [[ "$canamelen" -lt 5 ]]; then fi CRYPTO_SETTINGS="" - +FIPS_SETTINGS="" openssl list -providers 2>&1 | grep "\(invalid command\|unknown option\)" >/dev/null if [ $? -ne 0 ] ; then echo "Using legacy provider" @@ -44,7 +44,7 @@ for var in "$@" do if [ "$var" == "-fips" ] || [ "$var" == "--fips" ];then fips=true - CRYPTO_SETTINGS='-macalg SHA256 -aes256 -descert -keypbe AES-256-CBC -certpbe AES-256-CBC' + FIPS_SETTINGS='-macalg SHA256 -aes256 -descert -keypbe AES-256-CBC -certpbe AES-256-CBC' fi done @@ -54,9 +54,9 @@ openssl req -new -sha256 -x509 -days 3652 -extensions v3_ca -keyout ca-do-not-sh openssl x509 -in ca.pem -addtrust clientAuth -addtrust serverAuth -setalias "${CA_NAME}" -out ca-trusted.pem if [ "$fips" = true ];then - openssl pkcs12 -legacy -export -in ca-trusted.pem -out truststore-root-legacy.p12 -nokeys -caname "${CA_NAME}" -passout pass:${CAPASS} + openssl pkcs12 ${CRYPTO_SETTINGS} -export -in ca-trusted.pem -out truststore-root-legacy.p12 -nokeys -caname "${CA_NAME}" -passout pass:${CAPASS} fi -openssl pkcs12 ${CRYPTO_SETTINGS} -export -in ca-trusted.pem -out truststore-root.p12 -nokeys -caname "${CA_NAME}" -passout pass:${CAPASS} +openssl pkcs12 ${CRYPTO_SETTINGS} ${FIPS_SETTINGS} -export -in ca-trusted.pem -out truststore-root.p12 -nokeys -caname "${CA_NAME}" -passout pass:${CAPASS} keytool -import -trustcacerts -file ca.pem -keystore truststore-root.jks -alias "${CA_NAME}" -storepass "${CAPASS}" -noprompt cp truststore-root.jks fed-truststore.jks @@ -74,7 +74,4 @@ if ! $(grep -q unique_subject crl_index.txt.attr); then echo "unique_subject = no" >> crl_index.txt.attr fi -openssl ca -config ../config.cfg -gencrl -keyfile ca-do-not-share.key $KEYPASS -cert ca.pem -out ca.crl - - - +openssl ca -config ../config.cfg -gencrl -keyfile ca-do-not-share.key $KEYPASS -cert ca.pem -out ca.crl \ No newline at end of file diff --git a/src/takserver-core/scripts/config/takserver-config b/src/takserver-core/scripts/config/takserver-config index 537031f2..055c9f94 100644 --- a/src/takserver-core/scripts/config/takserver-config +++ b/src/takserver-core/scripts/config/takserver-config @@ -33,7 +33,7 @@ TAK_HOME=/opt/tak case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${TAK_HOME} && ./takserver-config.sh &> /opt/tak/logs/takserver-config-console.log &" + su tak -c "cd ${TAK_HOME} && ./takserver-config.sh > /opt/tak/logs/takserver-config-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/takserver-core/scripts/messaging-readiness.sh b/src/takserver-core/scripts/messaging-readiness.sh index 4951e70e..375be58d 100755 --- a/src/takserver-core/scripts/messaging-readiness.sh +++ b/src/takserver-core/scripts/messaging-readiness.sh @@ -1,2 +1,9 @@ #!/bin/sh + +set -e + +# Check if the authentidation data has been created cat 'UserAuthenticationFile.xml' | grep -q 'ROLE_ADMIN' + +# Check if the postgres host is available +nc -zw3 $POSTGRES_HOST $POSTGRES_PORT \ No newline at end of file diff --git a/src/takserver-core/scripts/messaging/takserver-messaging b/src/takserver-core/scripts/messaging/takserver-messaging index 4a02d1ce..1b909639 100644 --- a/src/takserver-core/scripts/messaging/takserver-messaging +++ b/src/takserver-core/scripts/messaging/takserver-messaging @@ -35,7 +35,7 @@ TAK_HOME=/opt/tak case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${TAK_HOME} && ./takserver-messaging.sh &> /opt/tak/logs/takserver-messaging-console.log &" + su tak -c "cd ${TAK_HOME} && ./takserver-messaging.sh > /opt/tak/logs/takserver-messaging-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/takserver-core/scripts/plugins/takserver-plugins b/src/takserver-core/scripts/plugins/takserver-plugins index 48301e77..936e4b4f 100644 --- a/src/takserver-core/scripts/plugins/takserver-plugins +++ b/src/takserver-core/scripts/plugins/takserver-plugins @@ -33,7 +33,7 @@ TAK_HOME=/opt/tak case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${TAK_HOME} && ./takserver-plugins.sh &> /opt/tak/logs/takserver-plugins-console.log &" + su tak -c "cd ${TAK_HOME} && ./takserver-plugins.sh > /opt/tak/logs/takserver-plugins-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/takserver-core/scripts/retention/takserver-retention b/src/takserver-core/scripts/retention/takserver-retention index a4fb11b2..f0c07766 100644 --- a/src/takserver-core/scripts/retention/takserver-retention +++ b/src/takserver-core/scripts/retention/takserver-retention @@ -33,7 +33,7 @@ TAK_HOME=/opt/tak case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${TAK_HOME} && ./takserver-retention.sh &> /opt/tak/logs/takserver-retention-console.log &" + su tak -c "cd ${TAK_HOME} && ./takserver-retention.sh > /opt/tak/logs/takserver-retention-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/takserver-core/scripts/utils/takserver-combined b/src/takserver-core/scripts/utils/takserver-combined index 74b852eb..439906e7 100644 --- a/src/takserver-core/scripts/utils/takserver-combined +++ b/src/takserver-core/scripts/utils/takserver-combined @@ -35,7 +35,7 @@ TAK_HOME=/opt/tak case "$1" in start) echo -n "Starting $SERVICE: " - su tak -c "cd ${TAK_HOME} && ./takserver.sh &> /opt/tak/logs/takserver-console.log &" + su tak -c "cd ${TAK_HOME} && ./takserver.sh > /opt/tak/logs/takserver-console.log 2>&1 &" if [ $? -eq 0 ]; then echo "OK" else diff --git a/src/takserver-core/src/main/java/com/bbn/cot/filter/DataFeedFilter.java b/src/takserver-core/src/main/java/com/bbn/cot/filter/DataFeedFilter.java index 5f3ff4cc..98b5ab4b 100644 --- a/src/takserver-core/src/main/java/com/bbn/cot/filter/DataFeedFilter.java +++ b/src/takserver-core/src/main/java/com/bbn/cot/filter/DataFeedFilter.java @@ -49,7 +49,7 @@ public class DataFeedFilter { private static final Logger logger = LoggerFactory.getLogger(DataFeedFilter.class); - private static DataFeedFilter instance = null; + private volatile static DataFeedFilter instance = null; @Autowired private DataFeedService dataFeedService; diff --git a/src/takserver-core/src/main/java/com/bbn/cot/filter/StreamingEndpointRewriteFilter.java b/src/takserver-core/src/main/java/com/bbn/cot/filter/StreamingEndpointRewriteFilter.java index a6c1d492..77f4fd5d 100644 --- a/src/takserver-core/src/main/java/com/bbn/cot/filter/StreamingEndpointRewriteFilter.java +++ b/src/takserver-core/src/main/java/com/bbn/cot/filter/StreamingEndpointRewriteFilter.java @@ -100,16 +100,22 @@ public CotEventContainer filter(final CotEventContainer cot) { } if (config.getFilter().getStreamingbroker().isEnable()) { - List destList = cot.getDocument().selectNodes(DEST_XPATH); + List destList = cot.getDocument().selectNodes(DEST_XPATH); // XML node with all the dests if (destList.size() > 0) { List publishList = new LinkedList(); List callsignList = new LinkedList(); Set uids = new HashSet<>(); + + // Mission name supporting data structures Set missionNames = new HashSet<>(); + Map missionNamePathMap = new HashMap<>(); + Map missionNameAfterMap = new HashMap<>(); + + // Mission guid supporting data structures Set missionGuids = new HashSet<>(); - Map missionPathMap = new HashMap<>(); - Map missionAfterMap = new HashMap<>(); + Map missionGuidPathMap = new HashMap<>(); + Map missionGuidAfterMap = new HashMap<>(); String clientUid = ""; try { @@ -162,9 +168,9 @@ public CotEventContainer filter(final CotEventContainer cot) { logger.debug("mission destination specified in message: {}", detached.attributeValue(MISSION_ATTR)); if (detached.attribute(PATH_ATTR) != null) { - missionPathMap.put(detached.attributeValue(MISSION_ATTR), detached.attributeValue(PATH_ATTR)); + missionNamePathMap.put(detached.attributeValue(MISSION_ATTR), detached.attributeValue(PATH_ATTR)); if (detached.attribute(AFTER_ATTR) != null) { - missionAfterMap.put(detached.attributeValue(MISSION_ATTR), detached.attributeValue(AFTER_ATTR)); + missionNameAfterMap.put(detached.attributeValue(MISSION_ATTR), detached.attributeValue(AFTER_ATTR)); } } } else if (detached.attribute(MISSION_ATTR_GUID) != null) { @@ -174,26 +180,26 @@ public CotEventContainer filter(final CotEventContainer cot) { logger.debug("mission guid string in message {}", guidString); // parse UUID - UUID missionUuid = null; + UUID missionGuid = null; try { - missionUuid = UUID.fromString(guidString); + missionGuid = UUID.fromString(guidString); } catch (IllegalArgumentException e) { logger.warn("invalid mission guid in streaming message {}", guidString); } - if (missionUuid != null) { + if (missionGuid != null) { - missionGuids.add(missionUuid); + missionGuids.add(missionGuid); - logger.debug("mission destination specified in message: {}", missionUuid); + logger.debug("mission destination specified in message: {}", missionGuid); if (detached.attribute(PATH_ATTR) != null) { - missionPathMap.put(detached.attributeValue(MISSION_ATTR_GUID), detached.attributeValue(PATH_ATTR)); + missionGuidPathMap.put(missionGuid, detached.attributeValue(PATH_ATTR)); if (detached.attribute(AFTER_ATTR) != null) { - missionAfterMap.put(detached.attributeValue(MISSION_ATTR_GUID), detached.attributeValue(AFTER_ATTR)); + missionGuidAfterMap.put(missionGuid, detached.attributeValue(AFTER_ATTR)); } } } @@ -230,8 +236,8 @@ public CotEventContainer filter(final CotEventContainer cot) { logger.debug("explicit callsigns for message " + cot.getUid() + " " + callsignList); // use thread pool? - processTracksByMissionName(cot, missionNames, clientUid, uids, missionPathMap, missionAfterMap); - processTracksByMissionGuid(cot, missionGuids, clientUid, uids, missionPathMap, missionAfterMap); + processTracksByMissionName(cot, missionNames, clientUid, uids, missionNamePathMap, missionNameAfterMap); + processTracksByMissionGuid(cot, missionGuids, clientUid, uids, missionGuidPathMap, missionGuidAfterMap); if (uids.size() > 0) { cot.setContextValue(EXPLICIT_UID_KEY, new ArrayList(uids)); @@ -440,187 +446,191 @@ private void processTracksByMissionGuid( final CotEventContainer cot, Set missionGuids, String clientUid, Set uids, - Map missionPathMap, // map of , ? - Map missionAfterMap) { + Map missionGuidPathMap, // map of , + Map missionGuidAfterMap) { + + + Configuration config = CoreConfigFacade.getInstance().getRemoteConfiguration(); + + // add the client uid for each mission subscriber to the explicit uid list + try { + + String groupVector = RemoteUtil.getInstance().bitVectorToString( + RemoteUtil.getInstance().getBitVectorForGroups( + (NavigableSet)cot.getContext(Constants.GROUPS_KEY))); + + for (final UUID missionGuid : missionGuids) { + + MissionSubscription missionSubscription = null; + User user = (User) cot.getContextValue(Constants.USER_KEY); + if (user != null) { + + missionSubscription = missionService + .getMissionSubcriptionByMissionGuidAndClientUidAndUsernameNoMission(missionGuid.toString(), clientUid, user.getName()); + + if (missionSubscription == null + && user.getCert() != null && user.getCert().getSubjectX500Principal() != null) { + // lookup the mission subscription based on CN, needed when input auth=ldap or auth=file + String cn = new LdapName(user.getCert().getSubjectX500Principal().getName()) + .getRdns().stream().filter(i -> i.getType().equalsIgnoreCase("CN")) + .findFirst().get().getValue().toString(); + missionSubscription = missionService + .getMissionSubcriptionByMissionGuidAndClientUidAndUsernameNoMission( + missionGuid.toString(), clientUid, cn); + } + + } else { + missionSubscription = missionService.getMissionSubscriptionByMissionGuidAndClientUidNoMission( + missionGuid.toString(), clientUid); + } + + if (missionSubscription == null) { + logger.error("unable to find mission subscription for mission with guid {} for client {} ", missionGuid, clientUid); + continue; + } else { + changeLogger.debug("mission sub for explcit mission sender to mission guid {} for subscription {}.", missionGuid.toString(), missionSubscription); + } -// Configuration config = CoreConfigFacade.getInstance().getRemoteConfiguration(); -// -// // add the client uid for each mission subscriber to the explicit uid list -// try { -// -// String groupVector = RemoteUtil.getInstance().bitVectorToString( -// RemoteUtil.getInstance().getBitVectorForGroups( -// (NavigableSet)cot.getContext(Constants.GROUPS_KEY))); -// -// for (final UUID missionGuid : missionGuids) { -// -// // TODO refactor this into a method -// -// MissionSubscription missionSubscription = null; -// User user = (User) cot.getContextValue(Constants.USER_KEY); -// if (user != null) { -// missionSubscription = missionService -// .getMissionSubcriptionByMissionNameAndClientUidAndUsernameNoMission( -// missionGuid.toString(), clientUid, user.getName()); -// -// if (missionSubscription == null -// && user.getCert() != null && user.getCert().getSubjectX500Principal() != null) { -// // lookup the mission subscription based on CN, needed when input auth=ldap or auth=file -// String cn = new LdapName(user.getCert().getSubjectX500Principal().getName()) -// .getRdns().stream().filter(i -> i.getType().equalsIgnoreCase("CN")) -// .findFirst().get().getValue().toString(); -// missionSubscription = missionService -// .getMissionSubcriptionByMissionGuidAndClientUidAndUsernameNoMission( -// missionGuid.toString(), clientUid, cn); -// } -// -// } else { -// missionSubscription = missionService.getMissionSubscriptionByMissionGuidAndClientUidNoMission( -// missionGuid.toString(), clientUid); -// } -// -// if (missionSubscription == null) { -// logger.error("unable to find mission subscription for client {}, {} ", missionGuid, clientUid); -// continue; -// } else { -// changeLogger.debug("mission sub for explcit mission sender to {}: {}", missionGuid, missionSubscription); -// } -// -// if (missionSubscription.getRole() != null && !missionSubscription.getRole(). -// hasPermission(MissionPermission.Permission.MISSION_WRITE)) { -// logger.error("Illegal attempt to adding streaming content to mission!"); -// continue; -// } -// -// for (String missionClientUid : subscriptionManager.getMissionSubscriptions(missionGuid, true)) { -// // don't send the event back to the submitter -// if (clientUid != null && clientUid.compareTo(missionClientUid) == 0) { -// continue; -// } -// -// uids.add(missionClientUid); -// } -// -// if (changeLogger.isDebugEnabled()) { -// changeLogger.debug("mission client uid for mission sub count: " + uids.size()); -// } -// -// // final copy of variable to use in inner class -// final String finClientUid = clientUid; -// final String finGroupVector = groupVector; -// -// // TODO: refactor this for performance checks, separation of concerns -// CotEventContainer copyCot = cot.copy(); -// -// final String fclientUid = clientUid; -// -// if (changeLogger.isDebugEnabled()) { -// changeLogger.debug("sending change to executor for clientUid: " + fclientUid + " and add uid: " + copyCot.getUid()); -// } -// -//// Resources.missionContentProcessor.execute(() -> { -// // Don't add content if this message came over NATS. It was already added on the origin node -// if (!cot.hasContextKey(Constants.NATS_MESSAGE_KEY)) { -// try { -// MissionContent missionContent = new MissionContent(); -// missionContent.getUids().add(copyCot.getUid()); -// -// if (missionPathMap.containsKey(missionName)) { -// -// if (missionAfterMap.containsKey(missionName)) { -// missionContent.setAfter(missionAfterMap.get(missionName)); -// } -// -// MissionContent pathContent = new MissionContent(); -// pathContent.getOrCreatePaths().put( -// missionPathMap.get(missionName), Arrays.asList(missionContent)); -// missionContent = pathContent; -// } -// -// missionService.addMissionContent(missionName, missionContent, finClientUid, finGroupVector); -// -// //TODO - add case here for mission guid. Where to get it -// } catch (Exception e) { -// logger.error("exception adding content uid to mission " + e.getMessage(), e); -// } -// } -// -// if (config.getFederation().isEnableFederation() -// && config.getFederation().isAllowMissionFederation()) { -// // federate this mission update (subject to group filtering) -// try { -// -// NavigableSet groups = null; -// -// String uid = cot.getUid(); -// -// if (Strings.isNullOrEmpty(uid)) { -// throw new IllegalArgumentException("empty uid in cot for mission content add"); -// } -// -// MissionContent content = new MissionContent(); -// content.getUids().add(uid); -// -// if (cot.getContextValue(Constants.GROUPS_KEY) != null) { -// try { -// groups = (NavigableSet) cot.getContextValue(Constants.GROUPS_KEY); -// -// if (logger.isDebugEnabled()) { -// logger.debug("groups for message: " + cot + ": " + groups); -// } -// -// } catch (ClassCastException e) { -// logger.debug("Not trying to get group info for message with invalid type of groups object: " + cot); -// } -// } else { -// if (logger.isDebugEnabled()) { -// logger.debug("Groups context key not set for message: " + cot); -// } -// } -// -// if (groups != null) { -// -// MissionMetadata mission = repositoryService.getMissionMetadata(missionName); -// -// if (mission == null) { -// logger.debug("nothing to federate for non-existent mission {}", missionName); -// return; -// } -// -// -// if (config.getFederation().isFederateOnlyPublicMissions()) { -// if ("public".equals(mission.getTool())) { -// // allow public. no action needed as of now -// } else if (config.getNetwork().getMissionCopTool().equals(mission.getTool())) { -// if (!config.getVbm().isEnabled()) { -// logger.debug("not federating vbm mission action for mission " + missionName + " since vbm is disabled"); -// return; -// } -// } else { -// logger.debug("not federating non-public mission action for mission " + missionName); -// return; -// } -// } -// -// ROL rol = RemoteUtil.getInstance().getROLforMissionChange(content, missionName, fclientUid, mission.getCreatorUid(), mission.getChatRoom(), mission.getTool(), mission.getDescription()); -// -// if (logger.isDebugEnabled()) { -// logger.debug("rol to federate for mission change " + rol + " to groups " + groups); -// } -// -// federationManager.submitMissionFederateROL(rol, groups, missionName); -// } else { -// logger.warn("unable to federate mission uid add - cot message specified no groups"); -// } -// -// } catch (Exception e) { -// logger.debug("exception adding content uid to mission " + e.getMessage(), e); -// } -// } -//// }); -// } -// } catch (Exception e) { -// logger.debug("exception getting mission subscriber uids: " + e.getMessage(), e); -// } + if (missionSubscription.getRole() != null && !missionSubscription.getRole(). + hasPermission(MissionPermission.Permission.MISSION_WRITE)) { + logger.error("Illegal attempt to adding streaming content to mission!"); + continue; + } + + for (String missionClientUid : subscriptionManager.getMissionSubscriptions(missionGuid, true)) { + // don't send the event back to the submitter + if (clientUid != null && clientUid.compareTo(missionClientUid) == 0) { + continue; + } + + uids.add(missionClientUid); + } + + if (changeLogger.isDebugEnabled()) { + changeLogger.debug("mission client uid for mission sub count: " + uids.size()); + } + + // final copy of variable to use in inner class + final String finClientUid = clientUid; + final String finGroupVector = groupVector; + + // TODO: refactor this for performance checks, separation of concerns + CotEventContainer copyCot = cot.copy(); + + final String fclientUid = clientUid; + + if (changeLogger.isDebugEnabled()) { + changeLogger.debug("sending change to executor for clientUid: " + fclientUid + " and add uid: " + copyCot.getUid()); + } + + // Don't add content if this message came over NATS. It was already added on the origin node + if (!cot.hasContextKey(Constants.NATS_MESSAGE_KEY)) { + try { + MissionContent missionContent = new MissionContent(); + missionContent.getUids().add(copyCot.getUid()); + + if (missionGuidPathMap.containsKey(missionGuid)) { + + if (missionGuidAfterMap.containsKey(missionGuid)) { + missionContent.setAfter(missionGuidAfterMap.get(missionGuid)); + } + + MissionContent pathContent = new MissionContent(); + pathContent.getOrCreatePaths().put( + missionGuidPathMap.get(missionGuid), Arrays.asList(missionContent)); + missionContent = pathContent; + } + + missionService.addMissionContent(missionGuid, missionContent, finClientUid, finGroupVector); + + } catch (Exception e) { + logger.error("exception adding content uid to mission " + e.getMessage(), e); + } + } + + if (config.getFederation().isEnableFederation() + && config.getFederation().isAllowMissionFederation()) { + // federate this mission update (subject to group filtering) + try { + + NavigableSet groups = null; + + String uid = cot.getUid(); + + if (Strings.isNullOrEmpty(uid)) { + throw new IllegalArgumentException("empty uid in cot for mission content add"); + } + + MissionContent content = new MissionContent(); + content.getUids().add(uid); + + if (cot.getContextValue(Constants.GROUPS_KEY) != null) { + try { + groups = (NavigableSet) cot.getContextValue(Constants.GROUPS_KEY); + + if (logger.isDebugEnabled()) { + logger.debug("groups for message: " + cot + ": " + groups); + } + + } catch (ClassCastException e) { + logger.debug("Not trying to get group info for message with invalid type of groups object: " + cot); + } + } else { + if (logger.isDebugEnabled()) { + logger.debug("Groups context key not set for message: " + cot); + } + } + + if (groups != null) { + + MissionMetadata mission = repositoryService.getMissionMetadataByGuid(missionGuid); + + if (mission == null) { + logger.debug("nothing to federate for non-existent mission {}", missionGuid); + return; + } + + + if (config.getFederation().isFederateOnlyPublicMissions()) { + if ("public".equals(mission.getTool())) { + // allow public. no action needed as of now + } else if (config.getNetwork().getMissionCopTool().equals(mission.getTool())) { + if (!config.getVbm().isEnabled()) { + logger.debug("not federating vbm mission action for mission {} since vbm is disabled", missionGuid); + return; + } + } else { + logger.debug("not federating non-public mission action for mission {} ", missionGuid); + return; + } + } + + String missionName = missionService.getMissionNameByGuid(missionGuid); + + if (!Strings.isNullOrEmpty(missionName)) { + + // TODO: when implementing support for federating missions by guid, update this codepath to include the mission guid along with the mission name + ROL rol = RemoteUtil.getInstance().getROLforMissionChange(content, missionName, fclientUid, mission.getCreatorUid(), mission.getChatRoom(), mission.getTool(), mission.getDescription()); + + if (logger.isDebugEnabled()) { + logger.debug("rol to federate for mission change " + rol + " to groups " + groups); + } + + federationManager.submitMissionFederateROL(rol, groups, missionName); + } else { + logger.warn("no mission name found for mission guid {}. Nothing to federate", missionGuid); + } + } else { + logger.warn("unable to federate mission uid add - cot message specified no groups"); + } + + } catch (Exception e) { + logger.debug("exception adding content uid to mission " + e.getMessage(), e); + } + } + } + } catch (Exception e) { + logger.debug("exception getting mission subscriber uids: " + e.getMessage(), e); + } } } diff --git a/src/takserver-core/src/main/java/com/bbn/file/FileConfigurationApi.java b/src/takserver-core/src/main/java/com/bbn/file/FileConfigurationApi.java index b3fd71c1..bcb40b58 100644 --- a/src/takserver-core/src/main/java/com/bbn/file/FileConfigurationApi.java +++ b/src/takserver-core/src/main/java/com/bbn/file/FileConfigurationApi.java @@ -40,7 +40,7 @@ public void setFileConfiguration(@RequestBody FileConfigurationModel fileConfigu } try { - CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().setEnterpriseSyncSizeLimitMB(fileConfigurationModel.getUploadSizeLimit()); + CoreConfigFacade.getInstance().setAndSaveEnterpriseSyncSizeLimit(fileConfigurationModel.getUploadSizeLimit()); CoreConfigFacade.getInstance().saveChangesAndUpdateCache(); } catch (Exception e) { logger.error("Error in setFileConfiguration: ", e); diff --git a/src/takserver-core/src/main/java/com/bbn/marti/groups/LdapSSLSocketFactory.java b/src/takserver-core/src/main/java/com/bbn/marti/groups/LdapSSLSocketFactory.java index 4f669085..0c6e0112 100644 --- a/src/takserver-core/src/main/java/com/bbn/marti/groups/LdapSSLSocketFactory.java +++ b/src/takserver-core/src/main/java/com/bbn/marti/groups/LdapSSLSocketFactory.java @@ -76,6 +76,11 @@ public LdapSSLSocketFactory() { } } + @Override + public Socket createSocket() throws IOException { + return socketFactory.createSocket(); + } + @Override public Socket createSocket(Socket socket, String host, int port, boolean autoClose) throws IOException { return socketFactory.createSocket(socket, host, port, autoClose); diff --git a/src/takserver-core/src/main/java/com/bbn/marti/groups/X509Authenticator.java b/src/takserver-core/src/main/java/com/bbn/marti/groups/X509Authenticator.java index d0e56c18..d059e8a1 100644 --- a/src/takserver-core/src/main/java/com/bbn/marti/groups/X509Authenticator.java +++ b/src/takserver-core/src/main/java/com/bbn/marti/groups/X509Authenticator.java @@ -137,7 +137,7 @@ private User auth(User user, Input input) { if (CoreConfigFacade.getInstance().getRemoteConfiguration().getAuth().isX509TokenAuth() && cert != null && cert.token != null && cert.token.length() > 0) { - user.setName(cert.token); + user.setToken(cert.token); try { groupManager.authenticate("oauth", user); } catch (InvalidBearerTokenException | JwtException e) { diff --git a/src/takserver-core/src/main/java/com/bbn/marti/service/DistributedSubscriptionManager.java b/src/takserver-core/src/main/java/com/bbn/marti/service/DistributedSubscriptionManager.java index a8ec78eb..17b1f68e 100644 --- a/src/takserver-core/src/main/java/com/bbn/marti/service/DistributedSubscriptionManager.java +++ b/src/takserver-core/src/main/java/com/bbn/marti/service/DistributedSubscriptionManager.java @@ -135,7 +135,7 @@ public class DistributedSubscriptionManager implements SubscriptionManager, org. private final Logger changeLogger = LoggerFactory.getLogger(Constants.CHANGE_LOGGER); - private Validator validator; + private volatile Validator validator; private boolean applyClassificationFilter = false; @@ -145,7 +145,7 @@ public DistributedSubscriptionManager() { } } - private static DistributedSubscriptionManager instance = null; + private volatile static DistributedSubscriptionManager instance = null; public static synchronized DistributedSubscriptionManager getInstance() { if (instance == null) { @@ -502,7 +502,8 @@ public Collection getMatches(CotEventContainer c) { } // send a delivery failure notification if the chat message recipient is offline - if (reachableMatches.isEmpty() && c.getType().startsWith("b-t-f")) { + if (reachableMatches.isEmpty() && c.getType().startsWith("b-t-f") + && !c.getType().equals("b-t-f-s")) { Subscription senderSub = getSubscription(sender); messagingUtil().sendDeliveryFailure(senderSub.clientUid, c); } @@ -950,8 +951,8 @@ synchronized public void addSubscription(RemoteSubscription remote) { tokens[1], remote.iface, Integer.parseInt(tokens[2]), remote.xpath, remote.uid, remote.filterGroups, remote.toStatic().getFilter()); - CoreConfigFacade.getInstance().getRemoteConfiguration().getSubscription().getStatic().add(remote.toStatic()); - CoreConfigFacade.getInstance().saveChangesAndUpdateCache(); + CoreConfigFacade.getInstance().addStaticSubscriptionAndSave(remote.toStatic()); + } catch (Exception e) { throw new TakException("Error adding subscription", e); } @@ -1100,14 +1101,7 @@ public boolean deleteSubscription(String uid) { } private void checkAndDeleteStaticSubscription(String uid) { - List staticSubs = CoreConfigFacade.getInstance().getRemoteConfiguration().getSubscription().getStatic(); - for(com.bbn.marti.config.Subscription.Static ss : staticSubs) { - if (ss.getName().compareTo(uid) == 0) { - staticSubs.remove(ss); - CoreConfigFacade.getInstance().saveChangesAndUpdateCache(); - break; - } - } + CoreConfigFacade.getInstance().removeStaticSubscriptionAndSave(uid); } private boolean doDeleteSubscription(Subscription sub, String uid) { diff --git a/src/takserver-core/src/main/java/com/bbn/marti/service/RepositoryService.java b/src/takserver-core/src/main/java/com/bbn/marti/service/RepositoryService.java index d7f858cf..15bf3762 100755 --- a/src/takserver-core/src/main/java/com/bbn/marti/service/RepositoryService.java +++ b/src/takserver-core/src/main/java/com/bbn/marti/service/RepositoryService.java @@ -988,6 +988,41 @@ public MissionMetadata getMissionMetadata(String missionName) { return mm; } + + @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") + public MissionMetadata getMissionMetadataByGuid(UUID missionGuid) { + if (missionGuid == null) { + throw new IllegalArgumentException("empty mission guid"); + } + + MissionMetadata mm = new MissionMetadata(); + + try (Connection connection = getConnection(); PreparedStatement ps = connection.prepareStatement("select creatoruid, chatroom, tool, description, name from mission where guid = uuid(?)")) { + ps.setString(1, missionGuid.toString()); + try (ResultSet rs = ps.executeQuery()) { + + if (rs.next()) { + mm.setGuid(missionGuid); + mm.setCreatorUid(rs.getString(1)); + mm.setChatRoom(rs.getString(2)); + mm.setTool(rs.getString(3)); + mm.setDescription(rs.getString(4)); + mm.setName(rs.getString(5)); + } else { + throw new TakException("mission for guid " + missionGuid + " does not exist"); + } + } + + } catch (Exception e) { + throw new TakException("Exception getting mission metadata from database for mission " + missionGuid, e); + } + + if (log.isDebugEnabled()) { + log.debug("retrieved " + mm + " for mission name " + missionGuid); + } + + return mm; + } public byte[] getContentByHash(String hash) throws SQLException, NamingException { byte[] result = null; diff --git a/src/takserver-core/src/main/java/com/bbn/marti/service/SubmissionService.java b/src/takserver-core/src/main/java/com/bbn/marti/service/SubmissionService.java index f1f7eff3..bed83816 100644 --- a/src/takserver-core/src/main/java/com/bbn/marti/service/SubmissionService.java +++ b/src/takserver-core/src/main/java/com/bbn/marti/service/SubmissionService.java @@ -152,6 +152,7 @@ public class SubmissionService extends BaseService implements MessagingConfigura private static final Logger logger = LoggerFactory.getLogger(SubmissionService.class); private static final Logger dlogger = LoggerFactory.getLogger("DistributedSubmissionServiceReceiver"); private static final Logger dupeLogger = LoggerFactory.getLogger("dupe-message-logger"); + public static String LINK_XPATH = "/event/detail/link"; public static final String CHANNEL_TYPE_KEY = "channel.type"; public static enum ChannelType {FED_V1, FED_V2, COT}; @@ -647,7 +648,26 @@ public void init() throws IOException, DocumentException { logger.debug("Convert Message to Plugin CotEventContainer."); } CotEventContainer pluginCotEvent = messageConverter.dataMessageToCot(m, false); - + + if (!Strings.isNullOrEmpty(pluginCotEvent.getCallsign()) + && !Strings.isNullOrEmpty(pluginCotEvent.getEndpoint()) && !Strings.isNullOrEmpty(pluginCotEvent.getUid())) { + if (config.getFederation() != null) { + try { + NavigableSet groups = (NavigableSet) pluginCotEvent.getContextValue(Constants.GROUPS_KEY); + federationManager.addPluginContact(pluginCotEvent, groups); + } catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("exception adding federated plugin contact", e); + } + } + } + } else if (pluginCotEvent.getType().equals("t-x-d-d")) { + Element link = (Element)pluginCotEvent.getDocument().selectSingleNode(LINK_XPATH); + if (link != null && link.attribute("uid") != null) { + federationManager.removeLocalContact(link.attributeValue("uid")); + } + } + if (isDataFeedMessage) { if (logger.isDebugEnabled()) { diff --git a/src/takserver-core/src/main/java/com/bbn/marti/util/MessagingDependencyInjectionProxy.java b/src/takserver-core/src/main/java/com/bbn/marti/util/MessagingDependencyInjectionProxy.java index 15471672..241ccb62 100644 --- a/src/takserver-core/src/main/java/com/bbn/marti/util/MessagingDependencyInjectionProxy.java +++ b/src/takserver-core/src/main/java/com/bbn/marti/util/MessagingDependencyInjectionProxy.java @@ -49,7 +49,7 @@ public class MessagingDependencyInjectionProxy implements ApplicationContextAware { private static ApplicationContext springContext; - private static MessagingDependencyInjectionProxy instance = null; + private volatile static MessagingDependencyInjectionProxy instance = null; public static MessagingDependencyInjectionProxy getInstance() { if (instance == null) { @@ -73,7 +73,7 @@ public void setApplicationContext(ApplicationContext context) throws BeansExcept this.springContext = context; } - private GroupManager groupManager = null; + private volatile GroupManager groupManager = null; public GroupManager groupManager() { if (groupManager == null) { @@ -87,7 +87,7 @@ public GroupManager groupManager() { return groupManager; } - private GroupDao groupDao = null; + private volatile GroupDao groupDao = null; public GroupDao groupDao() { if (groupDao == null) { @@ -101,7 +101,7 @@ public GroupDao groupDao() { return groupDao; } - private GroupStore groupStore = null; + private volatile GroupStore groupStore = null; public GroupStore groupStore() { if (groupStore == null) { @@ -115,7 +115,7 @@ public GroupStore groupStore() { return groupStore; } - private CoreConfig coreConfig = null; + private volatile CoreConfig coreConfig = null; public CoreConfig coreConfig() { if (coreConfig == null) { @@ -129,7 +129,7 @@ public CoreConfig coreConfig() { return coreConfig; } - private CoreConfig simpleCoreConfig = null; + private volatile CoreConfig simpleCoreConfig = null; public CoreConfig simpleCoreConfig() { if (simpleCoreConfig == null) { @@ -143,7 +143,7 @@ public CoreConfig simpleCoreConfig() { return simpleCoreConfig; } - private SubscriptionStore subStore = null; + private volatile SubscriptionStore subStore = null; public SubscriptionStore subscriptionStore() { if (subStore == null) { @@ -157,7 +157,7 @@ public SubscriptionStore subscriptionStore() { return subStore; } - private MissionSubscriptionRepository msr = null; + private volatile MissionSubscriptionRepository msr = null; public MissionSubscriptionRepository missionSubscriptionRepository() { if (msr == null) { @@ -171,7 +171,7 @@ public MissionSubscriptionRepository missionSubscriptionRepository() { return msr; } - private SubmissionService submissionService = null; + private volatile SubmissionService submissionService = null; public SubmissionService submissionService() { if (submissionService == null) { @@ -185,7 +185,7 @@ public SubmissionService submissionService() { return submissionService; } - private ContactManager contactManager = null; + private volatile ContactManager contactManager = null; public ContactManager contactManager() { if (contactManager == null) { @@ -199,7 +199,7 @@ public ContactManager contactManager() { return contactManager; } - private RepeaterStore repeaterStore = null; + private volatile RepeaterStore repeaterStore = null; public RepeaterStore repeaterStore() { if (repeaterStore == null) { @@ -213,7 +213,7 @@ public RepeaterStore repeaterStore() { return repeaterStore; } - private MissionService missionService = null; + private volatile MissionService missionService = null; public MissionService missionService() { if (missionService == null) { @@ -227,7 +227,7 @@ public MissionService missionService() { return missionService; } - private FileAuthenticator fileAuthenticator = null; + private volatile FileAuthenticator fileAuthenticator = null; public FileAuthenticator fileAuthenticator() { if (fileAuthenticator == null) { @@ -241,7 +241,7 @@ public FileAuthenticator fileAuthenticator() { return fileAuthenticator; } - private NioNettyBuilder nioNettyBuilder = null; + private volatile NioNettyBuilder nioNettyBuilder = null; public NioNettyBuilder nioNettyBuilder() { if (nioNettyBuilder == null) { @@ -255,7 +255,7 @@ public NioNettyBuilder nioNettyBuilder() { return nioNettyBuilder; } - private Messenger cotMessenger = null; + private volatile Messenger cotMessenger = null; @SuppressWarnings("unchecked") public Messenger cotMessenger() { @@ -270,7 +270,7 @@ public Messenger cotMessenger() { return cotMessenger; } - private ClusterManager clusterManager = null; + private volatile ClusterManager clusterManager = null; public ClusterManager clusterManager() { if (!coreConfig().getRemoteConfiguration().getCluster().isEnabled()) return null; @@ -286,7 +286,7 @@ public ClusterManager clusterManager() { return clusterManager; } - private FederationEventRepository fedEventRepo = null; + private volatile FederationEventRepository fedEventRepo = null; private Object fedEventRepoLock = new Object(); @@ -303,7 +303,7 @@ public FederationEventRepository fedEventRepo() { return fedEventRepo; } - private ServerInfo serverInfo = null; + private volatile ServerInfo serverInfo = null; public ServerInfo serverInfo() { @@ -318,7 +318,7 @@ public ServerInfo serverInfo() { return serverInfo; } - private MessageDeliveryStrategy mds = null; + private volatile MessageDeliveryStrategy mds = null; public MessageDeliveryStrategy mds() { @@ -333,7 +333,7 @@ public MessageDeliveryStrategy mds() { return mds; } - private MessageReadStrategy mrs = null; + private volatile MessageReadStrategy mrs = null; public MessageReadStrategy mrs() { @@ -348,7 +348,7 @@ public MessageReadStrategy mrs() { return mrs; } - private MessageDOSStrategy mdoss = null; + private volatile MessageDOSStrategy mdoss = null; public MessageDOSStrategy mdoss() { @@ -401,7 +401,7 @@ public ApplicationEventPublisher eventPublisher() { return aep; } - private DataFeedRepository dataFeedRepository = null; + private volatile DataFeedRepository dataFeedRepository = null; public DataFeedRepository dataFeedRepository() { if (dataFeedRepository == null) { @@ -415,7 +415,7 @@ public DataFeedRepository dataFeedRepository() { return dataFeedRepository; } - private EnterpriseSyncService esyncService = null; + private volatile EnterpriseSyncService esyncService = null; public EnterpriseSyncService esyncService() { if (esyncService == null) { @@ -428,7 +428,7 @@ public EnterpriseSyncService esyncService() { return esyncService; } - private MissionRepository missionRepository = null; + private volatile MissionRepository missionRepository = null; public MissionRepository missionRepository() { if (missionRepository == null) { @@ -442,7 +442,7 @@ public MissionRepository missionRepository() { return missionRepository; } - private SubscriptionManagerLite subscriptionManagerLite = null; + private volatile SubscriptionManagerLite subscriptionManagerLite = null; public SubscriptionManagerLite subscriptionManagerLite() { if (subscriptionManagerLite == null) { @@ -456,7 +456,7 @@ public SubscriptionManagerLite subscriptionManagerLite() { return subscriptionManagerLite; } - private MissionRoleRepository missionRoleRepository = null; + private volatile MissionRoleRepository missionRoleRepository = null; public MissionRoleRepository missionRoleRepository() { if (missionRoleRepository == null) { @@ -469,7 +469,7 @@ public MissionRoleRepository missionRoleRepository() { return missionRoleRepository; } - private DatafeedCacheHelper pluginDatafeedCacheHelper = null; + private volatile DatafeedCacheHelper pluginDatafeedCacheHelper = null; public DatafeedCacheHelper pluginDatafeedCacheHelper() { if (pluginDatafeedCacheHelper == null) { @@ -482,7 +482,7 @@ public DatafeedCacheHelper pluginDatafeedCacheHelper() { return pluginDatafeedCacheHelper; } - private PluginDataFeedJdbc pluginDataFeedJdbc = null; + private volatile PluginDataFeedJdbc pluginDataFeedJdbc = null; public PluginDataFeedJdbc pluginDataFeedJdbc() { if (pluginDataFeedJdbc == null) { @@ -495,7 +495,7 @@ public PluginDataFeedJdbc pluginDataFeedJdbc() { return pluginDataFeedJdbc; } - private RemoteUtil remoteUtil = null; + private volatile RemoteUtil remoteUtil = null; public RemoteUtil remoteUtil() { if (remoteUtil == null) { diff --git a/src/takserver-core/src/main/java/tak/server/CustomizeEmbeddedTomcatContainer.java b/src/takserver-core/src/main/java/tak/server/CustomizeEmbeddedTomcatContainer.java new file mode 100644 index 00000000..ba4fdc16 --- /dev/null +++ b/src/takserver-core/src/main/java/tak/server/CustomizeEmbeddedTomcatContainer.java @@ -0,0 +1,53 @@ +package tak.server; + +import java.io.CharArrayWriter; + +import org.apache.catalina.valves.AbstractAccessLogValve; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; +import org.springframework.boot.web.server.WebServerFactoryCustomizer; +import org.springframework.stereotype.Component; + +import com.bbn.marti.config.Logging; +import com.bbn.marti.remote.config.CoreConfigFacade; + +@Component +public class CustomizeEmbeddedTomcatContainer implements WebServerFactoryCustomizer { + + private static final Logger httpAccessLogger = LoggerFactory.getLogger("http_access_logger"); + private static final Logger logger = LoggerFactory.getLogger(CustomizeEmbeddedTomcatContainer.class); + + @Override + public void customize(TomcatServletWebServerFactory factory) { + + Logging loggingConf = CoreConfigFacade.getInstance().getRemoteConfiguration().getLogging(); + + if (loggingConf != null) { + logger.info("http access logging enabled: " + loggingConf.isHttpAccessEnabled()); + } + + + if (loggingConf != null && loggingConf.isHttpAccessEnabled()) { + + TomcatSlf4jAccessValve accessLogValve = new TomcatSlf4jAccessValve(); + accessLogValve.setEnabled(true); + + /** + * for pattern format see https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/catalina/valves/AccessLogValve.html + */ + accessLogValve.setPattern("request: method=%m uri=\"%U\" response: statuscode=%s bytes=%b duration=%D(ms) client: remoteip=%a user=%u useragent=\"%{User-Agent}i\""); + + factory.addContextValves(accessLogValve); + } + } + + public static class TomcatSlf4jAccessValve extends AbstractAccessLogValve { + + @Override + protected void log(CharArrayWriter message) { + httpAccessLogger.info(message.toString()); + } + + } +} \ No newline at end of file diff --git a/src/takserver-core/src/main/java/tak/server/ServerConfiguration.java b/src/takserver-core/src/main/java/tak/server/ServerConfiguration.java index 42b098b1..fa01292d 100644 --- a/src/takserver-core/src/main/java/tak/server/ServerConfiguration.java +++ b/src/takserver-core/src/main/java/tak/server/ServerConfiguration.java @@ -381,7 +381,7 @@ public void customize(Context context) { } catch (Exception e) { logger.warn("Exception setting Tomcat working directory", e); } - + return factory; } diff --git a/src/takserver-core/src/main/java/tak/server/config/ApiConfiguration.java b/src/takserver-core/src/main/java/tak/server/config/ApiConfiguration.java index f5b250af..9681954c 100644 --- a/src/takserver-core/src/main/java/tak/server/config/ApiConfiguration.java +++ b/src/takserver-core/src/main/java/tak/server/config/ApiConfiguration.java @@ -151,6 +151,7 @@ import jakarta.servlet.ServletContextListener; import jakarta.servlet.http.HttpSessionListener; import tak.server.Constants; +import tak.server.CustomizeEmbeddedTomcatContainer; import tak.server.api.DistributedPluginCoreConfigApi; import tak.server.api.DistributedPluginFileApi; import tak.server.api.DistributedPluginMissionApi; @@ -964,6 +965,11 @@ public PropertiesService propertiesService(DataSource dataSource) { public FileManagerService fileManagerService(DataSource dataSource) { return new FileManagerServiceDefaultImpl(dataSource); } + + @Bean + public CustomizeEmbeddedTomcatContainer customizeEmbeddedTomcatContainer() { + return new CustomizeEmbeddedTomcatContainer(); + } @Override public void configurePathMatch(PathMatchConfigurer configurer) { diff --git a/src/takserver-core/src/main/java/tak/server/federation/DistributedFederationManager.java b/src/takserver-core/src/main/java/tak/server/federation/DistributedFederationManager.java index c6d4e8d5..56ad6b32 100644 --- a/src/takserver-core/src/main/java/tak/server/federation/DistributedFederationManager.java +++ b/src/takserver-core/src/main/java/tak/server/federation/DistributedFederationManager.java @@ -103,6 +103,7 @@ import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.collect.Multimap; +import com.google.common.collect.Sets; import com.google.common.collect.SortedSetMultimap; import com.google.common.collect.TreeMultimap; import com.google.protobuf.ByteString; @@ -1188,7 +1189,7 @@ public synchronized void disableOutgoing(@NotNull FederationOutgoing outgoing) { } return; } - + FederatedSubscriptionManager fedSubManager = SubscriptionStore.getInstanceFederatedSubscriptionManager(); if (fedSubManager == null) { String errorMessage = "FedSubManager is null"; @@ -1275,6 +1276,8 @@ public synchronized void disableOutgoing(@NotNull FederationOutgoing outgoing) { saveOutgoing(outgoing, false); } fedSubManager.updateFederateOutgoingStatusCache(outgoing.getDisplayName(), status); + SubscriptionStore.getInstanceFederatedSubscriptionManager() + .getAndPutFederateOutgoingStatus(outgoing.getDisplayName(), status); } } @@ -1322,7 +1325,7 @@ public synchronized void enableOutgoing(String name) { @Override public synchronized void addOutgoingConnection(String name, String host, int port, int reconnect, int maxRetries, - boolean unlimitedRetries, boolean enable, int protocolVersion, String fallback) { + boolean unlimitedRetries, boolean enable, int protocolVersion, String fallback, String token) { // check for dupes by name in the cache if (SubscriptionStore.getInstanceFederatedSubscriptionManager().getCachedFederationConnectionStatus(name) != null) { @@ -1343,6 +1346,7 @@ public synchronized void addOutgoingConnection(String name, String host, int por outgoing.setFallback(fallback); outgoing.setMaxRetries(maxRetries); outgoing.setUnlimitedRetries(unlimitedRetries); + outgoing.setConnectionToken(token); Federation federationConfig = CoreConfigFacade.getInstance().getRemoteConfiguration().getFederation(); @@ -1372,7 +1376,7 @@ public synchronized void addOutgoingConnection(String name, String host, int por } @Override - public synchronized void updateOutgoingConnection(Federation.FederationOutgoing original, Federation.FederationOutgoing update) { + public synchronized void updateOutgoingConnection(Federation.FederationOutgoing original, Federation.FederationOutgoing update) { boolean wasNameChange = false; // if the name is being updated, make sure the new name isn't taken @@ -1390,9 +1394,22 @@ public synchronized void updateOutgoingConnection(Federation.FederationOutgoing throw new DuplicateFederateException("outgoing for " + update.getAddress() + ", " + update.getPort() + " already exists"); } } + + // clear old + disableOutgoing(original); + SubscriptionStore.getInstanceFederatedSubscriptionManager().removeOutgoingNumRetries(original.getDisplayName()); + SubscriptionStore.getInstanceFederatedSubscriptionManager().removeOutgoingRetryScheduled(original.getDisplayName()); + + // add new + SubscriptionStore.getInstanceFederatedSubscriptionManager().putOutgoingNumRetries(update.getDisplayName()); + SubscriptionStore.getInstanceFederatedSubscriptionManager().putOutgoingRetryScheduled(update.getDisplayName()); + + // remove references to old name + if (wasNameChange) { + SubscriptionStore.getInstanceFederatedSubscriptionManager().removeFederateOutgoingStatus(original.getDisplayName()); + } Configuration config = CoreConfigFacade.getInstance().getRemoteConfiguration(); - for (Federation.FederationOutgoing f : config.getFederation().getFederationOutgoing()) { if (f.getDisplayName().compareTo(original.getDisplayName()) == 0) { @@ -1407,56 +1424,21 @@ public synchronized void updateOutgoingConnection(Federation.FederationOutgoing f.setFallback(update.getFallback()); f.setMaxRetries(update.getMaxRetries()); f.setUnlimitedRetries(update.isUnlimitedRetries()); - + f.setConnectionToken(update.getConnectionToken()); + CoreConfigFacade.getInstance().setAndSaveFederation(federationConfig); break; } } - - // clear old - SubscriptionStore.getInstanceFederatedSubscriptionManager().removeOutgoingNumRetries(original.getDisplayName()); - SubscriptionStore.getInstanceFederatedSubscriptionManager().removeOutgoingRetryScheduled(original.getDisplayName()); - - // add new - SubscriptionStore.getInstanceFederatedSubscriptionManager().putOutgoingNumRetries(update.getDisplayName()); - SubscriptionStore.getInstanceFederatedSubscriptionManager().putOutgoingRetryScheduled(update.getDisplayName()); - - // if the original and the updated are disabled, just update cache incase the name changed - if (!original.isEnabled() && !update.isEnabled()) { - ConnectionStatus status = new ConnectionStatus(ConnectionStatusValue.DISABLED); - SubscriptionStore.getInstanceFederatedSubscriptionManager().getAndPutFederateOutgoingStatus(update.getDisplayName(), status); - } - // if the original was enabled and the updated is disabled, nothing to do other than disable - else if (original.isEnabled() && !update.isEnabled()) { - // disable the original outgoing connection - disableOutgoing(original); - } - // if the original was disabled and the updated is enabled, initiate the outgoing - else if (!original.isEnabled() && update.isEnabled()) { + + if (update.isEnabled()) { ConnectionStatus status = new ConnectionStatus(ConnectionStatusValue.CONNECTING); SubscriptionStore.getInstanceFederatedSubscriptionManager().getAndPutFederateOutgoingStatus(update.getDisplayName(), status); initiateOutgoing(update, status); - } - // if the display name, address, port or protocol version has changed, we need to restart the connection - // otherwise we can just update the config and keep it connected - else if (original.isEnabled() && update.isEnabled()) { - if (!original.getDisplayName().equals(update.getDisplayName()) || !original.getPort().equals(update.getPort()) || - !original.getAddress().equals(update.getAddress()) || original.getProtocolVersion() != update.getProtocolVersion()) { - - // disable the original outgoing connection - disableOutgoing(original); - - // re-enable with the new outgoing connection - ConnectionStatus status = new ConnectionStatus(ConnectionStatusValue.CONNECTING); - SubscriptionStore.getInstanceFederatedSubscriptionManager().getAndPutFederateOutgoingStatus(update.getDisplayName(), status); - initiateOutgoing(update, status); - } - } - - // remove references to old name - if (wasNameChange) { - SubscriptionStore.getInstanceFederatedSubscriptionManager().removeFederateOutgoingStatus(original.getDisplayName()); + } else { + ConnectionStatus status = new ConnectionStatus(ConnectionStatusValue.DISABLED); + SubscriptionStore.getInstanceFederatedSubscriptionManager().getAndPutFederateOutgoingStatus(update.getDisplayName(), status); } } @@ -2044,6 +2026,59 @@ public void addRemoteContact(ContactListEntry contact, ChannelHandler handler) { } } + public void addPluginContact(CotEventContainer cot, Set groups) { + try { + + if (logger.isDebugEnabled()) { + logger.debug("federation manager add plugin contact " + cot.asXml()); + } + + // prevent NullPointerExceptions from getting thrown by the msg builder + if (cot.getCallsign() == null || cot.getEndpoint() == null || cot.getUid() == null) { + if (logger.isDebugEnabled()) { + logger.debug("null callsign or uid in FederationManager.addPluginContact " + cot.asXml()); + } + return; + } + + // build up a new contact to send + ContactListEntry newContact = ContactListEntry.newBuilder() + .setCallsign(cot.getCallsign()) + .setUid(cot.getUid()) + .setOperation(CRUD.CREATE) + .build(); + + // get the IN group names for the plugin contacts + Set contactGroupNames = groups + .stream().filter(g -> g.getDirection() == Direction.IN) + .map(g -> g.getName()).collect(Collectors.toSet()); + + // iterate across federation subscriptions + for (FederateSubscription destSub : SubscriptionStore.getInstanceFederatedSubscriptionManager() + .getFederateSubscriptions()) { + + // get the OUT group names for the federation subscriptions + Set destSubGroupNames = groupManager().getGroups(destSub.getUser()) + .stream().filter(g -> g.getDirection() == Direction.OUT) + .map(g -> g.getName()).collect(Collectors.toSet()); + Sets.SetView commonGroupNames = Sets.intersection(contactGroupNames, destSubGroupNames); + + // skip this federation if no OUT groups in common with the contact + if (commonGroupNames.size() == 0) { + continue; + } + + // apply the common groups to the federated contact event and send it + FederatedEvent f = FederatedEvent.newBuilder().setContact(newContact). + addAllFederateGroups(commonGroupNames).build(); + destSub.submitLocalContact(f, System.currentTimeMillis()); + } + + } catch (Exception e) { + logger.error("exception in addPluginContact", e); + } + } + public void addLocalContact(CotEventContainer cot, ChannelHandler src) { if (logger.isDebugEnabled()) { @@ -2164,11 +2199,14 @@ public void tryReconnect(final ConnectionStatus status, final FederationOutgoing Resources.fedReconnectThreadPool.schedule(new Runnable() { @Override - public void run() { + public void run() { + if (status.getConnectionStatusValue() != ConnectionStatusValue.RETRY_SCHEDULED) { + return; + } + boolean retryScheduled = SubscriptionStore.getInstanceFederatedSubscriptionManager().getOutgoingRetryScheduled(outgoing.getDisplayName()).compareAndSet(true, false); if (SubscriptionStore.getInstanceFederatedSubscriptionManager() .getFederationConnectionStatus(outgoing.getDisplayName()) != null && status.compareAndSetConnectionStatusValue(ConnectionStatusValue.RETRY_SCHEDULED, ConnectionStatusValue.CONNECTING) && retryScheduled) { - if (outgoing.isUnlimitedRetries() || (SubscriptionStore.getInstanceFederatedSubscriptionManager() .getOutgoingNumRetries(outgoing.getDisplayName()) diff --git a/src/takserver-core/src/main/java/tak/server/federation/FederationServer.java b/src/takserver-core/src/main/java/tak/server/federation/FederationServer.java index a5a05228..31803885 100644 --- a/src/takserver-core/src/main/java/tak/server/federation/FederationServer.java +++ b/src/takserver-core/src/main/java/tak/server/federation/FederationServer.java @@ -9,6 +9,7 @@ import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.IOException; +import java.math.BigInteger; import java.net.InetAddress; import java.net.SocketAddress; import java.nio.ByteBuffer; @@ -560,7 +561,7 @@ public void clientEventStream(Subscription subscription, StreamObserver(clientStream, fedName, FederationUtils.getBytesSHA256(clientCertArray[0].getEncoded()), session, subscription, new Comparator() { + streamHolder = new GuardedStreamHolder(clientStream, fedName, FederationUtils.getBytesSHA256(clientCertArray[0].getEncoded()), new BigInteger(session.getId()).toString(), subscription, new Comparator() { @Override public int compare(FederatedEvent a, FederatedEvent b) { @@ -692,7 +693,7 @@ public void clientROLStream(Subscription subscription, StreamObserver clien clientStream, clientName, fedCertHash, - session, + new BigInteger(session.getId()).toString(), subscription, (ROL a, ROL b) -> ComparisonChain.start().compare(a.hashCode(), b.hashCode()).result(), false); @@ -1468,7 +1469,7 @@ public void serverFederateGroupsStream(Subscription subscription, StreamObserver responseObserver, clientName, fedCertHash, - session, + new BigInteger(session.getId()).toString(), subscription, (FederateGroups a, FederateGroups b) -> ComparisonChain.start().compare(a.hashCode(), b.hashCode()).result(), false); diff --git a/src/takserver-core/src/main/java/tak/server/federation/TakFigClient.java b/src/takserver-core/src/main/java/tak/server/federation/TakFigClient.java index 4cbeb6a9..557915af 100644 --- a/src/takserver-core/src/main/java/tak/server/federation/TakFigClient.java +++ b/src/takserver-core/src/main/java/tak/server/federation/TakFigClient.java @@ -203,6 +203,8 @@ public String getClientName() { } private String clientUid = ""; + + private String connectionToken = ""; private FigFederateSubscription federateSubscription = null; @@ -272,6 +274,8 @@ public void start(FederationOutgoing outgoing, ConnectionStatus status) { this.outgoingName = outgoing.getDisplayName(); this.status = status; + this.connectionToken = outgoing.getConnectionToken(); + Tls figTls = CoreConfigFacade.getInstance().getRemoteConfiguration().getFederation().getFederationServer().getTls(); // use the client name from the outgoing configuration @@ -335,10 +339,17 @@ public void start(FederationOutgoing outgoing, ConnectionStatus status) { } catch (Exception e) { logger.warn("exception initializing trust store", e); } - - SslContextBuilder sslContextBuilder = GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.OPENSSL) // this ensures that we are using OpenSSL, not JRE SSL - .keyManager(keyMgrFactory) - .trustManager(trustMgrFactory); + + SslContextBuilder sslContextBuilder; + // don't use a key manager if we are using token auth + if (!Strings.isNullOrEmpty(connectionToken)) { + sslContextBuilder = GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.OPENSSL) // this ensures that we are using OpenSSL, not JRE SSL + .trustManager(trustMgrFactory); + } else { + sslContextBuilder = GrpcSslContexts.configure(SslContextBuilder.forClient(), SslProvider.OPENSSL) // this ensures that we are using OpenSSL, not JRE SSL + .keyManager(keyMgrFactory) + .trustManager(trustMgrFactory); + } String context = "TLSv1.2,TLSv1.3"; @@ -359,7 +370,13 @@ public void start(FederationOutgoing outgoing, ConnectionStatus status) { } blockingFederatedChannel = FederatedChannelGrpc.newBlockingStub(channel); - asyncFederatedChannel = FederatedChannelGrpc.newStub(channel); + + if (!Strings.isNullOrEmpty(connectionToken)) { + TokenAuthCredential credential = new TokenAuthCredential(connectionToken); + asyncFederatedChannel = FederatedChannelGrpc.newStub(channel).withCallCredentials(credential); + } else { + asyncFederatedChannel = FederatedChannelGrpc.newStub(channel); + } connectionInfo = new ConnectionInfo(); diff --git a/src/takserver-core/src/main/java/tak/server/profile/DistributedServerInfo.java b/src/takserver-core/src/main/java/tak/server/profile/DistributedServerInfo.java index 91e08dba..5a65c963 100644 --- a/src/takserver-core/src/main/java/tak/server/profile/DistributedServerInfo.java +++ b/src/takserver-core/src/main/java/tak/server/profile/DistributedServerInfo.java @@ -28,7 +28,7 @@ public final class DistributedServerInfo implements ServerInfo, Service { private static final Logger logger = LoggerFactory.getLogger(DistributedServerInfo.class); - private String serverId = null; + private volatile String serverId = null; private boolean isCluster = false; @@ -106,7 +106,7 @@ private String generateServerId() { String id = UUID.randomUUID().toString().replace("-", ""); try { - CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().setServerId(id); + CoreConfigFacade.getInstance().setAndSaveServerId(id); CoreConfigFacade.getInstance().saveChangesAndUpdateCache(); } catch (Exception e) { diff --git a/src/takserver-core/src/main/java/tak/server/qos/MessageBaseStrategy.java b/src/takserver-core/src/main/java/tak/server/qos/MessageBaseStrategy.java index cd3cb1b5..bbe0e57c 100644 --- a/src/takserver-core/src/main/java/tak/server/qos/MessageBaseStrategy.java +++ b/src/takserver-core/src/main/java/tak/server/qos/MessageBaseStrategy.java @@ -28,7 +28,7 @@ public abstract class MessageBaseStrategy implements MessageStrategy { - private Cache cache; + private volatile Cache cache; protected long maxCacheSize; diff --git a/src/takserver-core/src/main/resources/logback-spring.xml b/src/takserver-core/src/main/resources/logback-spring.xml index e9d9496f..c6c84a95 100644 --- a/src/takserver-core/src/main/resources/logback-spring.xml +++ b/src/takserver-core/src/main/resources/logback-spring.xml @@ -77,6 +77,33 @@ + + logs/takserver-api-access.log + + + logs/takserver-api-access.%d{yyyy-MM-dd}.log.gz + 90 + + + + + + ${LINE_SEPARATED} + ${DOUBLE_SPACED} + + ${PRETTY_PRINT} + + yyyy-MM-dd'T'HH:mm:ss.SSS'Z' + + + + + + %d{yyyy-MM-dd-HH:mm:ss.SSS} [%thread] %level %logger{36} - %msg%n + + + + @@ -164,6 +191,10 @@ + + + + @@ -219,11 +250,25 @@ %d{yyyy-MM-dd-HH:mm:ss.SSS} [%thread] %level %logger{36} - %msg%n + + + - + + + + + + + + + + + + - - + + diff --git a/src/takserver-core/src/main/resources/security-context.xml b/src/takserver-core/src/main/resources/security-context.xml index 65f264c3..eca20079 100644 --- a/src/takserver-core/src/main/resources/security-context.xml +++ b/src/takserver-core/src/main/resources/security-context.xml @@ -344,21 +344,28 @@ - - + + + + + + + + + + - + + + - - - - - + + diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/404.html b/src/takserver-core/src/main/webapp/Marti/documentation/404.html new file mode 100644 index 00000000..e322cd96 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/404.html @@ -0,0 +1,14 @@ + + + + + +Page Not Found | TAK Server Documentation + + + + + +

Page Not Found

We could not find what you were looking for.

Please contact the owner of the site that linked you to the original URL and let them know their link is broken.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/css/styles.f107caf2.css b/src/takserver-core/src/main/webapp/Marti/documentation/assets/css/styles.f107caf2.css new file mode 100644 index 00000000..c3715b22 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/css/styles.f107caf2.css @@ -0,0 +1 @@ +.col,.container{padding:0 var(--ifm-spacing-horizontal);width:100%}.markdown>h2,.markdown>h3,.markdown>h4,.markdown>h5,.markdown>h6{margin-bottom:calc(var(--ifm-heading-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown li,body{word-wrap:break-word}body,ol ol,ol ul,ul ol,ul ul{margin:0}pre,table{overflow:auto}blockquote,pre{margin:0 0 var(--ifm-spacing-vertical)}.breadcrumbs__link,.button{transition-timing-function:var(--ifm-transition-timing-default)}.button,code{vertical-align:middle}.button--outline.button--active,.button--outline:active,.button--outline:hover,:root{--ifm-button-color:var(--ifm-font-color-base-inverse)}.menu__link:hover,a{transition:color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.navbar--dark,:root{--ifm-navbar-link-hover-color:var(--ifm-color-primary)}.menu,.navbar-sidebar{overflow-x:hidden}:root,html[data-theme=dark]{--ifm-color-emphasis-500:var(--ifm-color-gray-500)}.toggleButton_gllP,html{-webkit-tap-highlight-color:transparent}.clean-list,.containsTaskList_mC6p,.details_lb9f>summary,.dropdown__menu,.menu__list{list-style:none}:root{--ifm-color-scheme:light;--ifm-dark-value:10%;--ifm-darker-value:15%;--ifm-darkest-value:30%;--ifm-light-value:15%;--ifm-lighter-value:30%;--ifm-lightest-value:50%;--ifm-contrast-background-value:90%;--ifm-contrast-foreground-value:70%;--ifm-contrast-background-dark-value:70%;--ifm-contrast-foreground-dark-value:90%;--ifm-color-primary:#3578e5;--ifm-color-secondary:#ebedf0;--ifm-color-success:#00a400;--ifm-color-info:#54c7ec;--ifm-color-warning:#ffba00;--ifm-color-danger:#fa383e;--ifm-color-primary-dark:#306cce;--ifm-color-primary-darker:#2d66c3;--ifm-color-primary-darkest:#2554a0;--ifm-color-primary-light:#538ce9;--ifm-color-primary-lighter:#72a1ed;--ifm-color-primary-lightest:#9abcf2;--ifm-color-primary-contrast-background:#ebf2fc;--ifm-color-primary-contrast-foreground:#102445;--ifm-color-secondary-dark:#d4d5d8;--ifm-color-secondary-darker:#c8c9cc;--ifm-color-secondary-darkest:#a4a6a8;--ifm-color-secondary-light:#eef0f2;--ifm-color-secondary-lighter:#f1f2f5;--ifm-color-secondary-lightest:#f5f6f8;--ifm-color-secondary-contrast-background:#fdfdfe;--ifm-color-secondary-contrast-foreground:#474748;--ifm-color-success-dark:#009400;--ifm-color-success-darker:#008b00;--ifm-color-success-darkest:#007300;--ifm-color-success-light:#26b226;--ifm-color-success-lighter:#4dbf4d;--ifm-color-success-lightest:#80d280;--ifm-color-success-contrast-background:#e6f6e6;--ifm-color-success-contrast-foreground:#003100;--ifm-color-info-dark:#4cb3d4;--ifm-color-info-darker:#47a9c9;--ifm-color-info-darkest:#3b8ba5;--ifm-color-info-light:#6ecfef;--ifm-color-info-lighter:#87d8f2;--ifm-color-info-lightest:#aae3f6;--ifm-color-info-contrast-background:#eef9fd;--ifm-color-info-contrast-foreground:#193c47;--ifm-color-warning-dark:#e6a700;--ifm-color-warning-darker:#d99e00;--ifm-color-warning-darkest:#b38200;--ifm-color-warning-light:#ffc426;--ifm-color-warning-lighter:#ffcf4d;--ifm-color-warning-lightest:#ffdd80;--ifm-color-warning-contrast-background:#fff8e6;--ifm-color-warning-contrast-foreground:#4d3800;--ifm-color-danger-dark:#e13238;--ifm-color-danger-darker:#d53035;--ifm-color-danger-darkest:#af272b;--ifm-color-danger-light:#fb565b;--ifm-color-danger-lighter:#fb7478;--ifm-color-danger-lightest:#fd9c9f;--ifm-color-danger-contrast-background:#ffebec;--ifm-color-danger-contrast-foreground:#4b1113;--ifm-color-white:#fff;--ifm-color-black:#000;--ifm-color-gray-0:var(--ifm-color-white);--ifm-color-gray-100:#f5f6f7;--ifm-color-gray-200:#ebedf0;--ifm-color-gray-300:#dadde1;--ifm-color-gray-400:#ccd0d5;--ifm-color-gray-500:#bec3c9;--ifm-color-gray-600:#8d949e;--ifm-color-gray-700:#606770;--ifm-color-gray-800:#444950;--ifm-color-gray-900:#1c1e21;--ifm-color-gray-1000:var(--ifm-color-black);--ifm-color-emphasis-0:var(--ifm-color-gray-0);--ifm-color-emphasis-100:var(--ifm-color-gray-100);--ifm-color-emphasis-200:var(--ifm-color-gray-200);--ifm-color-emphasis-300:var(--ifm-color-gray-300);--ifm-color-emphasis-400:var(--ifm-color-gray-400);--ifm-color-emphasis-600:var(--ifm-color-gray-600);--ifm-color-emphasis-700:var(--ifm-color-gray-700);--ifm-color-emphasis-800:var(--ifm-color-gray-800);--ifm-color-emphasis-900:var(--ifm-color-gray-900);--ifm-color-emphasis-1000:var(--ifm-color-gray-1000);--ifm-color-content:var(--ifm-color-emphasis-900);--ifm-color-content-inverse:var(--ifm-color-emphasis-0);--ifm-color-content-secondary:#525860;--ifm-background-color:#0000;--ifm-background-surface-color:var(--ifm-color-content-inverse);--ifm-global-border-width:1px;--ifm-global-radius:0.4rem;--ifm-hover-overlay:#0000000d;--ifm-font-color-base:var(--ifm-color-content);--ifm-font-color-base-inverse:var(--ifm-color-content-inverse);--ifm-font-color-secondary:var(--ifm-color-content-secondary);--ifm-font-family-base:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol";--ifm-font-family-monospace:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace;--ifm-font-size-base:100%;--ifm-font-weight-light:300;--ifm-font-weight-normal:400;--ifm-font-weight-semibold:500;--ifm-font-weight-bold:700;--ifm-font-weight-base:var(--ifm-font-weight-normal);--ifm-line-height-base:1.65;--ifm-global-spacing:1rem;--ifm-spacing-vertical:var(--ifm-global-spacing);--ifm-spacing-horizontal:var(--ifm-global-spacing);--ifm-transition-fast:200ms;--ifm-transition-slow:400ms;--ifm-transition-timing-default:cubic-bezier(0.08,0.52,0.52,1);--ifm-global-shadow-lw:0 1px 2px 0 #0000001a;--ifm-global-shadow-md:0 5px 40px #0003;--ifm-global-shadow-tl:0 12px 28px 0 #0003,0 2px 4px 0 #0000001a;--ifm-z-index-dropdown:100;--ifm-z-index-fixed:200;--ifm-z-index-overlay:400;--ifm-container-width:1140px;--ifm-container-width-xl:1320px;--ifm-code-background:#f6f7f8;--ifm-code-border-radius:var(--ifm-global-radius);--ifm-code-font-size:90%;--ifm-code-padding-horizontal:0.1rem;--ifm-code-padding-vertical:0.1rem;--ifm-pre-background:var(--ifm-code-background);--ifm-pre-border-radius:var(--ifm-code-border-radius);--ifm-pre-color:inherit;--ifm-pre-line-height:1.45;--ifm-pre-padding:1rem;--ifm-heading-color:inherit;--ifm-heading-margin-top:0;--ifm-heading-margin-bottom:var(--ifm-spacing-vertical);--ifm-heading-font-family:var(--ifm-font-family-base);--ifm-heading-font-weight:var(--ifm-font-weight-bold);--ifm-heading-line-height:1.25;--ifm-h1-font-size:2rem;--ifm-h2-font-size:1.5rem;--ifm-h3-font-size:1.25rem;--ifm-h4-font-size:1rem;--ifm-h5-font-size:0.875rem;--ifm-h6-font-size:0.85rem;--ifm-image-alignment-padding:1.25rem;--ifm-leading-desktop:1.25;--ifm-leading:calc(var(--ifm-leading-desktop)*1rem);--ifm-list-left-padding:2rem;--ifm-list-margin:1rem;--ifm-list-item-margin:0.25rem;--ifm-list-paragraph-margin:1rem;--ifm-table-cell-padding:0.75rem;--ifm-table-background:#0000;--ifm-table-stripe-background:#00000008;--ifm-table-border-width:1px;--ifm-table-border-color:var(--ifm-color-emphasis-300);--ifm-table-head-background:inherit;--ifm-table-head-color:inherit;--ifm-table-head-font-weight:var(--ifm-font-weight-bold);--ifm-table-cell-color:inherit;--ifm-link-color:var(--ifm-color-primary);--ifm-link-decoration:none;--ifm-link-hover-color:var(--ifm-link-color);--ifm-link-hover-decoration:underline;--ifm-paragraph-margin-bottom:var(--ifm-leading);--ifm-blockquote-font-size:var(--ifm-font-size-base);--ifm-blockquote-border-left-width:2px;--ifm-blockquote-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-blockquote-padding-vertical:0;--ifm-blockquote-shadow:none;--ifm-blockquote-color:var(--ifm-color-emphasis-800);--ifm-blockquote-border-color:var(--ifm-color-emphasis-300);--ifm-hr-background-color:var(--ifm-color-emphasis-500);--ifm-hr-height:1px;--ifm-hr-margin-vertical:1.5rem;--ifm-scrollbar-size:7px;--ifm-scrollbar-track-background-color:#f1f1f1;--ifm-scrollbar-thumb-background-color:silver;--ifm-scrollbar-thumb-hover-background-color:#a7a7a7;--ifm-alert-background-color:inherit;--ifm-alert-border-color:inherit;--ifm-alert-border-radius:var(--ifm-global-radius);--ifm-alert-border-width:0px;--ifm-alert-border-left-width:5px;--ifm-alert-color:var(--ifm-font-color-base);--ifm-alert-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-alert-padding-vertical:var(--ifm-spacing-vertical);--ifm-alert-shadow:var(--ifm-global-shadow-lw);--ifm-avatar-intro-margin:1rem;--ifm-avatar-intro-alignment:inherit;--ifm-avatar-photo-size:3rem;--ifm-badge-background-color:inherit;--ifm-badge-border-color:inherit;--ifm-badge-border-radius:var(--ifm-global-radius);--ifm-badge-border-width:var(--ifm-global-border-width);--ifm-badge-color:var(--ifm-color-white);--ifm-badge-padding-horizontal:calc(var(--ifm-spacing-horizontal)*0.5);--ifm-badge-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-breadcrumb-border-radius:1.5rem;--ifm-breadcrumb-spacing:0.5rem;--ifm-breadcrumb-color-active:var(--ifm-color-primary);--ifm-breadcrumb-item-background-active:var(--ifm-hover-overlay);--ifm-breadcrumb-padding-horizontal:0.8rem;--ifm-breadcrumb-padding-vertical:0.4rem;--ifm-breadcrumb-size-multiplier:1;--ifm-breadcrumb-separator:url('data:image/svg+xml;utf8,');--ifm-breadcrumb-separator-filter:none;--ifm-breadcrumb-separator-size:0.5rem;--ifm-breadcrumb-separator-size-multiplier:1.25;--ifm-button-background-color:inherit;--ifm-button-border-color:var(--ifm-button-background-color);--ifm-button-border-width:var(--ifm-global-border-width);--ifm-button-font-weight:var(--ifm-font-weight-bold);--ifm-button-padding-horizontal:1.5rem;--ifm-button-padding-vertical:0.375rem;--ifm-button-size-multiplier:1;--ifm-button-transition-duration:var(--ifm-transition-fast);--ifm-button-border-radius:calc(var(--ifm-global-radius)*var(--ifm-button-size-multiplier));--ifm-button-group-spacing:2px;--ifm-card-background-color:var(--ifm-background-surface-color);--ifm-card-border-radius:calc(var(--ifm-global-radius)*2);--ifm-card-horizontal-spacing:var(--ifm-global-spacing);--ifm-card-vertical-spacing:var(--ifm-global-spacing);--ifm-toc-border-color:var(--ifm-color-emphasis-300);--ifm-toc-link-color:var(--ifm-color-content-secondary);--ifm-toc-padding-vertical:0.5rem;--ifm-toc-padding-horizontal:0.5rem;--ifm-dropdown-background-color:var(--ifm-background-surface-color);--ifm-dropdown-font-weight:var(--ifm-font-weight-semibold);--ifm-dropdown-link-color:var(--ifm-font-color-base);--ifm-dropdown-hover-background-color:var(--ifm-hover-overlay);--ifm-footer-background-color:var(--ifm-color-emphasis-100);--ifm-footer-color:inherit;--ifm-footer-link-color:var(--ifm-color-emphasis-700);--ifm-footer-link-hover-color:var(--ifm-color-primary);--ifm-footer-link-horizontal-spacing:0.5rem;--ifm-footer-padding-horizontal:calc(var(--ifm-spacing-horizontal)*2);--ifm-footer-padding-vertical:calc(var(--ifm-spacing-vertical)*2);--ifm-footer-title-color:inherit;--ifm-footer-logo-max-width:min(30rem,90vw);--ifm-hero-background-color:var(--ifm-background-surface-color);--ifm-hero-text-color:var(--ifm-color-emphasis-800);--ifm-menu-color:var(--ifm-color-emphasis-700);--ifm-menu-color-active:var(--ifm-color-primary);--ifm-menu-color-background-active:var(--ifm-hover-overlay);--ifm-menu-color-background-hover:var(--ifm-hover-overlay);--ifm-menu-link-padding-horizontal:0.75rem;--ifm-menu-link-padding-vertical:0.375rem;--ifm-menu-link-sublist-icon:url('data:image/svg+xml;utf8,');--ifm-menu-link-sublist-icon-filter:none;--ifm-navbar-background-color:var(--ifm-background-surface-color);--ifm-navbar-height:3.75rem;--ifm-navbar-item-padding-horizontal:0.75rem;--ifm-navbar-item-padding-vertical:0.25rem;--ifm-navbar-link-color:var(--ifm-font-color-base);--ifm-navbar-link-active-color:var(--ifm-link-color);--ifm-navbar-padding-horizontal:var(--ifm-spacing-horizontal);--ifm-navbar-padding-vertical:calc(var(--ifm-spacing-vertical)*0.5);--ifm-navbar-shadow:var(--ifm-global-shadow-lw);--ifm-navbar-search-input-background-color:var(--ifm-color-emphasis-200);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-800);--ifm-navbar-search-input-placeholder-color:var(--ifm-color-emphasis-500);--ifm-navbar-search-input-icon:url('data:image/svg+xml;utf8,');--ifm-navbar-sidebar-width:83vw;--ifm-pagination-border-radius:var(--ifm-global-radius);--ifm-pagination-color-active:var(--ifm-color-primary);--ifm-pagination-font-size:1rem;--ifm-pagination-item-active-background:var(--ifm-hover-overlay);--ifm-pagination-page-spacing:0.2em;--ifm-pagination-padding-horizontal:calc(var(--ifm-spacing-horizontal)*1);--ifm-pagination-padding-vertical:calc(var(--ifm-spacing-vertical)*0.25);--ifm-pagination-nav-border-radius:var(--ifm-global-radius);--ifm-pagination-nav-color-hover:var(--ifm-color-primary);--ifm-pills-color-active:var(--ifm-color-primary);--ifm-pills-color-background-active:var(--ifm-hover-overlay);--ifm-pills-spacing:0.125rem;--ifm-tabs-color:var(--ifm-font-color-secondary);--ifm-tabs-color-active:var(--ifm-color-primary);--ifm-tabs-color-active-border:var(--ifm-tabs-color-active);--ifm-tabs-padding-horizontal:1rem;--ifm-tabs-padding-vertical:1rem;--docusaurus-progress-bar-color:var(--ifm-color-primary);--ifm-color-primary:#2e8555;--ifm-color-primary-dark:#29784c;--ifm-color-primary-darker:#277148;--ifm-color-primary-darkest:#205d3b;--ifm-color-primary-light:#33925d;--ifm-color-primary-lighter:#359962;--ifm-color-primary-lightest:#3cad6e;--ifm-code-font-size:95%;--docusaurus-highlighted-code-line-bg:#0000001a;--docusaurus-announcement-bar-height:auto;--docusaurus-collapse-button-bg:#0000;--docusaurus-collapse-button-bg-hover:#0000001a;--doc-sidebar-width:300px;--doc-sidebar-hidden-width:30px;--docusaurus-tag-list-border:var(--ifm-color-emphasis-300)}.badge--danger,.badge--info,.badge--primary,.badge--secondary,.badge--success,.badge--warning{--ifm-badge-border-color:var(--ifm-badge-background-color)}.button--link,.button--outline{--ifm-button-background-color:#0000}*{box-sizing:border-box}html{-webkit-font-smoothing:antialiased;-webkit-text-size-adjust:100%;text-size-adjust:100%;background-color:var(--ifm-background-color);color:var(--ifm-font-color-base);color-scheme:var(--ifm-color-scheme);font:var(--ifm-font-size-base)/var(--ifm-line-height-base) var(--ifm-font-family-base);text-rendering:optimizelegibility}iframe{border:0;color-scheme:auto}.container{margin:0 auto;max-width:var(--ifm-container-width)}.container--fluid{max-width:inherit}.row{display:flex;flex-wrap:wrap;margin:0 calc(var(--ifm-spacing-horizontal)*-1)}.margin-bottom--none,.margin-vert--none,.markdown>:last-child{margin-bottom:0!important}.margin-top--none,.margin-vert--none{margin-top:0!important}.row--no-gutters{margin-left:0;margin-right:0}.margin-horiz--none,.margin-right--none{margin-right:0!important}.row--no-gutters>.col{padding-left:0;padding-right:0}.row--align-top{align-items:flex-start}.row--align-bottom{align-items:flex-end}.menuExternalLink_NmtK,.row--align-center{align-items:center}.row--align-stretch{align-items:stretch}.row--align-baseline{align-items:baseline}.col{--ifm-col-width:100%;flex:1 0;margin-left:0;max-width:var(--ifm-col-width)}.padding-bottom--none,.padding-vert--none{padding-bottom:0!important}.padding-top--none,.padding-vert--none{padding-top:0!important}.padding-horiz--none,.padding-left--none{padding-left:0!important}.padding-horiz--none,.padding-right--none{padding-right:0!important}.col[class*=col--]{flex:0 0 var(--ifm-col-width)}.col--1{--ifm-col-width:8.33333%}.col--offset-1{margin-left:8.33333%}.col--2{--ifm-col-width:16.66667%}.col--offset-2{margin-left:16.66667%}.col--3{--ifm-col-width:25%}.col--offset-3{margin-left:25%}.col--4{--ifm-col-width:33.33333%}.col--offset-4{margin-left:33.33333%}.col--5{--ifm-col-width:41.66667%}.col--offset-5{margin-left:41.66667%}.col--6{--ifm-col-width:50%}.col--offset-6{margin-left:50%}.col--7{--ifm-col-width:58.33333%}.col--offset-7{margin-left:58.33333%}.col--8{--ifm-col-width:66.66667%}.col--offset-8{margin-left:66.66667%}.col--9{--ifm-col-width:75%}.col--offset-9{margin-left:75%}.col--10{--ifm-col-width:83.33333%}.col--offset-10{margin-left:83.33333%}.col--11{--ifm-col-width:91.66667%}.col--offset-11{margin-left:91.66667%}.col--12{--ifm-col-width:100%}.col--offset-12{margin-left:100%}.margin-horiz--none,.margin-left--none{margin-left:0!important}.margin--none{margin:0!important}.margin-bottom--xs,.margin-vert--xs{margin-bottom:.25rem!important}.margin-top--xs,.margin-vert--xs{margin-top:.25rem!important}.margin-horiz--xs,.margin-left--xs{margin-left:.25rem!important}.margin-horiz--xs,.margin-right--xs{margin-right:.25rem!important}.margin--xs{margin:.25rem!important}.margin-bottom--sm,.margin-vert--sm{margin-bottom:.5rem!important}.margin-top--sm,.margin-vert--sm{margin-top:.5rem!important}.margin-horiz--sm,.margin-left--sm{margin-left:.5rem!important}.margin-horiz--sm,.margin-right--sm{margin-right:.5rem!important}.margin--sm{margin:.5rem!important}.margin-bottom--md,.margin-vert--md{margin-bottom:1rem!important}.margin-top--md,.margin-vert--md{margin-top:1rem!important}.margin-horiz--md,.margin-left--md{margin-left:1rem!important}.margin-horiz--md,.margin-right--md{margin-right:1rem!important}.margin--md{margin:1rem!important}.margin-bottom--lg,.margin-vert--lg{margin-bottom:2rem!important}.margin-top--lg,.margin-vert--lg{margin-top:2rem!important}.margin-horiz--lg,.margin-left--lg{margin-left:2rem!important}.margin-horiz--lg,.margin-right--lg{margin-right:2rem!important}.margin--lg{margin:2rem!important}.margin-bottom--xl,.margin-vert--xl{margin-bottom:5rem!important}.margin-top--xl,.margin-vert--xl{margin-top:5rem!important}.margin-horiz--xl,.margin-left--xl{margin-left:5rem!important}.margin-horiz--xl,.margin-right--xl{margin-right:5rem!important}.margin--xl{margin:5rem!important}.padding--none{padding:0!important}.padding-bottom--xs,.padding-vert--xs{padding-bottom:.25rem!important}.padding-top--xs,.padding-vert--xs{padding-top:.25rem!important}.padding-horiz--xs,.padding-left--xs{padding-left:.25rem!important}.padding-horiz--xs,.padding-right--xs{padding-right:.25rem!important}.padding--xs{padding:.25rem!important}.padding-bottom--sm,.padding-vert--sm{padding-bottom:.5rem!important}.padding-top--sm,.padding-vert--sm{padding-top:.5rem!important}.padding-horiz--sm,.padding-left--sm{padding-left:.5rem!important}.padding-horiz--sm,.padding-right--sm{padding-right:.5rem!important}.padding--sm{padding:.5rem!important}.padding-bottom--md,.padding-vert--md{padding-bottom:1rem!important}.padding-top--md,.padding-vert--md{padding-top:1rem!important}.padding-horiz--md,.padding-left--md{padding-left:1rem!important}.padding-horiz--md,.padding-right--md{padding-right:1rem!important}.padding--md{padding:1rem!important}.padding-bottom--lg,.padding-vert--lg{padding-bottom:2rem!important}.padding-top--lg,.padding-vert--lg{padding-top:2rem!important}.padding-horiz--lg,.padding-left--lg{padding-left:2rem!important}.padding-horiz--lg,.padding-right--lg{padding-right:2rem!important}.padding--lg{padding:2rem!important}.padding-bottom--xl,.padding-vert--xl{padding-bottom:5rem!important}.padding-top--xl,.padding-vert--xl{padding-top:5rem!important}.padding-horiz--xl,.padding-left--xl{padding-left:5rem!important}.padding-horiz--xl,.padding-right--xl{padding-right:5rem!important}.padding--xl{padding:5rem!important}code{background-color:var(--ifm-code-background);border:.1rem solid #0000001a;border-radius:var(--ifm-code-border-radius);font-family:var(--ifm-font-family-monospace);font-size:var(--ifm-code-font-size);padding:var(--ifm-code-padding-vertical) var(--ifm-code-padding-horizontal)}a code{color:inherit}pre{background-color:var(--ifm-pre-background);border-radius:var(--ifm-pre-border-radius);color:var(--ifm-pre-color);font:var(--ifm-code-font-size)/var(--ifm-pre-line-height) var(--ifm-font-family-monospace);padding:var(--ifm-pre-padding)}pre code{background-color:initial;border:none;font-size:100%;line-height:inherit;padding:0}kbd{background-color:var(--ifm-color-emphasis-0);border:1px solid var(--ifm-color-emphasis-400);border-radius:.2rem;box-shadow:inset 0 -1px 0 var(--ifm-color-emphasis-400);color:var(--ifm-color-emphasis-800);font:80% var(--ifm-font-family-monospace);padding:.15rem .3rem}h1,h2,h3,h4,h5,h6{color:var(--ifm-heading-color);font-family:var(--ifm-heading-font-family);font-weight:var(--ifm-heading-font-weight);line-height:var(--ifm-heading-line-height);margin:var(--ifm-heading-margin-top) 0 var(--ifm-heading-margin-bottom) 0}h1{font-size:var(--ifm-h1-font-size)}h2{font-size:var(--ifm-h2-font-size)}h3{font-size:var(--ifm-h3-font-size)}h4{font-size:var(--ifm-h4-font-size)}h5{font-size:var(--ifm-h5-font-size)}h6{font-size:var(--ifm-h6-font-size)}img{max-width:100%}img[align=right]{padding-left:var(--image-alignment-padding)}img[align=left]{padding-right:var(--image-alignment-padding)}.markdown{--ifm-h1-vertical-rhythm-top:3;--ifm-h2-vertical-rhythm-top:2;--ifm-h3-vertical-rhythm-top:1.5;--ifm-heading-vertical-rhythm-top:1.25;--ifm-h1-vertical-rhythm-bottom:1.25;--ifm-heading-vertical-rhythm-bottom:1}.markdown:after,.markdown:before{content:"";display:table}.markdown:after{clear:both}.markdown h1:first-child{--ifm-h1-font-size:3rem;margin-bottom:calc(var(--ifm-h1-vertical-rhythm-bottom)*var(--ifm-leading))}.markdown>h2{--ifm-h2-font-size:2rem;margin-top:calc(var(--ifm-h2-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h3{--ifm-h3-font-size:1.5rem;margin-top:calc(var(--ifm-h3-vertical-rhythm-top)*var(--ifm-leading))}.markdown>h4,.markdown>h5,.markdown>h6{margin-top:calc(var(--ifm-heading-vertical-rhythm-top)*var(--ifm-leading))}.markdown>p,.markdown>pre,.markdown>ul{margin-bottom:var(--ifm-leading)}.markdown li>p{margin-top:var(--ifm-list-paragraph-margin)}.markdown li+li{margin-top:var(--ifm-list-item-margin)}ol,ul{margin:0 0 var(--ifm-list-margin);padding-left:var(--ifm-list-left-padding)}ol ol,ul ol{list-style-type:lower-roman}ol ol ol,ol ul ol,ul ol ol,ul ul ol{list-style-type:lower-alpha}table{border-collapse:collapse;display:block;margin-bottom:var(--ifm-spacing-vertical)}table thead tr{border-bottom:2px solid var(--ifm-table-border-color)}table thead,table tr:nth-child(2n){background-color:var(--ifm-table-stripe-background)}table tr{background-color:var(--ifm-table-background);border-top:var(--ifm-table-border-width) solid var(--ifm-table-border-color)}table td,table th{border:var(--ifm-table-border-width) solid var(--ifm-table-border-color);padding:var(--ifm-table-cell-padding)}table th{background-color:var(--ifm-table-head-background);color:var(--ifm-table-head-color);font-weight:var(--ifm-table-head-font-weight)}table td{color:var(--ifm-table-cell-color)}strong{font-weight:var(--ifm-font-weight-bold)}a{color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}a:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button:hover,.text--no-decoration,.text--no-decoration:hover,a:not([href]){text-decoration:none}p{margin:0 0 var(--ifm-paragraph-margin-bottom)}blockquote{border-left:var(--ifm-blockquote-border-left-width) solid var(--ifm-blockquote-border-color);box-shadow:var(--ifm-blockquote-shadow);color:var(--ifm-blockquote-color);font-size:var(--ifm-blockquote-font-size);padding:var(--ifm-blockquote-padding-vertical) var(--ifm-blockquote-padding-horizontal)}blockquote>:first-child{margin-top:0}blockquote>:last-child{margin-bottom:0}hr{background-color:var(--ifm-hr-background-color);border:0;height:var(--ifm-hr-height);margin:var(--ifm-hr-margin-vertical) 0}.shadow--lw{box-shadow:var(--ifm-global-shadow-lw)!important}.shadow--md{box-shadow:var(--ifm-global-shadow-md)!important}.shadow--tl{box-shadow:var(--ifm-global-shadow-tl)!important}.text--primary,.wordWrapButtonEnabled_EoeP .wordWrapButtonIcon_Bwma{color:var(--ifm-color-primary)}.text--secondary{color:var(--ifm-color-secondary)}.text--success{color:var(--ifm-color-success)}.text--info{color:var(--ifm-color-info)}.text--warning{color:var(--ifm-color-warning)}.text--danger{color:var(--ifm-color-danger)}.text--center{text-align:center}.text--left{text-align:left}.text--justify{text-align:justify}.text--right{text-align:right}.text--capitalize{text-transform:capitalize}.text--lowercase{text-transform:lowercase}.admonitionHeading_Gvgb,.alert__heading,.text--uppercase{text-transform:uppercase}.text--light{font-weight:var(--ifm-font-weight-light)}.text--normal{font-weight:var(--ifm-font-weight-normal)}.text--semibold{font-weight:var(--ifm-font-weight-semibold)}.text--bold{font-weight:var(--ifm-font-weight-bold)}.text--italic{font-style:italic}.text--truncate{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.text--break{word-wrap:break-word!important;word-break:break-word!important}.clean-btn{background:none;border:none;color:inherit;cursor:pointer;font-family:inherit;padding:0}.alert,.alert .close{color:var(--ifm-alert-foreground-color)}.clean-list{padding-left:0}.alert--primary{--ifm-alert-background-color:var(--ifm-color-primary-contrast-background);--ifm-alert-background-color-highlight:#3578e526;--ifm-alert-foreground-color:var(--ifm-color-primary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-primary-dark)}.alert--secondary{--ifm-alert-background-color:var(--ifm-color-secondary-contrast-background);--ifm-alert-background-color-highlight:#ebedf026;--ifm-alert-foreground-color:var(--ifm-color-secondary-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-secondary-dark)}.alert--success{--ifm-alert-background-color:var(--ifm-color-success-contrast-background);--ifm-alert-background-color-highlight:#00a40026;--ifm-alert-foreground-color:var(--ifm-color-success-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-success-dark)}.alert--info{--ifm-alert-background-color:var(--ifm-color-info-contrast-background);--ifm-alert-background-color-highlight:#54c7ec26;--ifm-alert-foreground-color:var(--ifm-color-info-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-info-dark)}.alert--warning{--ifm-alert-background-color:var(--ifm-color-warning-contrast-background);--ifm-alert-background-color-highlight:#ffba0026;--ifm-alert-foreground-color:var(--ifm-color-warning-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-warning-dark)}.alert--danger{--ifm-alert-background-color:var(--ifm-color-danger-contrast-background);--ifm-alert-background-color-highlight:#fa383e26;--ifm-alert-foreground-color:var(--ifm-color-danger-contrast-foreground);--ifm-alert-border-color:var(--ifm-color-danger-dark)}.alert{--ifm-code-background:var(--ifm-alert-background-color-highlight);--ifm-link-color:var(--ifm-alert-foreground-color);--ifm-link-hover-color:var(--ifm-alert-foreground-color);--ifm-link-decoration:underline;--ifm-tabs-color:var(--ifm-alert-foreground-color);--ifm-tabs-color-active:var(--ifm-alert-foreground-color);--ifm-tabs-color-active-border:var(--ifm-alert-border-color);background-color:var(--ifm-alert-background-color);border:var(--ifm-alert-border-width) solid var(--ifm-alert-border-color);border-left-width:var(--ifm-alert-border-left-width);border-radius:var(--ifm-alert-border-radius);box-shadow:var(--ifm-alert-shadow);padding:var(--ifm-alert-padding-vertical) var(--ifm-alert-padding-horizontal)}.alert__heading{align-items:center;display:flex;font:700 var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family);margin-bottom:.5rem}.alert__icon{display:inline-flex;margin-right:.4em}.alert__icon svg{fill:var(--ifm-alert-foreground-color);stroke:var(--ifm-alert-foreground-color);stroke-width:0}.alert .close{margin:calc(var(--ifm-alert-padding-vertical)*-1) calc(var(--ifm-alert-padding-horizontal)*-1) 0 0;opacity:.75}.alert .close:focus,.alert .close:hover{opacity:1}.alert a{text-decoration-color:var(--ifm-alert-border-color)}.alert a:hover{text-decoration-thickness:2px}.avatar{column-gap:var(--ifm-avatar-intro-margin);display:flex}.avatar__photo{border-radius:50%;display:block;height:var(--ifm-avatar-photo-size);overflow:hidden;width:var(--ifm-avatar-photo-size)}.card--full-height,.navbar__logo img,body,html{height:100%}.avatar__photo--sm{--ifm-avatar-photo-size:2rem}.avatar__photo--lg{--ifm-avatar-photo-size:4rem}.avatar__photo--xl{--ifm-avatar-photo-size:6rem}.avatar__intro{display:flex;flex:1 1;flex-direction:column;justify-content:center;text-align:var(--ifm-avatar-intro-alignment)}.badge,.breadcrumbs__item,.breadcrumbs__link,.button,.dropdown>.navbar__link:after{display:inline-block}.avatar__name{font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base)}.avatar__subtitle{margin-top:.25rem}.avatar--vertical{--ifm-avatar-intro-alignment:center;--ifm-avatar-intro-margin:0.5rem;align-items:center;flex-direction:column}.badge{background-color:var(--ifm-badge-background-color);border:var(--ifm-badge-border-width) solid var(--ifm-badge-border-color);border-radius:var(--ifm-badge-border-radius);color:var(--ifm-badge-color);font-size:75%;font-weight:var(--ifm-font-weight-bold);line-height:1;padding:var(--ifm-badge-padding-vertical) var(--ifm-badge-padding-horizontal)}.badge--primary{--ifm-badge-background-color:var(--ifm-color-primary)}.badge--secondary{--ifm-badge-background-color:var(--ifm-color-secondary);color:var(--ifm-color-black)}.breadcrumbs__link,.button.button--secondary.button--outline:not(.button--active):not(:hover){color:var(--ifm-font-color-base)}.badge--success{--ifm-badge-background-color:var(--ifm-color-success)}.badge--info{--ifm-badge-background-color:var(--ifm-color-info)}.badge--warning{--ifm-badge-background-color:var(--ifm-color-warning)}.badge--danger{--ifm-badge-background-color:var(--ifm-color-danger)}.breadcrumbs{margin-bottom:0;padding-left:0}.breadcrumbs__item:not(:last-child):after{background:var(--ifm-breadcrumb-separator) center;content:" ";display:inline-block;filter:var(--ifm-breadcrumb-separator-filter);height:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier));margin:0 var(--ifm-breadcrumb-spacing);opacity:.5;width:calc(var(--ifm-breadcrumb-separator-size)*var(--ifm-breadcrumb-size-multiplier)*var(--ifm-breadcrumb-separator-size-multiplier))}.breadcrumbs__item--active .breadcrumbs__link{background:var(--ifm-breadcrumb-item-background-active);color:var(--ifm-breadcrumb-color-active)}.breadcrumbs__link{border-radius:var(--ifm-breadcrumb-border-radius);font-size:calc(1rem*var(--ifm-breadcrumb-size-multiplier));padding:calc(var(--ifm-breadcrumb-padding-vertical)*var(--ifm-breadcrumb-size-multiplier)) calc(var(--ifm-breadcrumb-padding-horizontal)*var(--ifm-breadcrumb-size-multiplier));transition-duration:var(--ifm-transition-fast);transition-property:background,color}.breadcrumbs__link:any-link:hover,.breadcrumbs__link:link:hover,.breadcrumbs__link:visited:hover,area[href].breadcrumbs__link:hover{background:var(--ifm-breadcrumb-item-background-active);text-decoration:none}.breadcrumbs--sm{--ifm-breadcrumb-size-multiplier:0.8}.breadcrumbs--lg{--ifm-breadcrumb-size-multiplier:1.2}.button{background-color:var(--ifm-button-background-color);border:var(--ifm-button-border-width) solid var(--ifm-button-border-color);border-radius:var(--ifm-button-border-radius);cursor:pointer;font-size:calc(.875rem*var(--ifm-button-size-multiplier));font-weight:var(--ifm-button-font-weight);line-height:1.5;padding:calc(var(--ifm-button-padding-vertical)*var(--ifm-button-size-multiplier)) calc(var(--ifm-button-padding-horizontal)*var(--ifm-button-size-multiplier));text-align:center;transition-duration:var(--ifm-button-transition-duration);transition-property:color,background,border-color;-webkit-user-select:none;user-select:none;white-space:nowrap}.button,.button:hover{color:var(--ifm-button-color)}.button--outline{--ifm-button-color:var(--ifm-button-border-color)}.button--outline:hover{--ifm-button-background-color:var(--ifm-button-border-color)}.button--link{--ifm-button-border-color:#0000;color:var(--ifm-link-color);text-decoration:var(--ifm-link-decoration)}.button--link.button--active,.button--link:active,.button--link:hover{color:var(--ifm-link-hover-color);text-decoration:var(--ifm-link-hover-decoration)}.button.disabled,.button:disabled,.button[disabled]{opacity:.65;pointer-events:none}.button--sm{--ifm-button-size-multiplier:0.8}.button--lg{--ifm-button-size-multiplier:1.35}.button--block{display:block;width:100%}.button.button--secondary{color:var(--ifm-color-gray-900)}:where(.button--primary){--ifm-button-background-color:var(--ifm-color-primary);--ifm-button-border-color:var(--ifm-color-primary)}:where(.button--primary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-primary-dark);--ifm-button-border-color:var(--ifm-color-primary-dark)}.button--primary.button--active,.button--primary:active{--ifm-button-background-color:var(--ifm-color-primary-darker);--ifm-button-border-color:var(--ifm-color-primary-darker)}:where(.button--secondary){--ifm-button-background-color:var(--ifm-color-secondary);--ifm-button-border-color:var(--ifm-color-secondary)}:where(.button--secondary):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-secondary-dark);--ifm-button-border-color:var(--ifm-color-secondary-dark)}.button--secondary.button--active,.button--secondary:active{--ifm-button-background-color:var(--ifm-color-secondary-darker);--ifm-button-border-color:var(--ifm-color-secondary-darker)}:where(.button--success){--ifm-button-background-color:var(--ifm-color-success);--ifm-button-border-color:var(--ifm-color-success)}:where(.button--success):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-success-dark);--ifm-button-border-color:var(--ifm-color-success-dark)}.button--success.button--active,.button--success:active{--ifm-button-background-color:var(--ifm-color-success-darker);--ifm-button-border-color:var(--ifm-color-success-darker)}:where(.button--info){--ifm-button-background-color:var(--ifm-color-info);--ifm-button-border-color:var(--ifm-color-info)}:where(.button--info):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-info-dark);--ifm-button-border-color:var(--ifm-color-info-dark)}.button--info.button--active,.button--info:active{--ifm-button-background-color:var(--ifm-color-info-darker);--ifm-button-border-color:var(--ifm-color-info-darker)}:where(.button--warning){--ifm-button-background-color:var(--ifm-color-warning);--ifm-button-border-color:var(--ifm-color-warning)}:where(.button--warning):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-warning-dark);--ifm-button-border-color:var(--ifm-color-warning-dark)}.button--warning.button--active,.button--warning:active{--ifm-button-background-color:var(--ifm-color-warning-darker);--ifm-button-border-color:var(--ifm-color-warning-darker)}:where(.button--danger){--ifm-button-background-color:var(--ifm-color-danger);--ifm-button-border-color:var(--ifm-color-danger)}:where(.button--danger):not(.button--outline):hover{--ifm-button-background-color:var(--ifm-color-danger-dark);--ifm-button-border-color:var(--ifm-color-danger-dark)}.button--danger.button--active,.button--danger:active{--ifm-button-background-color:var(--ifm-color-danger-darker);--ifm-button-border-color:var(--ifm-color-danger-darker)}.button-group{display:inline-flex;gap:var(--ifm-button-group-spacing)}.button-group>.button:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.button-group>.button:not(:last-child){border-bottom-right-radius:0;border-top-right-radius:0}.button-group--block{display:flex;justify-content:stretch}.button-group--block>.button{flex-grow:1}.card{background-color:var(--ifm-card-background-color);border-radius:var(--ifm-card-border-radius);box-shadow:var(--ifm-global-shadow-lw);display:flex;flex-direction:column;overflow:hidden}.card__image{padding-top:var(--ifm-card-vertical-spacing)}.card__image:first-child{padding-top:0}.card__body,.card__footer,.card__header{padding:var(--ifm-card-vertical-spacing) var(--ifm-card-horizontal-spacing)}.card__body:not(:last-child),.card__footer:not(:last-child),.card__header:not(:last-child){padding-bottom:0}.card__body>:last-child,.card__footer>:last-child,.card__header>:last-child{margin-bottom:0}.card__footer{margin-top:auto}.table-of-contents{font-size:.8rem;margin-bottom:0;padding:var(--ifm-toc-padding-vertical) 0}.table-of-contents,.table-of-contents ul{list-style:none;padding-left:var(--ifm-toc-padding-horizontal)}.table-of-contents li{margin:var(--ifm-toc-padding-vertical) var(--ifm-toc-padding-horizontal)}.table-of-contents__left-border{border-left:1px solid var(--ifm-toc-border-color)}.table-of-contents__link{color:var(--ifm-toc-link-color);display:block}.table-of-contents__link--active,.table-of-contents__link--active code,.table-of-contents__link:hover,.table-of-contents__link:hover code{color:var(--ifm-color-primary);text-decoration:none}.close{color:var(--ifm-color-black);float:right;font-size:1.5rem;font-weight:var(--ifm-font-weight-bold);line-height:1;opacity:.5;padding:1rem;transition:opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.close:hover{opacity:.7}.close:focus,.theme-code-block-highlighted-line .codeLineNumber_Tfdd:before{opacity:.8}.dropdown{display:inline-flex;font-weight:var(--ifm-dropdown-font-weight);position:relative;vertical-align:top}.dropdown--hoverable:hover .dropdown__menu,.dropdown--show .dropdown__menu{opacity:1;pointer-events:all;transform:translateY(-1px);visibility:visible}#nprogress,.dropdown__menu,.navbar__item.dropdown .navbar__link:not([href]){pointer-events:none}.dropdown--right .dropdown__menu{left:inherit;right:0}.dropdown--nocaret .navbar__link:after{content:none!important}.dropdown__menu{background-color:var(--ifm-dropdown-background-color);border-radius:var(--ifm-global-radius);box-shadow:var(--ifm-global-shadow-md);left:0;max-height:80vh;min-width:10rem;opacity:0;overflow-y:auto;padding:.5rem;position:absolute;top:calc(100% - var(--ifm-navbar-item-padding-vertical) + .3rem);transform:translateY(-.625rem);transition-duration:var(--ifm-transition-fast);transition-property:opacity,transform,visibility;transition-timing-function:var(--ifm-transition-timing-default);visibility:hidden;z-index:var(--ifm-z-index-dropdown)}.sidebar_re4s,.tableOfContents_bqdL{max-height:calc(100vh - var(--ifm-navbar-height) - 2rem)}.menu__caret,.menu__link,.menu__list-item-collapsible{border-radius:.25rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.dropdown__link{border-radius:.25rem;color:var(--ifm-dropdown-link-color);display:block;font-size:.875rem;margin-top:.2rem;padding:.25rem .5rem;white-space:nowrap}.dropdown__link--active,.dropdown__link:hover{background-color:var(--ifm-dropdown-hover-background-color);color:var(--ifm-dropdown-link-color);text-decoration:none}.dropdown__link--active,.dropdown__link--active:hover{--ifm-dropdown-link-color:var(--ifm-link-color)}.dropdown>.navbar__link:after{border-color:currentcolor #0000;border-style:solid;border-width:.4em .4em 0;content:"";margin-left:.3em;position:relative;top:2px;transform:translateY(-50%)}.footer{background-color:var(--ifm-footer-background-color);color:var(--ifm-footer-color);padding:var(--ifm-footer-padding-vertical) var(--ifm-footer-padding-horizontal)}.footer--dark{--ifm-footer-background-color:#303846;--ifm-footer-color:var(--ifm-footer-link-color);--ifm-footer-link-color:var(--ifm-color-secondary);--ifm-footer-title-color:var(--ifm-color-white)}.footer__links{margin-bottom:1rem}.footer__link-item{color:var(--ifm-footer-link-color);line-height:2}.footer__link-item:hover{color:var(--ifm-footer-link-hover-color)}.footer__link-separator{margin:0 var(--ifm-footer-link-horizontal-spacing)}.footer__logo{margin-top:1rem;max-width:var(--ifm-footer-logo-max-width)}.footer__title{color:var(--ifm-footer-title-color);font:700 var(--ifm-h4-font-size)/var(--ifm-heading-line-height) var(--ifm-font-family-base);margin-bottom:var(--ifm-heading-margin-bottom)}.menu,.navbar__link{font-weight:var(--ifm-font-weight-semibold)}.docItemContainer_Djhp article>:first-child,.docItemContainer_Djhp header+*,.footer__item{margin-top:0}.admonitionContent_BuS1>:last-child,.collapsibleContent_i85q p:last-child,.details_lb9f>summary>p:last-child,.footer__items{margin-bottom:0}.codeBlockStandalone_MEMb,[type=checkbox]{padding:0}.hero{align-items:center;background-color:var(--ifm-hero-background-color);color:var(--ifm-hero-text-color);display:flex;padding:4rem 2rem}.hero--primary{--ifm-hero-background-color:var(--ifm-color-primary);--ifm-hero-text-color:var(--ifm-font-color-base-inverse)}.hero--dark{--ifm-hero-background-color:#303846;--ifm-hero-text-color:var(--ifm-color-white)}.hero__title,.title_f1Hy{font-size:3rem}.hero__subtitle{font-size:1.5rem}.menu__list{margin:0;padding-left:0}.menu__caret,.menu__link{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu__list .menu__list{flex:0 0 100%;margin-top:.25rem;padding-left:var(--ifm-menu-link-padding-horizontal)}.menu__list-item:not(:first-child){margin-top:.25rem}.menu__list-item--collapsed .menu__list{height:0;overflow:hidden}.details_lb9f[data-collapsed=false].isBrowser_bmU9>summary:before,.details_lb9f[open]:not(.isBrowser_bmU9)>summary:before,.menu__list-item--collapsed .menu__caret:before,.menu__list-item--collapsed .menu__link--sublist:after{transform:rotate(90deg)}.menu__list-item-collapsible{display:flex;flex-wrap:wrap;position:relative}.menu__caret:hover,.menu__link:hover,.menu__list-item-collapsible--active,.menu__list-item-collapsible:hover{background:var(--ifm-menu-color-background-hover)}.menu__list-item-collapsible .menu__link--active,.menu__list-item-collapsible .menu__link:hover{background:none!important}.menu__caret,.menu__link{align-items:center;display:flex}.menu__link{color:var(--ifm-menu-color);flex:1;line-height:1.25}.menu__link:hover{color:var(--ifm-menu-color);text-decoration:none}.menu__caret:before,.menu__link--sublist-caret:after{content:"";height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast) linear;width:1.25rem;filter:var(--ifm-menu-link-sublist-icon-filter)}.menu__link--sublist-caret:after{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem;margin-left:auto;min-width:1.25rem}.menu__link--active,.menu__link--active:hover{color:var(--ifm-menu-color-active)}.navbar__brand,.navbar__link{color:var(--ifm-navbar-link-color)}.menu__link--active:not(.menu__link--sublist){background-color:var(--ifm-menu-color-background-active)}.menu__caret:before{background:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem}.navbar--dark,html[data-theme=dark]{--ifm-menu-link-sublist-icon-filter:invert(100%) sepia(94%) saturate(17%) hue-rotate(223deg) brightness(104%) contrast(98%)}.navbar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-navbar-shadow);height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar,.navbar>.container,.navbar>.container-fluid{display:flex}.navbar--fixed-top{position:sticky;top:0;z-index:var(--ifm-z-index-fixed)}.navbar-sidebar,.navbar-sidebar__backdrop{bottom:0;opacity:0;position:fixed;transition-duration:var(--ifm-transition-fast);transition-timing-function:ease-in-out;left:0;top:0;visibility:hidden}.navbar__inner{display:flex;flex-wrap:wrap;justify-content:space-between;width:100%}.navbar__brand{align-items:center;display:flex;margin-right:1rem;min-width:0}.navbar__brand:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.announcementBarContent_xLdY,.navbar__title{flex:1 1 auto}.navbar__toggle{display:none;margin-right:.5rem}.navbar__logo{flex:0 0 auto;height:2rem;margin-right:.5rem}.navbar__items{align-items:center;display:flex;flex:1;min-width:0}.navbar__items--center{flex:0 0 auto}.navbar__items--center .navbar__brand{margin:0}.navbar__items--center+.navbar__items--right{flex:1}.navbar__items--right{flex:0 0 auto;justify-content:flex-end}.navbar__items--right>:last-child{padding-right:0}.navbar__item{display:inline-block;padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.navbar__link--active,.navbar__link:hover{color:var(--ifm-navbar-link-hover-color);text-decoration:none}.navbar--dark,.navbar--primary{--ifm-menu-color:var(--ifm-color-gray-300);--ifm-navbar-link-color:var(--ifm-color-gray-100);--ifm-navbar-search-input-background-color:#ffffff1a;--ifm-navbar-search-input-placeholder-color:#ffffff80;color:var(--ifm-color-white)}.navbar--dark{--ifm-navbar-background-color:#242526;--ifm-menu-color-background-active:#ffffff0d;--ifm-navbar-search-input-color:var(--ifm-color-white)}.navbar--primary{--ifm-navbar-background-color:var(--ifm-color-primary);--ifm-navbar-link-hover-color:var(--ifm-color-white);--ifm-menu-color-active:var(--ifm-color-white);--ifm-navbar-search-input-color:var(--ifm-color-emphasis-500)}.navbar__search-input{-webkit-appearance:none;appearance:none;background:var(--ifm-navbar-search-input-background-color) var(--ifm-navbar-search-input-icon) no-repeat .75rem center/1rem 1rem;border:none;border-radius:2rem;color:var(--ifm-navbar-search-input-color);cursor:text;display:inline-block;font-size:.9rem;height:2rem;padding:0 .5rem 0 2.25rem;width:12.5rem}.navbar__search-input::placeholder{color:var(--ifm-navbar-search-input-placeholder-color)}.navbar-sidebar{background-color:var(--ifm-navbar-background-color);box-shadow:var(--ifm-global-shadow-md);transform:translate3d(-100%,0,0);transition-property:opacity,visibility,transform;width:var(--ifm-navbar-sidebar-width)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar__items{transform:translateZ(0)}.navbar-sidebar--show .navbar-sidebar,.navbar-sidebar--show .navbar-sidebar__backdrop{opacity:1;visibility:visible}.navbar-sidebar__backdrop{background-color:#0009;right:0;transition-property:opacity,visibility}.navbar-sidebar__brand{align-items:center;box-shadow:var(--ifm-navbar-shadow);display:flex;flex:1;height:var(--ifm-navbar-height);padding:var(--ifm-navbar-padding-vertical) var(--ifm-navbar-padding-horizontal)}.navbar-sidebar__items{display:flex;height:calc(100% - var(--ifm-navbar-height));transition:transform var(--ifm-transition-fast) ease-in-out}.navbar-sidebar__items--show-secondary{transform:translate3d(calc((var(--ifm-navbar-sidebar-width))*-1),0,0)}.navbar-sidebar__item{flex-shrink:0;padding:.5rem;width:calc(var(--ifm-navbar-sidebar-width))}.navbar-sidebar__back{background:var(--ifm-menu-color-background-active);font-size:15px;font-weight:var(--ifm-button-font-weight);margin:0 0 .2rem -.5rem;padding:.6rem 1.5rem;position:relative;text-align:left;top:-.5rem;width:calc(100% + 1rem)}.navbar-sidebar__close{display:flex;margin-left:auto}.pagination{column-gap:var(--ifm-pagination-page-spacing);display:flex;font-size:var(--ifm-pagination-font-size);padding-left:0}.pagination--sm{--ifm-pagination-font-size:0.8rem;--ifm-pagination-padding-horizontal:0.8rem;--ifm-pagination-padding-vertical:0.2rem}.pagination--lg{--ifm-pagination-font-size:1.2rem;--ifm-pagination-padding-horizontal:1.2rem;--ifm-pagination-padding-vertical:0.3rem}.pagination__item{display:inline-flex}.pagination__item>span{padding:var(--ifm-pagination-padding-vertical)}.pagination__item--active .pagination__link{color:var(--ifm-pagination-color-active)}.pagination__item--active .pagination__link,.pagination__item:not(.pagination__item--active):hover .pagination__link{background:var(--ifm-pagination-item-active-background)}.pagination__item--disabled,.pagination__item[disabled]{opacity:.25;pointer-events:none}.pagination__link{border-radius:var(--ifm-pagination-border-radius);color:var(--ifm-font-color-base);display:inline-block;padding:var(--ifm-pagination-padding-vertical) var(--ifm-pagination-padding-horizontal);transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination__link:hover,.sidebarItemLink_mo7H:hover{text-decoration:none}.pagination-nav{grid-gap:var(--ifm-spacing-horizontal);display:grid;gap:var(--ifm-spacing-horizontal);grid-template-columns:repeat(2,1fr)}.pagination-nav__link{border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-pagination-nav-border-radius);display:block;height:100%;line-height:var(--ifm-heading-line-height);padding:var(--ifm-global-spacing);transition:border-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.pagination-nav__link:hover{border-color:var(--ifm-pagination-nav-color-hover);text-decoration:none}.pagination-nav__link--next{grid-column:2/3;text-align:right}.pagination-nav__label{font-size:var(--ifm-h4-font-size);font-weight:var(--ifm-heading-font-weight);word-break:break-word}.pagination-nav__link--prev .pagination-nav__label:before{content:"« "}.pagination-nav__link--next .pagination-nav__label:after{content:" »"}.pagination-nav__sublabel{color:var(--ifm-color-content-secondary);font-size:var(--ifm-h5-font-size);font-weight:var(--ifm-font-weight-semibold);margin-bottom:.25rem}.pills__item,.sidebarItemTitle_pO2u,.tabs{font-weight:var(--ifm-font-weight-bold)}.pills{display:flex;gap:var(--ifm-pills-spacing);padding-left:0}.pills__item{border-radius:.5rem;cursor:pointer;display:inline-block;padding:.25rem 1rem;transition:background var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs,:not(.containsTaskList_mC6p>li)>.containsTaskList_mC6p{padding-left:0}.pills__item--active{color:var(--ifm-pills-color-active)}.pills__item--active,.pills__item:not(.pills__item--active):hover{background:var(--ifm-pills-color-background-active)}.pills--block{justify-content:stretch}.pills--block .pills__item{flex-grow:1;text-align:center}.tabs{color:var(--ifm-tabs-color);display:flex;margin-bottom:0;overflow-x:auto}.tabs__item{border-bottom:3px solid #0000;border-radius:var(--ifm-global-radius);cursor:pointer;display:inline-flex;padding:var(--ifm-tabs-padding-vertical) var(--ifm-tabs-padding-horizontal);transition:background-color var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.tabs__item--active{border-bottom-color:var(--ifm-tabs-color-active-border);border-bottom-left-radius:0;border-bottom-right-radius:0;color:var(--ifm-tabs-color-active)}.tabs__item:hover{background-color:var(--ifm-hover-overlay)}.tabs--block{justify-content:stretch}.tabs--block .tabs__item{flex-grow:1;justify-content:center}html[data-theme=dark]{--ifm-color-scheme:dark;--ifm-color-emphasis-0:var(--ifm-color-gray-1000);--ifm-color-emphasis-100:var(--ifm-color-gray-900);--ifm-color-emphasis-200:var(--ifm-color-gray-800);--ifm-color-emphasis-300:var(--ifm-color-gray-700);--ifm-color-emphasis-400:var(--ifm-color-gray-600);--ifm-color-emphasis-600:var(--ifm-color-gray-400);--ifm-color-emphasis-700:var(--ifm-color-gray-300);--ifm-color-emphasis-800:var(--ifm-color-gray-200);--ifm-color-emphasis-900:var(--ifm-color-gray-100);--ifm-color-emphasis-1000:var(--ifm-color-gray-0);--ifm-background-color:#1b1b1d;--ifm-background-surface-color:#242526;--ifm-hover-overlay:#ffffff0d;--ifm-color-content:#e3e3e3;--ifm-color-content-secondary:#fff;--ifm-breadcrumb-separator-filter:invert(64%) sepia(11%) saturate(0%) hue-rotate(149deg) brightness(99%) contrast(95%);--ifm-code-background:#ffffff1a;--ifm-scrollbar-track-background-color:#444;--ifm-scrollbar-thumb-background-color:#686868;--ifm-scrollbar-thumb-hover-background-color:#7a7a7a;--ifm-table-stripe-background:#ffffff12;--ifm-toc-border-color:var(--ifm-color-emphasis-200);--ifm-color-primary-contrast-background:#102445;--ifm-color-primary-contrast-foreground:#ebf2fc;--ifm-color-secondary-contrast-background:#474748;--ifm-color-secondary-contrast-foreground:#fdfdfe;--ifm-color-success-contrast-background:#003100;--ifm-color-success-contrast-foreground:#e6f6e6;--ifm-color-info-contrast-background:#193c47;--ifm-color-info-contrast-foreground:#eef9fd;--ifm-color-warning-contrast-background:#4d3800;--ifm-color-warning-contrast-foreground:#fff8e6;--ifm-color-danger-contrast-background:#4b1113;--ifm-color-danger-contrast-foreground:#ffebec}#nprogress .bar{background:var(--docusaurus-progress-bar-color);height:2px;left:0;position:fixed;top:0;width:100%;z-index:1031}#nprogress .peg{box-shadow:0 0 10px var(--docusaurus-progress-bar-color),0 0 5px var(--docusaurus-progress-bar-color);height:100%;opacity:1;position:absolute;right:0;transform:rotate(3deg) translateY(-4px);width:100px}[data-theme=dark]{--ifm-color-primary:#25c2a0;--ifm-color-primary-dark:#21af90;--ifm-color-primary-darker:#1fa588;--ifm-color-primary-darkest:#1a8870;--ifm-color-primary-light:#29d5b0;--ifm-color-primary-lighter:#32d8b4;--ifm-color-primary-lightest:#4fddbf;--docusaurus-highlighted-code-line-bg:#0000004d}body:not(.navigation-with-keyboard) :not(input):focus{outline:0}#__docusaurus-base-url-issue-banner-container,.docSidebarContainer_YfHR,.sidebarLogo_isFc,.themedComponent_mlkZ,[data-theme=dark] .lightToggleIcon_pyhR,[data-theme=light] .darkToggleIcon_wfgR,html[data-announcement-bar-initially-dismissed=true] .announcementBar_mb4j{display:none}.skipToContent_fXgn{background-color:var(--ifm-background-surface-color);color:var(--ifm-color-emphasis-900);left:100%;padding:calc(var(--ifm-global-spacing)/2) var(--ifm-global-spacing);position:fixed;top:1rem;z-index:calc(var(--ifm-z-index-fixed) + 1)}.skipToContent_fXgn:focus{box-shadow:var(--ifm-global-shadow-md);left:1rem}.closeButton_CVFx{line-height:0;padding:0}.content_knG7{font-size:85%;padding:5px 0;text-align:center}.content_knG7 a{color:inherit;text-decoration:underline}.announcementBar_mb4j{align-items:center;background-color:var(--ifm-color-white);border-bottom:1px solid var(--ifm-color-emphasis-100);color:var(--ifm-color-black);display:flex;height:var(--docusaurus-announcement-bar-height)}.announcementBarPlaceholder_vyr4{flex:0 0 10px}.announcementBarClose_gvF7{align-self:stretch;flex:0 0 30px}.toggle_vylO{height:2rem;width:2rem}.toggleButton_gllP{align-items:center;border-radius:50%;display:flex;height:100%;justify-content:center;transition:background var(--ifm-transition-fast);width:100%}.toggleButton_gllP:hover{background:var(--ifm-color-emphasis-200)}.toggleButtonDisabled_aARS{cursor:not-allowed}.darkNavbarColorModeToggle_X3D1:hover{background:var(--ifm-color-gray-800)}[data-theme=dark] .themedComponent--dark_xIcU,[data-theme=light] .themedComponent--light_NVdE,html:not([data-theme]) .themedComponent--light_NVdE{display:initial}.iconExternalLink_nPIU{margin-left:.3rem}.dropdownNavbarItemMobile_S0Fm{cursor:pointer}.iconLanguage_nlXk{margin-right:5px;vertical-align:text-bottom}@supports selector(:has(*)){.navbarSearchContainer_Bca1:not(:has(>*)){display:none}}.navbarHideable_m1mJ{transition:transform var(--ifm-transition-fast) ease}.navbarHidden_jGov{transform:translate3d(0,calc(-100% - 2px),0)}.errorBoundaryError_a6uf{color:red;white-space:pre-wrap}.errorBoundaryFallback_VBag{color:red;padding:.55rem}.footerLogoLink_BH7S{opacity:.5;transition:opacity var(--ifm-transition-fast) var(--ifm-transition-timing-default)}.footerLogoLink_BH7S:hover,.hash-link:focus,:hover>.hash-link{opacity:1}.anchorWithStickyNavbar_LWe7{scroll-margin-top:calc(var(--ifm-navbar-height) + .5rem)}.anchorWithHideOnScrollNavbar_WYt5{scroll-margin-top:.5rem}.hash-link{opacity:0;padding-left:.5rem;transition:opacity var(--ifm-transition-fast);-webkit-user-select:none;user-select:none}.hash-link:before{content:"#"}.mainWrapper_z2l0{display:flex;flex:1 0 auto;flex-direction:column}.docusaurus-mt-lg{margin-top:3rem}#__docusaurus{display:flex;flex-direction:column;min-height:100%}.sidebar_re4s{overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 2rem)}.sidebarItemTitle_pO2u{font-size:var(--ifm-h3-font-size)}.container_mt6G,.sidebarItemList_Yudw{font-size:.9rem}.sidebarItem__DBe{margin-top:.7rem}.sidebarItemLink_mo7H{color:var(--ifm-font-color-base);display:block}.sidebarItemLinkActive_I1ZP{color:var(--ifm-color-primary)!important}.authorCol_Hf19{flex-grow:1!important;max-width:inherit!important}.imageOnlyAuthorRow_pa_O{display:flex;flex-flow:row wrap}.imageOnlyAuthorCol_G86a{margin-left:.3rem;margin-right:.3rem}.backToTopButton_sjWU{background-color:var(--ifm-color-emphasis-200);border-radius:50%;bottom:1.3rem;box-shadow:var(--ifm-global-shadow-lw);height:3rem;opacity:0;position:fixed;right:1.3rem;transform:scale(0);transition:all var(--ifm-transition-fast) var(--ifm-transition-timing-default);visibility:hidden;width:3rem;z-index:calc(var(--ifm-z-index-fixed) - 1)}.buttonGroup__atx button,.codeBlockContainer_Ckt0{background:var(--prism-background-color);color:var(--prism-color)}.backToTopButton_sjWU:after{background-color:var(--ifm-color-emphasis-1000);content:" ";display:inline-block;height:100%;-webkit-mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;mask:var(--ifm-menu-link-sublist-icon) 50%/2rem 2rem no-repeat;width:100%}.backToTopButtonShow_xfvO{opacity:1;transform:scale(1);visibility:visible}[data-theme=dark]:root{--docusaurus-collapse-button-bg:#ffffff0d;--docusaurus-collapse-button-bg-hover:#ffffff1a}.collapseSidebarButton_PEFL{display:none;margin:0}.docMainContainer_TBSr,.docRoot_UBD9{display:flex;width:100%}.docsWrapper_hBAB{display:flex;flex:1 0 auto}.buttons_AeoN,.features_t9lD{align-items:center;display:flex}.features_t9lD{padding:2rem 0;width:100%}.featureSvg_GfXr{height:200px;width:200px}.heroBanner_qdFl{overflow:hidden;padding:4rem 0;position:relative;text-align:center}.buttons_AeoN{justify-content:center}.codeBlockContainer_Ckt0{border-radius:var(--ifm-code-border-radius);box-shadow:var(--ifm-global-shadow-lw);margin-bottom:var(--ifm-leading)}.codeBlockContent_biex{border-radius:inherit;direction:ltr;position:relative}.codeBlockTitle_Ktv7{border-bottom:1px solid var(--ifm-color-emphasis-300);border-top-left-radius:inherit;border-top-right-radius:inherit;font-size:var(--ifm-code-font-size);font-weight:500;padding:.75rem var(--ifm-pre-padding)}.codeBlock_bY9V{--ifm-pre-background:var(--prism-background-color);margin:0;padding:0}.codeBlockTitle_Ktv7+.codeBlockContent_biex .codeBlock_bY9V{border-top-left-radius:0;border-top-right-radius:0}.codeBlockLines_e6Vv{float:left;font:inherit;min-width:100%;padding:var(--ifm-pre-padding)}.codeBlockLinesWithNumbering_o6Pm{display:table;padding:var(--ifm-pre-padding) 0}.buttonGroup__atx{column-gap:.2rem;display:flex;position:absolute;right:calc(var(--ifm-pre-padding)/2);top:calc(var(--ifm-pre-padding)/2)}.buttonGroup__atx button{align-items:center;border:1px solid var(--ifm-color-emphasis-300);border-radius:var(--ifm-global-radius);display:flex;line-height:0;opacity:0;padding:.4rem;transition:opacity var(--ifm-transition-fast) ease-in-out}.buttonGroup__atx button:focus-visible,.buttonGroup__atx button:hover{opacity:1!important}.theme-code-block:hover .buttonGroup__atx button{opacity:.4}.iconEdit_Z9Sw{margin-right:.3em;vertical-align:sub}:where(:root){--docusaurus-highlighted-code-line-bg:#484d5b}:where([data-theme=dark]){--docusaurus-highlighted-code-line-bg:#646464}.theme-code-block-highlighted-line{background-color:var(--docusaurus-highlighted-code-line-bg);display:block;margin:0 calc(var(--ifm-pre-padding)*-1);padding:0 var(--ifm-pre-padding)}.codeLine_lJS_{counter-increment:a;display:table-row}.codeLineNumber_Tfdd{background:var(--ifm-pre-background);display:table-cell;left:0;overflow-wrap:normal;padding:0 var(--ifm-pre-padding);position:sticky;text-align:right;width:1%}.codeLineNumber_Tfdd:before{content:counter(a);opacity:.4}.codeLineContent_feaV{padding-right:var(--ifm-pre-padding)}.tag_zVej{border:1px solid var(--docusaurus-tag-list-border);transition:border var(--ifm-transition-fast)}.tag_zVej:hover{--docusaurus-tag-list-border:var(--ifm-link-color);text-decoration:none}.tagRegular_sFm0{border-radius:var(--ifm-global-radius);font-size:90%;padding:.2rem .5rem .3rem}.tagWithCount_h2kH{align-items:center;border-left:0;display:flex;padding:0 .5rem 0 1rem;position:relative}.tagWithCount_h2kH:after,.tagWithCount_h2kH:before{border:1px solid var(--docusaurus-tag-list-border);content:"";position:absolute;top:50%;transition:inherit}.tagWithCount_h2kH:before{border-bottom:0;border-right:0;height:1.18rem;right:100%;transform:translate(50%,-50%) rotate(-45deg);width:1.18rem}.tagWithCount_h2kH:after{border-radius:50%;height:.5rem;left:0;transform:translateY(-50%);width:.5rem}.tagWithCount_h2kH span{background:var(--ifm-color-secondary);border-radius:var(--ifm-global-radius);color:var(--ifm-color-black);font-size:.7rem;line-height:1.2;margin-left:.3rem;padding:.1rem .4rem}.tag_Nnez{display:inline-block;margin:.5rem .5rem 0 1rem}.theme-code-block:hover .copyButtonCopied_obH4{opacity:1!important}.copyButtonIcons_eSgA{height:1.125rem;position:relative;width:1.125rem}.copyButtonIcon_y97N,.copyButtonSuccessIcon_LjdS{fill:currentColor;height:inherit;left:0;opacity:inherit;position:absolute;top:0;transition:all var(--ifm-transition-fast) ease;width:inherit}.copyButtonSuccessIcon_LjdS{color:#00d600;left:50%;opacity:0;top:50%;transform:translate(-50%,-50%) scale(.33)}.copyButtonCopied_obH4 .copyButtonIcon_y97N{opacity:0;transform:scale(.33)}.copyButtonCopied_obH4 .copyButtonSuccessIcon_LjdS{opacity:1;transform:translate(-50%,-50%) scale(1);transition-delay:75ms}.tags_jXut{display:inline}.tag_QGVx{display:inline-block;margin:0 .4rem .5rem 0}.lastUpdated_vwxv{font-size:smaller;font-style:italic;margin-top:.2rem}.tocCollapsibleButton_TO0P{align-items:center;display:flex;font-size:inherit;justify-content:space-between;padding:.4rem .8rem;width:100%}.tocCollapsibleButton_TO0P:after{background:var(--ifm-menu-link-sublist-icon) 50% 50%/2rem 2rem no-repeat;content:"";filter:var(--ifm-menu-link-sublist-icon-filter);height:1.25rem;transform:rotate(180deg);transition:transform var(--ifm-transition-fast);width:1.25rem}.tocCollapsibleButtonExpanded_MG3E:after,.tocCollapsibleExpanded_sAul{transform:none}.tocCollapsible_ETCw{background-color:var(--ifm-menu-color-background-active);border-radius:var(--ifm-global-radius);margin:1rem 0}.tocCollapsibleContent_vkbj>ul{border-left:none;border-top:1px solid var(--ifm-color-emphasis-300);font-size:15px;padding:.2rem 0}.tocCollapsibleContent_vkbj ul li{margin:.4rem .8rem}.tocCollapsibleContent_vkbj a{display:block}.wordWrapButtonIcon_Bwma{height:1.2rem;width:1.2rem}.details_lb9f{--docusaurus-details-summary-arrow-size:0.38rem;--docusaurus-details-transition:transform 200ms ease;--docusaurus-details-decoration-color:grey}.details_lb9f>summary{cursor:pointer;padding-left:1rem;position:relative}.details_lb9f>summary::-webkit-details-marker{display:none}.details_lb9f>summary:before{border-color:#0000 #0000 #0000 var(--docusaurus-details-decoration-color);border-style:solid;border-width:var(--docusaurus-details-summary-arrow-size);content:"";left:0;position:absolute;top:.45rem;transform:rotate(0);transform-origin:calc(var(--docusaurus-details-summary-arrow-size)/2) 50%;transition:var(--docusaurus-details-transition)}.collapsibleContent_i85q{border-top:1px solid var(--docusaurus-details-decoration-color);margin-top:1rem;padding-top:1rem}.details_b_Ee{--docusaurus-details-decoration-color:var(--ifm-alert-border-color);--docusaurus-details-transition:transform var(--ifm-transition-fast) ease;border:1px solid var(--ifm-alert-border-color);margin:0 0 var(--ifm-spacing-vertical)}.img_ev3q{height:auto}.admonition_xJq3{margin-bottom:1em}.admonitionHeading_Gvgb{font:var(--ifm-heading-font-weight) var(--ifm-h5-font-size)/var(--ifm-heading-line-height) var(--ifm-heading-font-family)}.admonitionHeading_Gvgb:not(:last-child){margin-bottom:.3rem}.admonitionHeading_Gvgb code{text-transform:none}.admonitionIcon_Rf37{display:inline-block;margin-right:.4em;vertical-align:middle}.admonitionIcon_Rf37 svg{fill:var(--ifm-alert-foreground-color);display:inline-block;height:1.6em;width:1.6em}.blogPostFooterDetailsFull_mRVl{flex-direction:column}.tableOfContents_bqdL{overflow-y:auto;position:sticky;top:calc(var(--ifm-navbar-height) + 1rem)}.breadcrumbHomeIcon_YNFT{height:1.1rem;position:relative;top:1px;vertical-align:top;width:1.1rem}.breadcrumbsContainer_Z_bl{--ifm-breadcrumb-size-multiplier:0.8;margin-bottom:.8rem}.mdxPageWrapper_j9I6{justify-content:center}@media (min-width:997px){.collapseSidebarButton_PEFL,.expandButton_TmdG{background-color:var(--docusaurus-collapse-button-bg)}:root{--docusaurus-announcement-bar-height:30px}.announcementBarClose_gvF7,.announcementBarPlaceholder_vyr4{flex-basis:50px}.navbarSearchContainer_Bca1{padding:var(--ifm-navbar-item-padding-vertical) var(--ifm-navbar-item-padding-horizontal)}.collapseSidebarButton_PEFL{border:1px solid var(--ifm-toc-border-color);border-radius:0;bottom:0;display:block!important;height:40px;position:sticky}.collapseSidebarButtonIcon_kv0_{margin-top:4px;transform:rotate(180deg)}.expandButtonIcon_i1dp,[dir=rtl] .collapseSidebarButtonIcon_kv0_{transform:rotate(0)}.collapseSidebarButton_PEFL:focus,.collapseSidebarButton_PEFL:hover,.expandButton_TmdG:focus,.expandButton_TmdG:hover{background-color:var(--docusaurus-collapse-button-bg-hover)}.menuHtmlItem_M9Kj{padding:var(--ifm-menu-link-padding-vertical) var(--ifm-menu-link-padding-horizontal)}.menu_SIkG{flex-grow:1;padding:.5rem}@supports (scrollbar-gutter:stable){.menu_SIkG{padding:.5rem 0 .5rem .5rem;scrollbar-gutter:stable}}.menuWithAnnouncementBar_GW3s{margin-bottom:var(--docusaurus-announcement-bar-height)}.sidebar_njMd{display:flex;flex-direction:column;height:100%;padding-top:var(--ifm-navbar-height);width:var(--doc-sidebar-width)}.sidebarWithHideableNavbar_wUlq{padding-top:0}.sidebarHidden_VK0M{opacity:0;visibility:hidden}.sidebarLogo_isFc{align-items:center;color:inherit!important;display:flex!important;margin:0 var(--ifm-navbar-padding-horizontal);max-height:var(--ifm-navbar-height);min-height:var(--ifm-navbar-height);text-decoration:none!important}.sidebarLogo_isFc img{height:2rem;margin-right:.5rem}.expandButton_TmdG{align-items:center;display:flex;height:100%;justify-content:center;position:absolute;right:0;top:0;transition:background-color var(--ifm-transition-fast) ease;width:100%}[dir=rtl] .expandButtonIcon_i1dp{transform:rotate(180deg)}.docSidebarContainer_YfHR{border-right:1px solid var(--ifm-toc-border-color);-webkit-clip-path:inset(0);clip-path:inset(0);display:block;margin-top:calc(var(--ifm-navbar-height)*-1);transition:width var(--ifm-transition-fast) ease;width:var(--doc-sidebar-width);will-change:width}.docSidebarContainerHidden_DPk8{cursor:pointer;width:var(--doc-sidebar-hidden-width)}.sidebarViewport_aRkj{height:100%;max-height:100vh;position:sticky;top:0}.docMainContainer_TBSr{flex-grow:1;max-width:calc(100% - var(--doc-sidebar-width))}.docMainContainerEnhanced_lQrH{max-width:calc(100% - var(--doc-sidebar-hidden-width))}.docItemWrapperEnhanced_JWYK{max-width:calc(var(--ifm-container-width) + var(--doc-sidebar-width))!important}.lastUpdated_vwxv{text-align:right}.tocMobile_ITEo{display:none}.docItemCol_VOVn{max-width:75%!important}}@media (min-width:1440px){.container{max-width:var(--ifm-container-width-xl)}}@media (max-width:996px){.col{--ifm-col-width:100%;flex-basis:var(--ifm-col-width);margin-left:0}.footer{--ifm-footer-padding-horizontal:0}.colorModeToggle_DEke,.footer__link-separator,.navbar__item,.sidebar_re4s,.tableOfContents_bqdL{display:none}.footer__col{margin-bottom:calc(var(--ifm-spacing-vertical)*3)}.footer__link-item{display:block}.hero{padding-left:0;padding-right:0}.navbar>.container,.navbar>.container-fluid{padding:0}.navbar__toggle{display:inherit}.navbar__search-input{width:9rem}.pills--block,.tabs--block{flex-direction:column}.navbarSearchContainer_Bca1{position:absolute;right:var(--ifm-navbar-padding-horizontal)}.docItemContainer_F8PC{padding:0 .3rem}}@media screen and (max-width:996px){.heroBanner_qdFl{padding:2rem}}@media (max-width:576px){.markdown h1:first-child{--ifm-h1-font-size:2rem}.markdown>h2{--ifm-h2-font-size:1.5rem}.markdown>h3{--ifm-h3-font-size:1.25rem}.title_f1Hy{font-size:2rem}}@media (hover:hover){.backToTopButton_sjWU:hover{background-color:var(--ifm-color-emphasis-300)}}@media (pointer:fine){.thin-scrollbar{scrollbar-width:thin}.thin-scrollbar::-webkit-scrollbar{height:var(--ifm-scrollbar-size);width:var(--ifm-scrollbar-size)}.thin-scrollbar::-webkit-scrollbar-track{background:var(--ifm-scrollbar-track-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb{background:var(--ifm-scrollbar-thumb-background-color);border-radius:10px}.thin-scrollbar::-webkit-scrollbar-thumb:hover{background:var(--ifm-scrollbar-thumb-hover-background-color)}}@media (prefers-reduced-motion:reduce){:root{--ifm-transition-fast:0ms;--ifm-transition-slow:0ms}}@media print{.announcementBar_mb4j,.footer,.menu,.navbar,.pagination-nav,.table-of-contents,.tocMobile_ITEo{display:none}.tabs{page-break-inside:avoid}.codeBlockLines_e6Vv{white-space:pre-wrap}} \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/configure_ldap_web_interface-b413925576da0ff77673ac6fe0b48c61.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/configure_ldap_web_interface-b413925576da0ff77673ac6fe0b48c61.png new file mode 100644 index 00000000..86eb3f24 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/configure_ldap_web_interface-b413925576da0ff77673ac6fe0b48c61.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/configure_message_repository_settings_web_ui-144f7126107df0c25370e715c7a28b09.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/configure_message_repository_settings_web_ui-144f7126107df0c25370e715c7a28b09.png new file mode 100644 index 00000000..678b51d2 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/configure_message_repository_settings_web_ui-144f7126107df0c25370e715c7a28b09.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/database_connection_address-e7a41b233e0f5b778432cb2d2b24f020.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/database_connection_address-e7a41b233e0f5b778432cb2d2b24f020.png new file mode 100644 index 00000000..0fdbb54f Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/database_connection_address-e7a41b233e0f5b778432cb2d2b24f020.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/docusaurus-plushie-banner-a60f7593abca1e3eef26a9afa244e4fb.jpeg b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/docusaurus-plushie-banner-a60f7593abca1e3eef26a9afa244e4fb.jpeg new file mode 100644 index 00000000..11bda092 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/docusaurus-plushie-banner-a60f7593abca1e3eef26a9afa244e4fb.jpeg differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/dog-818fa64239695f20feb4fd68b4396bd6.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/dog-818fa64239695f20feb4fd68b4396bd6.png new file mode 100644 index 00000000..16176b6d Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/dog-818fa64239695f20feb4fd68b4396bd6.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_1-dbe72a427d923b6018ff4a4c7dfd5313.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_1-dbe72a427d923b6018ff4a4c7dfd5313.png new file mode 100644 index 00000000..1b7fce93 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_1-dbe72a427d923b6018ff4a4c7dfd5313.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_10-25b8d73c33c3f42065f26e4cf9ca0de4.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_10-25b8d73c33c3f42065f26e4cf9ca0de4.png new file mode 100644 index 00000000..d5596356 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_10-25b8d73c33c3f42065f26e4cf9ca0de4.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_2-6f47952affff8c0295409c5c4035b9e5.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_2-6f47952affff8c0295409c5c4035b9e5.png new file mode 100644 index 00000000..d55ee932 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_2-6f47952affff8c0295409c5c4035b9e5.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_3-f3c41c0b4d19555b0c4a046c4fda8d5b.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_3-f3c41c0b4d19555b0c4a046c4fda8d5b.png new file mode 100644 index 00000000..4e513f70 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_3-f3c41c0b4d19555b0c4a046c4fda8d5b.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_4-ac014207b1ba39bcf9cca69d8da9bd8f.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_4-ac014207b1ba39bcf9cca69d8da9bd8f.png new file mode 100644 index 00000000..6842637c Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_4-ac014207b1ba39bcf9cca69d8da9bd8f.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_5-820faa1841be40dc6c1d775cfa977671.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_5-820faa1841be40dc6c1d775cfa977671.png new file mode 100644 index 00000000..636070ee Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_5-820faa1841be40dc6c1d775cfa977671.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_6-75c9a98399cadc644a00467b1b7ee85d.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_6-75c9a98399cadc644a00467b1b7ee85d.png new file mode 100644 index 00000000..e36e914c Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_6-75c9a98399cadc644a00467b1b7ee85d.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_7-5ffb2096420eeae91eed62c1079a5e85.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_7-5ffb2096420eeae91eed62c1079a5e85.png new file mode 100644 index 00000000..b94860b1 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_7-5ffb2096420eeae91eed62c1079a5e85.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_8-cdef0c0440722b6bfb3b193633393887.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_8-cdef0c0440722b6bfb3b193633393887.png new file mode 100644 index 00000000..1b0c7316 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_8-cdef0c0440722b6bfb3b193633393887.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_9-e93e587a011894fdea4f18d0dad2c949.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_9-e93e587a011894fdea4f18d0dad2c949.png new file mode 100644 index 00000000..2a15e5d7 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/fed_9-e93e587a011894fdea4f18d0dad2c949.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/metrics_1-a6440cb2dd502e1d29149a070af422eb.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/metrics_1-a6440cb2dd502e1d29149a070af422eb.png new file mode 100644 index 00000000..f2700f40 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/metrics_1-a6440cb2dd502e1d29149a070af422eb.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/secure_connection_failed-56cfeb544f1c53afe1d119d408897cd4.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/secure_connection_failed-56cfeb544f1c53afe1d119d408897cd4.png new file mode 100644 index 00000000..8265aa20 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/secure_connection_failed-56cfeb544f1c53afe1d119d408897cd4.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/security_config_1-9e672c027ac08f2876117933ce923339.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/security_config_1-9e672c027ac08f2876117933ce923339.png new file mode 100644 index 00000000..0f49662b Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/security_config_1-9e672c027ac08f2876117933ce923339.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/setup_wizard_1-c0dd1888b20120e2842095d73f16d59a.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/setup_wizard_1-c0dd1888b20120e2842095d73f16d59a.png new file mode 100644 index 00000000..0424f921 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/setup_wizard_1-c0dd1888b20120e2842095d73f16d59a.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/setup_wizard_2-84157209442d76a4b290f9e78eae9136.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/setup_wizard_2-84157209442d76a4b290f9e78eae9136.png new file mode 100644 index 00000000..f69ad82b Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/setup_wizard_2-84157209442d76a4b290f9e78eae9136.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_1-ea2f4176c744d741688e08935a7e6e11.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_1-ea2f4176c744d741688e08935a7e6e11.png new file mode 100644 index 00000000..c026b9ec Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_1-ea2f4176c744d741688e08935a7e6e11.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_2-1a19fe7c484d18a00a6a3c624b93a977.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_2-1a19fe7c484d18a00a6a3c624b93a977.png new file mode 100644 index 00000000..75599c79 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_2-1a19fe7c484d18a00a6a3c624b93a977.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_3-eef404a96c26aa9915852afe7dc806d6.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_3-eef404a96c26aa9915852afe7dc806d6.png new file mode 100644 index 00000000..08dbb741 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_3-eef404a96c26aa9915852afe7dc806d6.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_4-1a7f3ef3849fdb9249390b5c29d9aac2.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_4-1a7f3ef3849fdb9249390b5c29d9aac2.png new file mode 100644 index 00000000..86bcd95c Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_4-1a7f3ef3849fdb9249390b5c29d9aac2.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_5-0fa98c7b0fc71e17b2a7b310d6fd6c88.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_5-0fa98c7b0fc71e17b2a7b310d6fd6c88.png new file mode 100644 index 00000000..8bc875b0 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_5-0fa98c7b0fc71e17b2a7b310d6fd6c88.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_6-109e5483782703d59efa3fde5d496fe8.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_6-109e5483782703d59efa3fde5d496fe8.png new file mode 100644 index 00000000..a2ccef11 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/user_manage_6-109e5483782703d59efa3fde5d496fe8.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_1-75153dbba7aa3d4932b48ec67c867c12.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_1-75153dbba7aa3d4932b48ec67c867c12.png new file mode 100644 index 00000000..39a61459 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_1-75153dbba7aa3d4932b48ec67c867c12.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_2-82af839caa956a42c2b26eef01cd0a3e.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_2-82af839caa956a42c2b26eef01cd0a3e.png new file mode 100644 index 00000000..e078859d Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_2-82af839caa956a42c2b26eef01cd0a3e.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_3-038e6792eb748581579c02a4f202dfa9.png b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_3-038e6792eb748581579c02a4f202dfa9.png new file mode 100644 index 00000000..7ab97155 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/assets/images/vbm_admin_3-038e6792eb748581579c02a4f202dfa9.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/00483006.6a921100.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/00483006.6a921100.js new file mode 100644 index 00000000..ffd4a892 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/00483006.6a921100.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6074],{8114:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>l,frontMatter:()=>i,metadata:()=>a,toc:()=>u});var n=o(5893),r=o(1151);const i={},s="Use Setup Wizard to Configure TAK Server",a={id:"installation/setup_wizard",title:"Use Setup Wizard to Configure TAK Server",description:"The TAK Server configuration wizard will help you set up common configuration options once you have installed and started TAK Server. The wizard will guide you through the setup process for a secure configuration, using the default ports that ATAK and WinTAK will connect to.",source:"@site/docs/installation/setup_wizard.md",sourceDirName:"installation",slug:"/installation/setup_wizard",permalink:"/docs/installation/setup_wizard",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/setup_wizard.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Configuration",permalink:"/docs/installation/twoserver/servertwo/configuretakserver"},next:{title:"Overview",permalink:"/docs/upgrade/overview"}},c={},u=[];function d(e){const t={a:"a",em:"em",h1:"h1",img:"img",p:"p",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"use-setup-wizard-to-configure-tak-server",children:"Use Setup Wizard to Configure TAK Server"}),"\n",(0,n.jsx)(t.p,{children:"The TAK Server configuration wizard will help you set up common configuration options once you have installed and started TAK Server. The wizard will guide you through the setup process for a secure configuration, using the default ports that ATAK and WinTAK will connect to."}),"\n",(0,n.jsx)(t.p,{children:"Once you have created your adminstrative login credentials as in the previous section, go to:"}),"\n",(0,n.jsxs)(t.p,{children:[(0,n.jsx)(t.a,{href:"https://localhost:8443/setup/",children:"https://localhost:8443/setup/"})," (Recommended. Uses the more secure client certificate)"]}),"\n",(0,n.jsx)(t.p,{children:"Then follow the prompts to begin configuring. The wizard will first walk you through recommended security configuration:"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Security Configuration",src:o(5300).Z+"",width:"2022",height:"1644"})}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.em,{children:"NOTE: Insecure ports are a potential security risk and may allow attackers to gain access to the system resulting in the disclosure of personal and sensitive information. Use of unencrypted ports should be avoided to ensure a secure TAK Server deployment."})}),"\n",(0,n.jsx)(t.p,{children:"Followed by the recommended federation configuration, if you wish to set up your TAK Server to support federation. (For more information on federation, go to section 8):"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Federation Configuration",src:o(6753).Z+"",width:"1171",height:"928"})})]})}function l(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},5300:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/setup_wizard_1-c0dd1888b20120e2842095d73f16d59a.png"},6753:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/setup_wizard_2-84157209442d76a4b290f9e78eae9136.png"},1151:(e,t,o)=>{o.d(t,{Z:()=>a,a:()=>s});var n=o(7294);const r={},i=n.createContext(r);function s(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/01a85c17.c5cc029e.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/01a85c17.c5cc029e.js new file mode 100644 index 00000000..fce9bfcf --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/01a85c17.c5cc029e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[4013],{1460:(e,s,t)=>{t.d(s,{Z:()=>v});var a=t(7294),i=t(512),r=t(6040),l=t(7524),n=t(3692),c=t(5999),o=t(6550),m=t(8596);function d(e){const{pathname:s}=(0,o.TH)();return(0,a.useMemo)((()=>e.filter((e=>function(e,s){return!(e.unlisted&&!(0,m.Mg)(e.permalink,s))}(e,s)))),[e,s])}const u={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var g=t(5893);function h(e){let{sidebar:s}=e;const t=d(s.items);return(0,g.jsx)("aside",{className:"col col--3",children:(0,g.jsxs)("nav",{className:(0,i.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,c.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,g.jsx)("div",{className:(0,i.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:s.title}),(0,g.jsx)("ul",{className:(0,i.Z)(u.sidebarItemList,"clean-list"),children:t.map((e=>(0,g.jsx)("li",{className:u.sidebarItem,children:(0,g.jsx)(n.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var b=t(3102);function p(e){let{sidebar:s}=e;const t=d(s.items);return(0,g.jsx)("ul",{className:"menu__list",children:t.map((e=>(0,g.jsx)("li",{className:"menu__list-item",children:(0,g.jsx)(n.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function j(e){return(0,g.jsx)(b.Zo,{component:p,props:e})}function x(e){let{sidebar:s}=e;const t=(0,l.i)();return s?.items.length?"mobile"===t?(0,g.jsx)(j,{sidebar:s}):(0,g.jsx)(h,{sidebar:s}):null}function v(e){const{sidebar:s,toc:t,children:a,...l}=e,n=s&&s.items.length>0;return(0,g.jsx)(r.Z,{...l,children:(0,g.jsx)("div",{className:"container margin-vert--lg",children:(0,g.jsxs)("div",{className:"row",children:[(0,g.jsx)(x,{sidebar:s}),(0,g.jsx)("main",{className:(0,i.Z)("col",{"col--7":n,"col--9 col--offset-1":!n}),itemScope:!0,itemType:"https://schema.org/Blog",children:a}),t&&(0,g.jsx)("div",{className:"col col--2",children:t})]})})})}},1223:(e,s,t)=>{t.r(s),t.d(s,{default:()=>p});t(7294);var a=t(512),i=t(5999);const r=()=>(0,i.I)({id:"theme.tags.tagsPageTitle",message:"Tags",description:"The title of the tag list page"});var l=t(1944),n=t(5281),c=t(1460),o=t(3008),m=t(2503);const d={tag:"tag_Nnez"};var u=t(5893);function g(e){let{letterEntry:s}=e;return(0,u.jsxs)("article",{children:[(0,u.jsx)(m.Z,{as:"h2",id:s.letter,children:s.letter}),(0,u.jsx)("ul",{className:"padding--none",children:s.tags.map((e=>(0,u.jsx)("li",{className:d.tag,children:(0,u.jsx)(o.Z,{...e})},e.permalink)))}),(0,u.jsx)("hr",{})]})}function h(e){let{tags:s}=e;const t=function(e){const s={};return Object.values(e).forEach((e=>{const t=function(e){return e[0].toUpperCase()}(e.label);s[t]??=[],s[t].push(e)})),Object.entries(s).sort(((e,s)=>{let[t]=e,[a]=s;return t.localeCompare(a)})).map((e=>{let[s,t]=e;return{letter:s,tags:t.sort(((e,s)=>e.label.localeCompare(s.label)))}}))}(s);return(0,u.jsx)("section",{className:"margin-vert--lg",children:t.map((e=>(0,u.jsx)(g,{letterEntry:e},e.letter)))})}var b=t(197);function p(e){let{tags:s,sidebar:t}=e;const i=r();return(0,u.jsxs)(l.FG,{className:(0,a.Z)(n.k.wrapper.blogPages,n.k.page.blogTagsListPage),children:[(0,u.jsx)(l.d,{title:i}),(0,u.jsx)(b.Z,{tag:"blog_tags_list"}),(0,u.jsxs)(c.Z,{sidebar:t,children:[(0,u.jsx)(m.Z,{as:"h1",children:i}),(0,u.jsx)(h,{tags:s})]})]})}},3008:(e,s,t)=>{t.d(s,{Z:()=>n});t(7294);var a=t(512),i=t(3692);const r={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=t(5893);function n(e){let{permalink:s,label:t,count:n}=e;return(0,l.jsxs)(i.Z,{href:s,className:(0,a.Z)(r.tag,n?r.tagWithCount:r.tagRegular),children:[t,n&&(0,l.jsx)("span",{children:n})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/031793e1.4541628a.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/031793e1.4541628a.js new file mode 100644 index 00000000..ac348aa5 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/031793e1.4541628a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1633],{2511:s=>{s.exports=JSON.parse('{"label":"facebook","permalink":"/blog/tags/facebook","allTagsPath":"/blog/tags","count":1,"unlisted":false}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/062c1b27.b08cd01f.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/062c1b27.b08cd01f.js new file mode 100644 index 00000000..7cbf1d3d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/062c1b27.b08cd01f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8199],{6631:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>c,contentTitle:()=>r,default:()=>h,frontMatter:()=>s,metadata:()=>a,toc:()=>u});var n=i(5893),o=i(1151);const s={},r="Group Filtering",a={id:"configuration/groupfiltering",title:"Group Filtering",description:"TAK Server has the ability to segment users so they only see a subset of the other users on the system. This is achieved by assigning groups to individual connections. If ATAK-A shares common group membership in at least one group with ATAK-B, they share data with each other. If not otherwise specified, all connections default to being in the special \u201cANON\u201d group (note 2 underscores as prefix and postfix). There are three ways to assign groups to a connection:",source:"@site/docs/configuration/groupfiltering.md",sourceDirName:"configuration",slug:"/configuration/groupfiltering",permalink:"/docs/configuration/groupfiltering",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/groupfiltering.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Configuring Security Through Web UI (Certificates/TLS)",permalink:"/docs/configuration/configurewebui"},next:{title:"Group Assignment by Input",permalink:"/docs/configuration/groupassignmentbyinput"}},c={},u=[];function l(e){const t={h1:"h1",li:"li",p:"p",strong:"strong",ul:"ul",...(0,o.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"group-filtering",children:"Group Filtering"}),"\n",(0,n.jsxs)(t.p,{children:["TAK Server has the ability to segment users so they only see a subset of the other users on the system. This is achieved by assigning groups to individual connections. If ATAK-A shares common group membership in at least one group with ATAK-B, they share data with each other. If not otherwise specified, all connections default to being in the special \u201c",(0,n.jsx)(t.strong,{children:"ANON"}),"\u201d group (note 2 underscores as prefix and postfix). There are three ways to assign groups to a connection:"]}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"Assigning elements to : this is simple, but provides no access control if you have multiple ports configured on the same server."}),"\n",(0,n.jsx)(t.li,{children:"Active Directory / LDAP / Flat file with additional authentication message"}),"\n",(0,n.jsx)(t.li,{children:"Active Directory / LDAP / Flat file without additional authentication messages (uses certificate-based identification)"}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Details on the three options:"})]})}function h(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(l,{...e})}):l(e)}},1151:(e,t,i)=>{i.d(t,{Z:()=>a,a:()=>r});var n=i(7294);const o={},s=n.createContext(o);function r(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:r(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/08c34d24.281f4211.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/08c34d24.281f4211.js new file mode 100644 index 00000000..54a95086 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/08c34d24.281f4211.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7357],{395:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>c,contentTitle:()=>r,default:()=>d,frontMatter:()=>s,metadata:()=>a,toc:()=>g});var i=n(5893),o=n(1151);const s={},r="Configuring Messaging and Repository Settings through Web UI",a={id:"configuration/configuremessagingrepositorywebui",title:"Configuring Messaging and Repository Settings through Web UI",description:"Messaging Configuration Web Interface",source:"@site/docs/configuration/configuremessagingrepositorywebui.md",sourceDirName:"configuration",slug:"/configuration/configuremessagingrepositorywebui",permalink:"/docs/configuration/configuremessagingrepositorywebui",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/configuremessagingrepositorywebui.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Authentication Backends",permalink:"/docs/configuration/authenticationbackends"},next:{title:"Optionally Disabling UI and WebTAK on HTTPS Ports",permalink:"/docs/configuration/optionallydisableui"}},c={},g=[];function u(t){const e={h1:"h1",img:"img",p:"p",...(0,o.a)(),...t.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(e.h1,{id:"configuring-messaging-and-repository-settings-through-web-ui",children:"Configuring Messaging and Repository Settings through Web UI"}),"\n",(0,i.jsx)(e.p,{children:(0,i.jsx)(e.img,{alt:"Messaging Configuration Web Interface",src:n(3081).Z+"",width:"478",height:"256"})}),"\n",(0,i.jsx)(e.p,{children:'Messaging/Repository settings configuration can be done through the input definitions page. To get there go to Configuration > Input Definitions in the menu bar. This page displays the current input definitions at the top and at the bottom the current configuration of Messaging and Repository settings are displayed. To edit these setting click "Edit Configuration". Note: Changes made here will only take effect after a server restart.'})]})}function d(t={}){const{wrapper:e}={...(0,o.a)(),...t.components};return e?(0,i.jsx)(e,{...t,children:(0,i.jsx)(u,{...t})}):u(t)}},3081:(t,e,n)=>{n.d(e,{Z:()=>i});const i=n.p+"assets/images/configure_message_repository_settings_web_ui-144f7126107df0c25370e715c7a28b09.png"},1151:(t,e,n)=>{n.d(e,{Z:()=>a,a:()=>r});var i=n(7294);const o={},s=i.createContext(o);function r(t){const e=i.useContext(s);return i.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function a(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:r(t.components),i.createElement(s.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/09573330.2192c522.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/09573330.2192c522.js new file mode 100644 index 00000000..1ce14a59 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/09573330.2192c522.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[5017],{8854:(e,n,s)=>{s.r(n),s.d(n,{assets:()=>c,contentTitle:()=>i,default:()=>l,frontMatter:()=>o,metadata:()=>a,toc:()=>u});var t=s(5893),r=s(1151);const o={},i="AWS / GovCloud Recommended Instance Type",a={id:"system-requirements/awsrequirements",title:"AWS / GovCloud Recommended Instance Type",description:"- c5.xlarge",source:"@site/docs/system-requirements/awsrequirements.md",sourceDirName:"system-requirements",slug:"/system-requirements/awsrequirements",permalink:"/docs/system-requirements/awsrequirements",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/system-requirements/awsrequirements.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Server Requirements",permalink:"/docs/system-requirements/serverrequirements"},next:{title:"Overview and Installer Files",permalink:"/docs/installation/overview"}},c={},u=[];function d(e){const n={a:"a",h1:"h1",li:"li",p:"p",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"aws--govcloud-recommended-instance-type",children:"AWS / GovCloud Recommended Instance Type"}),"\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsxs)(n.li,{children:["c5.xlarge","\n",(0,t.jsxs)(n.ul,{children:["\n",(0,t.jsx)(n.li,{children:"4 vCPU"}),"\n",(0,t.jsx)(n.li,{children:"8 GB RAM"}),"\n",(0,t.jsx)(n.li,{children:"Up to 10 Gbps network bandwidth"}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(n.li,{children:"For 2-server installation, use this instance type for both servers."}),"\n"]}),"\n",(0,t.jsx)(n.p,{children:"TAK Server is a TLS-enabled networking server. In order to ensure consistent performance, burstable AWS EC2 instance types such as T2 are not recommended. TLS and TCP processing requires consistent, continuous CPU performance. C4 and C5 instances are designed for predictable CPU performance, and are better-suited for TAK Server deployments."}),"\n",(0,t.jsxs)(n.p,{children:["More information about instance types may be found here:\n",(0,t.jsx)(n.a,{href:"https://aws.amazon.com/ec2/instance-types",children:"https://aws.amazon.com/ec2/instance-types"})]}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.strong,{children:"Usage of larger instance types or physical servers is supported for scalability, to support more concurrent active users."})})]})}function l(e={}){const{wrapper:n}={...(0,r.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},1151:(e,n,s)=>{s.d(n,{Z:()=>a,a:()=>i});var t=s(7294);const r={},o=t.createContext(r);function i(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/096bfee4.574352e1.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/096bfee4.574352e1.js new file mode 100644 index 00000000..b50692ef --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/096bfee4.574352e1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7178],{5010:s=>{s.exports=JSON.parse('{"permalink":"/blog/tags/facebook","page":1,"postsPerPage":10,"totalPages":1,"totalCount":1,"blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0a229eeb.6408989c.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0a229eeb.6408989c.js new file mode 100644 index 00000000..ea355d2e --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0a229eeb.6408989c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1925],{1563:(e,s,a)=>{a.r(s),a.d(s,{assets:()=>d,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>l,toc:()=>i});var r=a(5893),n=a(1151);const t={},o="Two-Server Upgrade",l={id:"upgrade/twoserver",title:"Two-Server Upgrade",description:"Rocky Linux 8",source:"@site/docs/upgrade/twoserver.md",sourceDirName:"upgrade",slug:"/upgrade/twoserver",permalink:"/docs/upgrade/twoserver",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/upgrade/twoserver.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Single-Server Upgrade",permalink:"/docs/upgrade/singleserver"},next:{title:"IronBank",permalink:"/docs/dockerinstall/ironbank"}},d={},i=[{value:"Rocky Linux 8",id:"rocky-linux-8",level:2},{value:"RHEL 8",id:"rhel-8",level:2},{value:"RHEL 7",id:"rhel-7",level:2},{value:"Ubuntu and Raspberry Pi OS",id:"ubuntu-and-raspberry-pi-os",level:2},{value:"Centos 7",id:"centos-7",level:2}];function c(e){const s={code:"code",em:"em",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,n.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.h1,{id:"two-server-upgrade",children:"Two-Server Upgrade"}),"\n",(0,r.jsx)(s.h2,{id:"rocky-linux-8",children:"Rocky Linux 8"}),"\n",(0,r.jsx)(s.p,{children:"Upgrade the two TAK Server packages on the servers on which they are installed."}),"\n",(0,r.jsx)(s.p,{children:"First, on the core server, install Java 17 and upgrade the core package:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install java-17-openjdk-devel -y\nsudo dnf install takserver-core-5.1-RELEASEx.noarch.rpm -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"Next, on the database server, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Install Java 17. Enable the 'powertools' repo for postgis dependencies. Install TAK Server RPM database and its dependencies."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install epel-release -y\nsudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo dnf update -y\nsudo dnf module disable postgresql\nsudo dnf install java-17-openjdk-devel -y\nsudo dnf config-manager --set-enabled powertools\n"})}),"\n",(0,r.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version in both servers"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"rhel-8",children:"RHEL 8"}),"\n",(0,r.jsx)(s.p,{children:"Upgrade the two TAK Server packages on the servers on which they are installed."}),"\n",(0,r.jsx)(s.p,{children:"First, on the core server, install Java 17 and upgrade the core package:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y\nsudo dnf install takserver-core-5.1-RELEASEx.noarch.rpm -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"Next, on the database server, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install Java 17. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y\nsudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo dnf update -y && sudo dnf install java-17-openjdk-devel -y\nsudo dnf module disable postgresql\nsudo subscription-manager config --rhsm.manage_repos=1\nsudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Note: If you get the error \u2018This system has no repositories available through subscriptions\u2019, you need to subscribe your system with:"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo subscription-manager register --username --password --auto-attach\n"})}),"\n",(0,r.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version in both servers"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"rhel-7",children:"RHEL 7"}),"\n",(0,r.jsx)(s.p,{children:"Install OpenJDK 17 and other dependencies (if you have not previously done so.)"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\n"})}),"\n",(0,r.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.\nUpgrade the two TAK Server packages on the servers on which they are installed."}),"\n",(0,r.jsx)(s.p,{children:"First, upgrade the core package:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo rpm -Uvh takserver-core-5.1-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"Next, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install TAK Server RPM database and its dependencies."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum update -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo rpm -Uvh takserver-database-5.1-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version in both servers"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"ubuntu-and-raspberry-pi-os",children:"Ubuntu and Raspberry Pi OS"}),"\n",(0,r.jsx)(s.p,{children:"Upgrade the two TAK Server packages on the servers on which they are installed."}),"\n",(0,r.jsx)(s.p,{children:"First, upgrade the core package."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo apt install ./takserver-core_5.1-RELEASE-x_all.deb\n"})}),"\n",(0,r.jsx)(s.p,{children:"Next, upgrade the database."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo apt install ./takserver-database_5.1-RELEASE-x_all.deb\n"})}),"\n",(0,r.jsx)(s.p,{children:"This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version in both servers"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"centos-7",children:"Centos 7"}),"\n",(0,r.jsx)(s.p,{children:"Install OpenJDK 17 and other dependencies (if you have not previously done so.)"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\n"})}),"\n",(0,r.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,r.jsx)(s.p,{children:"Upgrade the two TAK Server packages on the servers on which they are installed."}),"\n",(0,r.jsx)(s.p,{children:"First, upgrade the core package:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo rpm -Uvh takserver-core-5.1-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"Next, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install TAK Server RPM database and its dependencies."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install epel-release -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum update -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version in both servers"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})})]})}function h(e={}){const{wrapper:s}={...(0,n.a)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},1151:(e,s,a)=>{a.d(s,{Z:()=>l,a:()=>o});var r=a(7294);const n={},t=r.createContext(n);function o(e){const s=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:o(e.components),r.createElement(t.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0a2c82fb.30dfbad5.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0a2c82fb.30dfbad5.js new file mode 100644 index 00000000..f604c82d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0a2c82fb.30dfbad5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[54],{8871:(t,a,e)=>{e.r(a),e.d(a,{assets:()=>r,contentTitle:()=>u,default:()=>m,frontMatter:()=>c,metadata:()=>i,toc:()=>n});var s=e(5893),o=e(1151);const c={tags:["hello","docusaurus-static"],authors:{name:"OctoSpacc",title:"Chief Executive Officer @ Spacc Inc.",url:"https://hub.octt.eu.org",image_url:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}},u="Welcome to Docusaurus-Static!",i={permalink:"/blog/2024/01/28/welcome-to-docusaurus-static",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2024-01-28-welcome-to-docusaurus-static.md",source:"@site/blog/2024-01-28-welcome-to-docusaurus-static.md",title:"Welcome to Docusaurus-Static!",description:"Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.",date:"2024-01-28T00:00:00.000Z",formattedDate:"January 28, 2024",tags:[{label:"hello",permalink:"/blog/tags/hello"},{label:"docusaurus-static",permalink:"/blog/tags/docusaurus-static"}],readingTime:.38,hasTruncateMarker:!1,authors:[{name:"OctoSpacc",title:"Chief Executive Officer @ Spacc Inc.",url:"https://hub.octt.eu.org",image_url:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png",imageURL:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}],frontMatter:{tags:["hello","docusaurus-static"],authors:{name:"OctoSpacc",title:"Chief Executive Officer @ Spacc Inc.",url:"https://hub.octt.eu.org",image_url:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png",imageURL:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}},unlisted:!1,nextItem:{title:"Welcome to Docusaurus!",permalink:"/blog/welcome"}},r={authorsImageUrls:[void 0]},n=[];function l(t){const a={code:"code",p:"p",...(0,o.a)(),...t.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(a.p,{children:"Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server."}),"\n",(0,s.jsxs)(a.p,{children:["Not only can the built site be navigated as a collection of static HTML pages from ",(0,s.jsx)(a.code,{children:"file:///"})," locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening ",(0,s.jsx)("a",{href:"/docusaurus-static-single-file.html",children:"docusaurus-static-single-file.html"}),"..."]})]})}function m(t={}){const{wrapper:a}={...(0,o.a)(),...t.components};return a?(0,s.jsx)(a,{...t,children:(0,s.jsx)(l,{...t})}):l(t)}},1151:(t,a,e)=>{e.d(a,{Z:()=>i,a:()=>u});var s=e(7294);const o={},c=s.createContext(o);function u(t){const a=s.useContext(c);return s.useMemo((function(){return"function"==typeof t?t(a):{...a,...t}}),[a,t])}function i(t){let a;return a=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:u(t.components),s.createElement(c.Provider,{value:a},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0f425520.db72f9d7.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0f425520.db72f9d7.js new file mode 100644 index 00000000..fb7a0db5 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/0f425520.db72f9d7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7240],{3642:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>d,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var o=n(5893),i=n(1151);const r={},s="Overview",a={id:"configuration/overview",title:"Overview",description:"Configuration is primarily done through the web interface. Changes made in the interface will be reflected in the /opt/tak/CoreConfig.xml file. If that file does not exist (e.g. on a fresh install), then when TAK Server starts up it will copy /opt/tak/CoreConfig.example.xml. The example has many commented out options. Notable configuration options:",source:"@site/docs/configuration/overview.md",sourceDirName:"configuration",slug:"/configuration/overview",permalink:"/docs/configuration/overview",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/overview.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Software Installation Location",permalink:"/docs/softwareinstallationlocation"},next:{title:"Configuring Security Through Web UI (Certificates/TLS)",permalink:"/docs/configuration/configurewebui"}},c={},l=[];function u(e){const t={h1:"h1",li:"li",p:"p",ul:"ul",...(0,i.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"overview",children:"Overview"}),"\n",(0,o.jsx)(t.p,{children:"Configuration is primarily done through the web interface. Changes made in the interface will be reflected in the /opt/tak/CoreConfig.xml file. If that file does not exist (e.g. on a fresh install), then when TAK Server starts up it will copy /opt/tak/CoreConfig.example.xml. The example has many commented out options. Notable configuration options:"}),"\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsxs)(t.li,{children:["inputs: In the section there are a series of elements. These define ports the server will listen on. Protocol options are as follows:","\n",(0,o.jsxs)(t.ul,{children:["\n",(0,o.jsx)(t.li,{children:"udp: standard CoT udp protocol; unencrypted"}),"\n",(0,o.jsx)(t.li,{children:"mcast: like udp, but has additional configuration option for multicast group"}),"\n",(0,o.jsx)(t.li,{children:"tcp: publish-only port; standard CoT tcp protocol; unencrypted"}),"\n",(0,o.jsx)(t.li,{children:"stcp: streaming/bi-directional; this is for ATAK to connect to. Unencrypted, for testing only"}),"\n",(0,o.jsx)(t.li,{children:"tls: TCP+TLS streaming/bi-directional for encrypted communication with TAK clients"}),"\n"]}),"\n"]}),"\n",(0,o.jsx)(t.li,{children:" : you can use either a flat file or an LDAP backend for group filtering support"}),"\n",(0,o.jsx)(t.li,{children:": here you specify the keystore files to use for the secure port(s)"}),"\n"]})]})}function d(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(u,{...e})}):u(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>s});var o=n(7294);const i={},r=o.createContext(i);function s(e){const t=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),o.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/17495a52.b51d3ff0.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/17495a52.b51d3ff0.js new file mode 100644 index 00000000..59a9f10e --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/17495a52.b51d3ff0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[4620],{6535:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>s,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>c});var o=n(5893),r=n(1151);const i={},s="Overview",a={id:"federation/overview",title:"Overview",description:"Federation will allow subsets of ATAK users who are connected to different servers to work together, even though each TAK server instance (hereafter refered to as 'federates') may be run by independent organizations / administrative domains. It brings some of the following benefits/restrictions:",source:"@site/docs/federation/overview.md",sourceDirName:"federation",slug:"/federation/overview",permalink:"/docs/federation/overview",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/overview.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Device Profiles",permalink:"/docs/deviceprofiles"},next:{title:"Enable Federation",permalink:"/docs/federation/enablefederation"}},d={},c=[];function l(e){const t={h1:"h1",li:"li",ol:"ol",p:"p",...(0,r.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"overview",children:"Overview"}),"\n",(0,o.jsx)(t.p,{children:"Federation will allow subsets of ATAK users who are connected to different servers to work together, even though each TAK server instance (hereafter refered to as 'federates') may be run by independent organizations / administrative domains. It brings some of the following benefits/restrictions:"}),"\n",(0,o.jsxs)(t.ol,{children:["\n",(0,o.jsx)(t.li,{children:"Each administrative domain does not need to share anything about their internal structure (e.g. LDAP/Active Directory information / users) with the other administrative domain."}),"\n",(0,o.jsx)(t.li,{children:"Each administrative domain has control over what data they share with the other domain, but has no control over what the other administrative domain does with data that is shared."}),"\n",(0,o.jsx)(t.li,{children:"It requires no reconfiguration of ATAKs connected to either TAK Server, and the mechanism for connecting the TAK Servers does not allow direct connections of ATAK devices from the other administrative domain."}),"\n"]})]})}function h(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>s});var o=n(7294);const r={},i=o.createContext(r);function s(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1772.3883f26e.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1772.3883f26e.js new file mode 100644 index 00000000..008c9f7b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1772.3883f26e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1772],{5658:(e,t,s)=>{s.d(t,{Z:()=>r});s(7294);var i=s(512),n=s(5999),o=s(2503),a=s(5893);function r(e){let{className:t}=e;return(0,a.jsx)("main",{className:(0,i.Z)("container margin-vert--xl",t),children:(0,a.jsx)("div",{className:"row",children:(0,a.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,a.jsx)(o.Z,{as:"h1",className:"hero__title",children:(0,a.jsx)(n.Z,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,a.jsx)("p",{children:(0,a.jsx)(n.Z,{id:"theme.NotFound.p1",description:"The first paragraph of the 404 page",children:"We could not find what you were looking for."})}),(0,a.jsx)("p",{children:(0,a.jsx)(n.Z,{id:"theme.NotFound.p2",description:"The 2nd paragraph of the 404 page",children:"Please contact the owner of the site that linked you to the original URL and let them know their link is broken."})})]})})})}},1772:(e,t,s)=>{s.r(t),s.d(t,{default:()=>d});s(7294);var i=s(5999),n=s(1944),o=s(6040),a=s(5658),r=s(5893);function d(){const e=(0,i.I)({id:"theme.NotFound.title",message:"Page Not Found"});return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.d,{title:e}),(0,r.jsx)(o.Z,{children:(0,r.jsx)(a.Z,{})})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/17896441.a1d40f2d.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/17896441.a1d40f2d.js new file mode 100644 index 00000000..26ef1273 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/17896441.a1d40f2d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7918],{8945:(e,t,n)=>{n.r(t),n.d(t,{default:()=>de});var s=n(7294),a=n(1944),i=n(902),l=n(5893);const o=s.createContext(null);function r(e){let{children:t,content:n}=e;const a=function(e){return(0,s.useMemo)((()=>({metadata:e.metadata,frontMatter:e.frontMatter,assets:e.assets,contentTitle:e.contentTitle,toc:e.toc})),[e])}(n);return(0,l.jsx)(o.Provider,{value:a,children:t})}function c(){const e=(0,s.useContext)(o);if(null===e)throw new i.i6("DocProvider");return e}function d(){const{metadata:e,frontMatter:t,assets:n}=c();return(0,l.jsx)(a.d,{title:e.title,description:e.description,keywords:t.keywords,image:n.image??t.image})}var u=n(512),m=n(7524),h=n(5999),v=n(2244);function x(e){const{previous:t,next:n}=e;return(0,l.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,h.I)({id:"theme.docs.paginator.navAriaLabel",message:"Docs pages",description:"The ARIA label for the docs pagination"}),children:[t&&(0,l.jsx)(v.Z,{...t,subLabel:(0,l.jsx)(h.Z,{id:"theme.docs.paginator.previous",description:"The label used to navigate to the previous doc",children:"Previous"})}),n&&(0,l.jsx)(v.Z,{...n,subLabel:(0,l.jsx)(h.Z,{id:"theme.docs.paginator.next",description:"The label used to navigate to the next doc",children:"Next"}),isNext:!0})]})}function p(){const{metadata:e}=c();return(0,l.jsx)(x,{previous:e.previous,next:e.next})}var b=n(2263),g=n(3692),f=n(143),j=n(5281),L=n(373),N=n(4477);const C={unreleased:function(e){let{siteTitle:t,versionMetadata:n}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.unreleasedVersionLabel",description:"The label used to tell the user that he's browsing an unreleased doc version",values:{siteTitle:t,versionLabel:(0,l.jsx)("b",{children:n.label})},children:"This is unreleased documentation for {siteTitle} {versionLabel} version."})},unmaintained:function(e){let{siteTitle:t,versionMetadata:n}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.unmaintainedVersionLabel",description:"The label used to tell the user that he's browsing an unmaintained doc version",values:{siteTitle:t,versionLabel:(0,l.jsx)("b",{children:n.label})},children:"This is documentation for {siteTitle} {versionLabel}, which is no longer actively maintained."})}};function _(e){const t=C[e.versionMetadata.banner];return(0,l.jsx)(t,{...e})}function Z(e){let{versionLabel:t,to:n,onClick:s}=e;return(0,l.jsx)(h.Z,{id:"theme.docs.versions.latestVersionSuggestionLabel",description:"The label used to tell the user to check the latest version",values:{versionLabel:t,latestVersionLink:(0,l.jsx)("b",{children:(0,l.jsx)(g.Z,{to:n,onClick:s,children:(0,l.jsx)(h.Z,{id:"theme.docs.versions.latestVersionLinkLabel",description:"The label used for the latest version suggestion link label",children:"latest version"})})})},children:"For up-to-date documentation, see the {latestVersionLink} ({versionLabel})."})}function k(e){let{className:t,versionMetadata:n}=e;const{siteConfig:{title:s}}=(0,b.Z)(),{pluginId:a}=(0,f.gA)({failfast:!0}),{savePreferredVersionName:i}=(0,L.J)(a),{latestDocSuggestion:o,latestVersionSuggestion:r}=(0,f.Jo)(a),c=o??(d=r).docs.find((e=>e.id===d.mainDocId));var d;return(0,l.jsxs)("div",{className:(0,u.Z)(t,j.k.docs.docVersionBanner,"alert alert--warning margin-bottom--md"),role:"alert",children:[(0,l.jsx)("div",{children:(0,l.jsx)(_,{siteTitle:s,versionMetadata:n})}),(0,l.jsx)("div",{className:"margin-top--md",children:(0,l.jsx)(Z,{versionLabel:r.label,to:c.path,onClick:()=>i(r.name)})})]})}function T(e){let{className:t}=e;const n=(0,N.E)();return n.banner?(0,l.jsx)(k,{className:t,versionMetadata:n}):null}function U(e){let{className:t}=e;const n=(0,N.E)();return n.badge?(0,l.jsx)("span",{className:(0,u.Z)(t,j.k.docs.docVersionBadge,"badge badge--secondary"),children:(0,l.jsx)(h.Z,{id:"theme.docs.versionBadge.label",values:{versionLabel:n.label},children:"Version: {versionLabel}"})}):null}function H(e){let{lastUpdatedAt:t,formattedLastUpdatedAt:n}=e;return(0,l.jsx)(h.Z,{id:"theme.lastUpdated.atDate",description:"The words used to describe on which date a page has been last updated",values:{date:(0,l.jsx)("b",{children:(0,l.jsx)("time",{dateTime:new Date(1e3*t).toISOString(),children:n})})},children:" on {date}"})}function y(e){let{lastUpdatedBy:t}=e;return(0,l.jsx)(h.Z,{id:"theme.lastUpdated.byUser",description:"The words used to describe by who the page has been last updated",values:{user:(0,l.jsx)("b",{children:t})},children:" by {user}"})}function w(e){let{lastUpdatedAt:t,formattedLastUpdatedAt:n,lastUpdatedBy:s}=e;return(0,l.jsxs)("span",{className:j.k.common.lastUpdated,children:[(0,l.jsx)(h.Z,{id:"theme.lastUpdated.lastUpdatedAtBy",description:"The sentence used to display when a page has been last updated, and by who",values:{atDate:t&&n?(0,l.jsx)(H,{lastUpdatedAt:t,formattedLastUpdatedAt:n}):"",byUser:s?(0,l.jsx)(y,{lastUpdatedBy:s}):""},children:"Last updated{atDate}{byUser}"}),!1]})}var A=n(4881),M=n(1526);const E={lastUpdated:"lastUpdated_vwxv"};function I(e){return(0,l.jsx)("div",{className:(0,u.Z)(j.k.docs.docFooterTagsRow,"row margin-bottom--sm"),children:(0,l.jsx)("div",{className:"col",children:(0,l.jsx)(M.Z,{...e})})})}function B(e){let{editUrl:t,lastUpdatedAt:n,lastUpdatedBy:s,formattedLastUpdatedAt:a}=e;return(0,l.jsxs)("div",{className:(0,u.Z)(j.k.docs.docFooterEditMetaRow,"row"),children:[(0,l.jsx)("div",{className:"col",children:t&&(0,l.jsx)(A.Z,{editUrl:t})}),(0,l.jsx)("div",{className:(0,u.Z)("col",E.lastUpdated),children:(n||s)&&(0,l.jsx)(w,{lastUpdatedAt:n,formattedLastUpdatedAt:a,lastUpdatedBy:s})})]})}function O(){const{metadata:e}=c(),{editUrl:t,lastUpdatedAt:n,formattedLastUpdatedAt:s,lastUpdatedBy:a,tags:i}=e,o=i.length>0,r=!!(t||n||a);return o||r?(0,l.jsxs)("footer",{className:(0,u.Z)(j.k.docs.docFooter,"docusaurus-mt-lg"),children:[o&&(0,l.jsx)(I,{tags:i}),r&&(0,l.jsx)(B,{editUrl:t,lastUpdatedAt:n,lastUpdatedBy:a,formattedLastUpdatedAt:s})]}):null}var S=n(6043),V=n(3743);const P={tocCollapsibleButton:"tocCollapsibleButton_TO0P",tocCollapsibleButtonExpanded:"tocCollapsibleButtonExpanded_MG3E"};function R(e){let{collapsed:t,...n}=e;return(0,l.jsx)("button",{type:"button",...n,className:(0,u.Z)("clean-btn",P.tocCollapsibleButton,!t&&P.tocCollapsibleButtonExpanded,n.className),children:(0,l.jsx)(h.Z,{id:"theme.TOCCollapsible.toggleButtonLabel",description:"The label used by the button on the collapsible TOC component",children:"On this page"})})}const D={tocCollapsible:"tocCollapsible_ETCw",tocCollapsibleContent:"tocCollapsibleContent_vkbj",tocCollapsibleExpanded:"tocCollapsibleExpanded_sAul"};function F(e){let{toc:t,className:n,minHeadingLevel:s,maxHeadingLevel:a}=e;const{collapsed:i,toggleCollapsed:o}=(0,S.u)({initialState:!0});return(0,l.jsxs)("div",{className:(0,u.Z)(D.tocCollapsible,!i&&D.tocCollapsibleExpanded,n),children:[(0,l.jsx)(R,{collapsed:i,onClick:o}),(0,l.jsx)(S.z,{lazy:!0,className:D.tocCollapsibleContent,collapsed:i,children:(0,l.jsx)(V.Z,{toc:t,minHeadingLevel:s,maxHeadingLevel:a})})]})}const z={tocMobile:"tocMobile_ITEo"};function q(){const{toc:e,frontMatter:t}=c();return(0,l.jsx)(F,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:(0,u.Z)(j.k.docs.docTocMobile,z.tocMobile)})}var G=n(9407);function W(){const{toc:e,frontMatter:t}=c();return(0,l.jsx)(G.Z,{toc:e,minHeadingLevel:t.toc_min_heading_level,maxHeadingLevel:t.toc_max_heading_level,className:j.k.docs.docTocDesktop})}var $=n(2503),J=n(7395);function Q(e){let{children:t}=e;const n=function(){const{metadata:e,frontMatter:t,contentTitle:n}=c();return t.hide_title||void 0!==n?null:e.title}();return(0,l.jsxs)("div",{className:(0,u.Z)(j.k.docs.docMarkdown,"markdown"),children:[n&&(0,l.jsx)("header",{children:(0,l.jsx)($.Z,{as:"h1",children:n})}),(0,l.jsx)(J.Z,{children:t})]})}var X=n(2802),Y=n(8596),K=n(4996);function ee(e){return(0,l.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,l.jsx)("path",{d:"M10 19v-5h4v5c0 .55.45 1 1 1h3c.55 0 1-.45 1-1v-7h1.7c.46 0 .68-.57.33-.87L12.67 3.6c-.38-.34-.96-.34-1.34 0l-8.36 7.53c-.34.3-.13.87.33.87H5v7c0 .55.45 1 1 1h3c.55 0 1-.45 1-1z",fill:"currentColor"})})}const te={breadcrumbHomeIcon:"breadcrumbHomeIcon_YNFT"};function ne(){const e=(0,K.Z)("/");return(0,l.jsx)("li",{className:"breadcrumbs__item",children:(0,l.jsx)(g.Z,{"aria-label":(0,h.I)({id:"theme.docs.breadcrumbs.home",message:"Home page",description:"The ARIA label for the home page in the breadcrumbs"}),className:"breadcrumbs__link",href:e,children:(0,l.jsx)(ee,{className:te.breadcrumbHomeIcon})})})}const se={breadcrumbsContainer:"breadcrumbsContainer_Z_bl"};function ae(e){let{children:t,href:n,isLast:s}=e;const a="breadcrumbs__link";return s?(0,l.jsx)("span",{className:a,itemProp:"name",children:t}):n?(0,l.jsx)(g.Z,{className:a,href:n,itemProp:"item",children:(0,l.jsx)("span",{itemProp:"name",children:t})}):(0,l.jsx)("span",{className:a,children:t})}function ie(e){let{children:t,active:n,index:s,addMicrodata:a}=e;return(0,l.jsxs)("li",{...a&&{itemScope:!0,itemProp:"itemListElement",itemType:"https://schema.org/ListItem"},className:(0,u.Z)("breadcrumbs__item",{"breadcrumbs__item--active":n}),children:[t,(0,l.jsx)("meta",{itemProp:"position",content:String(s+1)})]})}function le(){const e=(0,X.s1)(),t=(0,Y.Ns)();return e?(0,l.jsx)("nav",{className:(0,u.Z)(j.k.docs.docBreadcrumbs,se.breadcrumbsContainer),"aria-label":(0,h.I)({id:"theme.docs.breadcrumbs.navAriaLabel",message:"Breadcrumbs",description:"The ARIA label for the breadcrumbs"}),children:(0,l.jsxs)("ul",{className:"breadcrumbs",itemScope:!0,itemType:"https://schema.org/BreadcrumbList",children:[t&&(0,l.jsx)(ne,{}),e.map(((t,n)=>{const s=n===e.length-1,a="category"===t.type&&t.linkUnlisted?void 0:t.href;return(0,l.jsx)(ie,{active:s,index:n,addMicrodata:!!a,children:(0,l.jsx)(ae,{href:a,isLast:s,children:t.label})},n)}))]})}):null}var oe=n(2212);const re={docItemContainer:"docItemContainer_Djhp",docItemCol:"docItemCol_VOVn"};function ce(e){let{children:t}=e;const n=function(){const{frontMatter:e,toc:t}=c(),n=(0,m.i)(),s=e.hide_table_of_contents,a=!s&&t.length>0;return{hidden:s,mobile:a?(0,l.jsx)(q,{}):void 0,desktop:!a||"desktop"!==n&&"ssr"!==n?void 0:(0,l.jsx)(W,{})}}(),{metadata:{unlisted:s}}=c();return(0,l.jsxs)("div",{className:"row",children:[(0,l.jsxs)("div",{className:(0,u.Z)("col",!n.hidden&&re.docItemCol),children:[s&&(0,l.jsx)(oe.Z,{}),(0,l.jsx)(T,{}),(0,l.jsxs)("div",{className:re.docItemContainer,children:[(0,l.jsxs)("article",{children:[(0,l.jsx)(le,{}),(0,l.jsx)(U,{}),n.mobile,(0,l.jsx)(Q,{children:t}),(0,l.jsx)(O,{})]}),(0,l.jsx)(p,{})]})]}),n.desktop&&(0,l.jsx)("div",{className:"col col--3",children:n.desktop})]})}function de(e){const t=`docs-doc-id-${e.content.metadata.id}`,n=e.content;return(0,l.jsx)(r,{content:e.content,children:(0,l.jsxs)(a.FG,{className:t,children:[(0,l.jsx)(d,{}),(0,l.jsx)(ce,{children:(0,l.jsx)(n,{})})]})})}},4881:(e,t,n)=>{n.d(t,{Z:()=>d});n(7294);var s=n(5999),a=n(5281),i=n(3692),l=n(512);const o={iconEdit:"iconEdit_Z9Sw"};var r=n(5893);function c(e){let{className:t,...n}=e;return(0,r.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,l.Z)(o.iconEdit,t),"aria-hidden":"true",...n,children:(0,r.jsx)("g",{children:(0,r.jsx)("path",{d:"m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z"})})})}function d(e){let{editUrl:t}=e;return(0,r.jsxs)(i.Z,{to:t,className:a.k.common.editThisPage,children:[(0,r.jsx)(c,{}),(0,r.jsx)(s.Z,{id:"theme.common.editThisPage",description:"The link label to edit the current page",children:"Edit this page"})]})}},2244:(e,t,n)=>{n.d(t,{Z:()=>l});n(7294);var s=n(512),a=n(3692),i=n(5893);function l(e){const{permalink:t,title:n,subLabel:l,isNext:o}=e;return(0,i.jsxs)(a.Z,{className:(0,s.Z)("pagination-nav__link",o?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[l&&(0,i.jsx)("div",{className:"pagination-nav__sublabel",children:l}),(0,i.jsx)("div",{className:"pagination-nav__label",children:n})]})}},9407:(e,t,n)=>{n.d(t,{Z:()=>c});n(7294);var s=n(512),a=n(3743);const i={tableOfContents:"tableOfContents_bqdL",docItemContainer:"docItemContainer_F8PC"};var l=n(5893);const o="table-of-contents__link toc-highlight",r="table-of-contents__link--active";function c(e){let{className:t,...n}=e;return(0,l.jsx)("div",{className:(0,s.Z)(i.tableOfContents,"thin-scrollbar",t),children:(0,l.jsx)(a.Z,{...n,linkClassName:o,linkActiveClassName:r})})}},3743:(e,t,n)=>{n.d(t,{Z:()=>x});var s=n(7294),a=n(6668);function i(e){const t=e.map((e=>({...e,parentIndex:-1,children:[]}))),n=Array(7).fill(-1);t.forEach(((e,t)=>{const s=n.slice(2,e.level);e.parentIndex=Math.max(...s),n[e.level]=t}));const s=[];return t.forEach((e=>{const{parentIndex:n,...a}=e;n>=0?t[n].children.push(a):s.push(a)})),s}function l(e){let{toc:t,minHeadingLevel:n,maxHeadingLevel:s}=e;return t.flatMap((e=>{const t=l({toc:e.children,minHeadingLevel:n,maxHeadingLevel:s});return function(e){return e.level>=n&&e.level<=s}(e)?[{...e,children:t}]:t}))}function o(e){const t=e.getBoundingClientRect();return t.top===t.bottom?o(e.parentNode):t}function r(e,t){let{anchorTopOffset:n}=t;const s=e.find((e=>o(e).top>=n));if(s){return function(e){return e.top>0&&e.bottom{e.current=t?0:document.querySelector(".navbar").clientHeight}),[t]),e}function d(e){const t=(0,s.useRef)(void 0),n=c();(0,s.useEffect)((()=>{if(!e)return()=>{};const{linkClassName:s,linkActiveClassName:a,minHeadingLevel:i,maxHeadingLevel:l}=e;function o(){const e=function(e){return Array.from(document.getElementsByClassName(e))}(s),o=function(e){let{minHeadingLevel:t,maxHeadingLevel:n}=e;const s=[];for(let a=t;a<=n;a+=1)s.push(`h${a}.anchor`);return Array.from(document.querySelectorAll(s.join()))}({minHeadingLevel:i,maxHeadingLevel:l}),c=r(o,{anchorTopOffset:n.current}),d=e.find((e=>c&&c.id===function(e){return decodeURIComponent(e.href.substring(e.href.indexOf("#")+1))}(e)));e.forEach((e=>{!function(e,n){n?(t.current&&t.current!==e&&t.current.classList.remove(a),e.classList.add(a),t.current=e):e.classList.remove(a)}(e,e===d)}))}return document.addEventListener("scroll",o),document.addEventListener("resize",o),o(),()=>{document.removeEventListener("scroll",o),document.removeEventListener("resize",o)}}),[e,n])}var u=n(3692),m=n(5893);function h(e){let{toc:t,className:n,linkClassName:s,isChild:a}=e;return t.length?(0,m.jsx)("ul",{className:a?void 0:n,children:t.map((e=>(0,m.jsxs)("li",{children:[(0,m.jsx)(u.Z,{to:`#${e.id}`,className:s??void 0,dangerouslySetInnerHTML:{__html:e.value}}),(0,m.jsx)(h,{isChild:!0,toc:e.children,className:n,linkClassName:s})]},e.id)))}):null}const v=s.memo(h);function x(e){let{toc:t,className:n="table-of-contents table-of-contents__left-border",linkClassName:o="table-of-contents__link",linkActiveClassName:r,minHeadingLevel:c,maxHeadingLevel:u,...h}=e;const x=(0,a.L)(),p=c??x.tableOfContents.minHeadingLevel,b=u??x.tableOfContents.maxHeadingLevel,g=function(e){let{toc:t,minHeadingLevel:n,maxHeadingLevel:a}=e;return(0,s.useMemo)((()=>l({toc:i(t),minHeadingLevel:n,maxHeadingLevel:a})),[t,n,a])}({toc:t,minHeadingLevel:p,maxHeadingLevel:b});return d((0,s.useMemo)((()=>{if(o&&r)return{linkClassName:o,linkActiveClassName:r,minHeadingLevel:p,maxHeadingLevel:b}}),[o,r,p,b])),(0,m.jsx)(v,{toc:g,className:n,linkClassName:o,...h})}},3008:(e,t,n)=>{n.d(t,{Z:()=>o});n(7294);var s=n(512),a=n(3692);const i={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var l=n(5893);function o(e){let{permalink:t,label:n,count:o}=e;return(0,l.jsxs)(a.Z,{href:t,className:(0,s.Z)(i.tag,o?i.tagWithCount:i.tagRegular),children:[n,o&&(0,l.jsx)("span",{children:o})]})}},1526:(e,t,n)=>{n.d(t,{Z:()=>r});n(7294);var s=n(512),a=n(5999),i=n(3008);const l={tags:"tags_jXut",tag:"tag_QGVx"};var o=n(5893);function r(e){let{tags:t}=e;return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)("b",{children:(0,o.jsx)(a.Z,{id:"theme.tags.tagsListLabel",description:"The label alongside a tag list",children:"Tags:"})}),(0,o.jsx)("ul",{className:(0,s.Z)(l.tags,"padding--none","margin-left--sm"),children:t.map((e=>{let{label:t,permalink:n}=e;return(0,o.jsx)("li",{className:l.tag,children:(0,o.jsx)(i.Z,{label:t,permalink:n})},n)}))})]})}},2212:(e,t,n)=>{n.d(t,{Z:()=>h});n(7294);var s=n(512),a=n(5999),i=n(5742),l=n(5893);function o(){return(0,l.jsx)(a.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function r(){return(0,l.jsx)(a.Z,{id:"theme.unlistedContent.message",description:"The unlisted content banner message",children:"This page is unlisted. Search engines will not index it, and only users having a direct link can access it."})}function c(){return(0,l.jsx)(i.Z,{children:(0,l.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})}var d=n(5281),u=n(9047);function m(e){let{className:t}=e;return(0,l.jsx)(u.Z,{type:"caution",title:(0,l.jsx)(o,{}),className:(0,s.Z)(t,d.k.common.unlistedBanner),children:(0,l.jsx)(r,{})})}function h(e){return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(c,{}),(0,l.jsx)(m,{...e})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/19826855.9478e407.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/19826855.9478e407.js new file mode 100644 index 00000000..5b831df2 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/19826855.9478e407.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8875],{7982:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>u,frontMatter:()=>r,metadata:()=>o,toc:()=>d});var s=n(5893),a=n(1151);const r={},i="User Management UI",o={id:"usermanagementui",title:"User Management UI",description:"Overview",source:"@site/docs/usermanagementui.md",sourceDirName:".",slug:"/usermanagementui",permalink:"/docs/usermanagementui",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/usermanagementui.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"OAuth2 Authentication",permalink:"/docs/oath2authentication"},next:{title:"Data Retention Tool",permalink:"/docs/dataretentiontool"}},c={},d=[{value:"Overview",id:"overview",level:2},{value:"Usage",id:"usage",level:2}];function l(e){const t={h1:"h1",h2:"h2",img:"img",li:"li",p:"p",ul:"ul",...(0,a.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"user-management-ui",children:"User Management UI"}),"\n",(0,s.jsx)(t.h2,{id:"overview",children:"Overview"}),"\n",(0,s.jsx)(t.p,{children:"The User Management UI provides an intuitive drag-and-drop mechanisms for managing TAK user accounts. The tool is integrated within TAK Server and can be accessed from the TAK main menu, under Administrative >> Manage Users. Users need to have an admin role to access the tool. Currently the User Management UI supports only file-based users and not LDAP/AD users.\nThe tool allows TAK administrators to create, manage, inspect and delete TAK user accounts. More specifically, the tool allows TAK administrators to:"}),"\n",(0,s.jsxs)(t.ul,{children:["\n",(0,s.jsx)(t.li,{children:"View, filter and search for existing user accounts and groups."}),"\n",(0,s.jsx)(t.li,{children:"View a list of users in each group."}),"\n",(0,s.jsx)(t.li,{children:"Change password for each user account."}),"\n",(0,s.jsx)(t.li,{children:"View and update groups (IN group, OUT group and both) for each user account."}),"\n",(0,s.jsx)(t.li,{children:"Delete user accounts."}),"\n",(0,s.jsx)(t.li,{children:"Create a new user account with a specified password and groups. Password complexity is checked to confirm compliance."}),"\n",(0,s.jsx)(t.li,{children:"Create new user accounts in bulk with username following a pattern. System uses password generation mechanism to create passwords that meet TAK password complexity requirements. System produces output file with user/password combos as a one-time downloadable item, after which system forgets the un-hashed passwords."}),"\n",(0,s.jsx)(t.li,{children:"Create new groups."}),"\n"]}),"\n",(0,s.jsx)(t.h2,{id:"usage",children:"Usage"}),"\n",(0,s.jsx)(t.p,{children:"The below figure shows the main page of the User Management UI. The left panel lists all user accounts, which can be filtered using the Search box on the top. The right panel lists all existing groups, which can be filtered using the Search box on the top."}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Main Page",src:n(816).Z+"",width:"3240",height:"1546"})}),"\n",(0,s.jsx)(t.p,{children:'To change user\u2019s password, click on the arrow right next to the username and select "Change password".'}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Change Password",src:n(5200).Z+"",width:"3240",height:"1566"})}),"\n",(0,s.jsx)(t.p,{children:'To view/edit groups for a user account, click on the arrow right next to the username and select "View/Edit groups". You can drag the groups from the right panel and drop to one of the three boxes in the middle panel. Click on \u201cReset\u201d button to bring the UI back to showing the current groups of the user. Click on \u201cUpdate\u201d button to update the groups of the user.'}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"View Edit Groups",src:n(5459).Z+"",width:"3240",height:"1584"})}),"\n",(0,s.jsx)(t.p,{children:'To delete an account, click on the arrow right next to the username and select "Delete User". You will be prompted to either confirm or cancel the action.'}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Delete Account",src:n(8887).Z+"",width:"3240",height:"1554"})}),"\n",(0,s.jsx)(t.p,{children:'To list all users in a group, click on the arrow right next to the group name and click on "List users".'}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"List Users",src:n(1336).Z+"",width:"3240",height:"1916"})}),"\n",(0,s.jsx)(t.p,{children:'To create a new user, click on "Add User" on the menu bar.'}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Add User",src:n(6702).Z+"",width:"3240",height:"1916"})}),"\n",(0,s.jsx)(t.p,{children:'To create new users in bulk, click on "Add Users" on the menu bar.'})]})}function u(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},816:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/user_manage_1-ea2f4176c744d741688e08935a7e6e11.png"},5200:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/user_manage_2-1a19fe7c484d18a00a6a3c624b93a977.png"},5459:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/user_manage_3-eef404a96c26aa9915852afe7dc806d6.png"},8887:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/user_manage_4-1a7f3ef3849fdb9249390b5c29d9aac2.png"},1336:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/user_manage_5-0fa98c7b0fc71e17b2a7b310d6fd6c88.png"},6702:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/user_manage_6-109e5483782703d59efa3fde5d496fe8.png"},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>i});var s=n(7294);const a={},r=s.createContext(a);function i(e){const t=s.useContext(r);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:i(e.components),s.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1b9776b6.7472ebb5.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1b9776b6.7472ebb5.js new file mode 100644 index 00000000..02cb65b1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1b9776b6.7472ebb5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8761],{664:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>p,contentTitle:()=>i,default:()=>u,frontMatter:()=>a,metadata:()=>d,toc:()=>s});var n=o(5893),r=o(1151);const a={},i="Federated Group Mapping",d={id:"federation/federatedgroupmapping",title:"Federated Group Mapping",description:"The flow of traffic between Federates may be directed using end-to-end group mapping. The Federated Group Mapping section is on the Federate Groups page.",source:"@site/docs/federation/federatedgroupmapping.md",sourceDirName:"federation",slug:"/federation/federatedgroupmapping",permalink:"/docs/federation/federatedgroupmapping",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/federatedgroupmapping.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Make the Connection",permalink:"/docs/federation/maketheconnection"},next:{title:"Mission Federation Disruption Tolerance",permalink:"/docs/federation/federationdisruptiontolerance"}},p={},s=[];function c(e){const t={h1:"h1",img:"img",p:"p",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"federated-group-mapping",children:"Federated Group Mapping"}),"\n",(0,n.jsx)(t.p,{children:"The flow of traffic between Federates may be directed using end-to-end group mapping. The Federated Group Mapping section is on the Federate Groups page."}),"\n",(0,n.jsx)(t.p,{children:"Groups are exchanged during active connections between Federates. The remote groups will appear in the \u2018Remote Group List\u2019 drop down in the Federated Group Mapping section. Connected Federates must have Federated Group Mapping enabled in order for the Federates to exchange their respective remote groups. This parameter is in the Federation Configuration section in the Configuration > Manage Federates page."}),"\n",(0,n.jsx)(t.p,{children:"To configure the end-to-end mapping, select a remote group and map it to a local Federate group. Remote groups may also be entered directly in the \u2018Remote Group\u2019 field. A single remote group can be mapped to many local groups. Additionally, multiple end-to-end group mappings may be defined. With a group mapping configured, traffic from the remote group will only flow to the mapped local group(s). Note: if no incoming traffic matches the remote groups configured, the federation traffic will fall back to the Federate Group scheme described previously."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Remote Groups",src:o(766).Z+"",width:"1440",height:"1125"})})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(c,{...e})}):c(e)}},766:(e,t,o)=>{o.d(t,{Z:()=>n});const n=o.p+"assets/images/fed_5-820faa1841be40dc6c1d775cfa977671.png"},1151:(e,t,o)=>{o.d(t,{Z:()=>d,a:()=>i});var n=o(7294);const r={},a=n.createContext(r);function i(e){const t=n.useContext(a);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function d(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),n.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1dba1ecf.46716bc6.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1dba1ecf.46716bc6.js new file mode 100644 index 00000000..061a471a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1dba1ecf.46716bc6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2510],{7937:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>d});var s=n(5893),r=n(1151);const i={},o="Metrics",a={id:"metrics",title:"Metrics",description:"The TAK Server Metrics Dashboard is available in the Monitoring menu. The dashboard continuously renders the following information:",source:"@site/docs/metrics.md",sourceDirName:".",slug:"/metrics",permalink:"/docs/metrics",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/metrics.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Federation Example",permalink:"/docs/federation/federationexample"},next:{title:"Logging",permalink:"/docs/logging"}},c={},d=[];function l(e){const t={h1:"h1",img:"img",p:"p",strong:"strong",...(0,r.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"metrics",children:"Metrics"}),"\n",(0,s.jsx)(t.p,{children:"The TAK Server Metrics Dashboard is available in the Monitoring menu. The dashboard continuously renders the following information:"}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Server Start Time and Server Up Time"})," This tell you when the server was turned on and how long it has been operating."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Clients Connected"})," This tells you how many connections your client is currently servicing. This corresponds to the number of clients that are displayed in the client dashboard."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Heap Usage"})," TAK server runs inside one or more Java Virtual Machines (JVM). Heap Commited is how much heap memory in MB is allocated to the API process for TAK Server, and Heap Used is how much of that is currently being used."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"Network I/O and Reads/Writes"})," This tells you how much TCP and UDP traffic the server is currently handling, as well as a brief history."]}),"\n",(0,s.jsxs)(t.p,{children:[(0,s.jsx)(t.strong,{children:"CPU Usage"})," How much of the CPU of the machine the server is running on is currently being used."]}),"\n",(0,s.jsx)(t.p,{children:(0,s.jsx)(t.img,{alt:"Metrics Page",src:n(9006).Z+"",width:"1045",height:"717"})})]})}function h(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(l,{...e})}):l(e)}},9006:(e,t,n)=>{n.d(t,{Z:()=>s});const s=n.p+"assets/images/metrics_1-a6440cb2dd502e1d29149a070af422eb.png"},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>o});var s=n(7294);const r={},i=s.createContext(r);function o(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1f391b9e.5c51203d.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1f391b9e.5c51203d.js new file mode 100644 index 00000000..564e1131 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/1f391b9e.5c51203d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3085],{4247:(e,n,t)=>{t.r(n),t.d(n,{default:()=>u});t(7294);var i=t(512),a=t(1944),s=t(5281),l=t(6040),r=t(7395),c=t(9407),o=t(2212);const d={mdxPageWrapper:"mdxPageWrapper_j9I6"};var m=t(5893);function u(e){const{content:n}=e,{metadata:{title:t,description:u,frontMatter:f,unlisted:v},assets:h}=n,{keywords:g,wrapperClassName:x,hide_table_of_contents:p}=f,L=h.image??f.image;return(0,m.jsx)(a.FG,{className:(0,i.Z)(x??s.k.wrapper.mdxPages,s.k.page.mdxPage),children:(0,m.jsxs)(l.Z,{children:[(0,m.jsx)(a.d,{title:t,description:u,keywords:g,image:L}),(0,m.jsx)("main",{className:"container container--fluid margin-vert--lg",children:(0,m.jsxs)("div",{className:(0,i.Z)("row",d.mdxPageWrapper),children:[(0,m.jsxs)("div",{className:(0,i.Z)("col",!p&&"col--8"),children:[v&&(0,m.jsx)(o.Z,{}),(0,m.jsx)("article",{children:(0,m.jsx)(r.Z,{children:(0,m.jsx)(n,{})})})]}),!p&&n.toc.length>0&&(0,m.jsx)("div",{className:"col col--2",children:(0,m.jsx)(c.Z,{toc:n.toc,minHeadingLevel:f.toc_min_heading_level,maxHeadingLevel:f.toc_max_heading_level})})]})})]})})}},9407:(e,n,t)=>{t.d(n,{Z:()=>o});t(7294);var i=t(512),a=t(3743);const s={tableOfContents:"tableOfContents_bqdL",docItemContainer:"docItemContainer_F8PC"};var l=t(5893);const r="table-of-contents__link toc-highlight",c="table-of-contents__link--active";function o(e){let{className:n,...t}=e;return(0,l.jsx)("div",{className:(0,i.Z)(s.tableOfContents,"thin-scrollbar",n),children:(0,l.jsx)(a.Z,{...t,linkClassName:r,linkActiveClassName:c})})}},3743:(e,n,t)=>{t.d(n,{Z:()=>h});var i=t(7294),a=t(6668);function s(e){const n=e.map((e=>({...e,parentIndex:-1,children:[]}))),t=Array(7).fill(-1);n.forEach(((e,n)=>{const i=t.slice(2,e.level);e.parentIndex=Math.max(...i),t[e.level]=n}));const i=[];return n.forEach((e=>{const{parentIndex:t,...a}=e;t>=0?n[t].children.push(a):i.push(a)})),i}function l(e){let{toc:n,minHeadingLevel:t,maxHeadingLevel:i}=e;return n.flatMap((e=>{const n=l({toc:e.children,minHeadingLevel:t,maxHeadingLevel:i});return function(e){return e.level>=t&&e.level<=i}(e)?[{...e,children:n}]:n}))}function r(e){const n=e.getBoundingClientRect();return n.top===n.bottom?r(e.parentNode):n}function c(e,n){let{anchorTopOffset:t}=n;const i=e.find((e=>r(e).top>=t));if(i){return function(e){return e.top>0&&e.bottom{e.current=n?0:document.querySelector(".navbar").clientHeight}),[n]),e}function d(e){const n=(0,i.useRef)(void 0),t=o();(0,i.useEffect)((()=>{if(!e)return()=>{};const{linkClassName:i,linkActiveClassName:a,minHeadingLevel:s,maxHeadingLevel:l}=e;function r(){const e=function(e){return Array.from(document.getElementsByClassName(e))}(i),r=function(e){let{minHeadingLevel:n,maxHeadingLevel:t}=e;const i=[];for(let a=n;a<=t;a+=1)i.push(`h${a}.anchor`);return Array.from(document.querySelectorAll(i.join()))}({minHeadingLevel:s,maxHeadingLevel:l}),o=c(r,{anchorTopOffset:t.current}),d=e.find((e=>o&&o.id===function(e){return decodeURIComponent(e.href.substring(e.href.indexOf("#")+1))}(e)));e.forEach((e=>{!function(e,t){t?(n.current&&n.current!==e&&n.current.classList.remove(a),e.classList.add(a),n.current=e):e.classList.remove(a)}(e,e===d)}))}return document.addEventListener("scroll",r),document.addEventListener("resize",r),r(),()=>{document.removeEventListener("scroll",r),document.removeEventListener("resize",r)}}),[e,t])}var m=t(3692),u=t(5893);function f(e){let{toc:n,className:t,linkClassName:i,isChild:a}=e;return n.length?(0,u.jsx)("ul",{className:a?void 0:t,children:n.map((e=>(0,u.jsxs)("li",{children:[(0,u.jsx)(m.Z,{to:`#${e.id}`,className:i??void 0,dangerouslySetInnerHTML:{__html:e.value}}),(0,u.jsx)(f,{isChild:!0,toc:e.children,className:t,linkClassName:i})]},e.id)))}):null}const v=i.memo(f);function h(e){let{toc:n,className:t="table-of-contents table-of-contents__left-border",linkClassName:r="table-of-contents__link",linkActiveClassName:c,minHeadingLevel:o,maxHeadingLevel:m,...f}=e;const h=(0,a.L)(),g=o??h.tableOfContents.minHeadingLevel,x=m??h.tableOfContents.maxHeadingLevel,p=function(e){let{toc:n,minHeadingLevel:t,maxHeadingLevel:a}=e;return(0,i.useMemo)((()=>l({toc:s(n),minHeadingLevel:t,maxHeadingLevel:a})),[n,t,a])}({toc:n,minHeadingLevel:g,maxHeadingLevel:x});return d((0,i.useMemo)((()=>{if(r&&c)return{linkClassName:r,linkActiveClassName:c,minHeadingLevel:g,maxHeadingLevel:x}}),[r,c,g,x])),(0,u.jsx)(v,{toc:p,className:t,linkClassName:r,...f})}},2212:(e,n,t)=>{t.d(n,{Z:()=>f});t(7294);var i=t(512),a=t(5999),s=t(5742),l=t(5893);function r(){return(0,l.jsx)(a.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function c(){return(0,l.jsx)(a.Z,{id:"theme.unlistedContent.message",description:"The unlisted content banner message",children:"This page is unlisted. Search engines will not index it, and only users having a direct link can access it."})}function o(){return(0,l.jsx)(s.Z,{children:(0,l.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})}var d=t(5281),m=t(9047);function u(e){let{className:n}=e;return(0,l.jsx)(m.Z,{type:"caution",title:(0,l.jsx)(r,{}),className:(0,i.Z)(n,d.k.common.unlistedBanner),children:(0,l.jsx)(c,{})})}function f(e){return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(o,{}),(0,l.jsx)(u,{...e})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/20598c5e.81cf8894.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/20598c5e.81cf8894.js new file mode 100644 index 00000000..8b1163ce --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/20598c5e.81cf8894.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2399],{8635:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>r,default:()=>l,frontMatter:()=>o,metadata:()=>c,toc:()=>u});var i=n(5893),a=n(1151);const o={},r="Authentication Backends",c={id:"configuration/authenticationbackends",title:"Authentication Backends",description:"File-Based",source:"@site/docs/configuration/authenticationbackends.md",sourceDirName:"configuration",slug:"/configuration/authenticationbackends",permalink:"/docs/configuration/authenticationbackends",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/authenticationbackends.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Group Assignment using Client Certificates",permalink:"/docs/configuration/groupassignmentusingclientcerts"},next:{title:"Configuring Messaging and Repository Settings through Web UI",permalink:"/docs/configuration/configuremessagingrepositorywebui"}},s={},u=[{value:"File-Based",id:"file-based",level:2},{value:"Active Directory (AD) / LDAP",id:"active-directory-ad--ldap",level:2},{value:"Configuring LDAP Through Web Interface",id:"configuring-ldap-through-web-interface",level:2}];function d(e){const t={code:"code",h1:"h1",h2:"h2",img:"img",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"authentication-backends",children:"Authentication Backends"}),"\n",(0,i.jsx)(t.h2,{id:"file-based",children:"File-Based"}),"\n",(0,i.jsx)(t.p,{children:"There is now a flat-file option available to inputs. Previously the only valid value for the \u201cauth\u201d attribute was \u201cldap\u201d. \u201cfile\u201d is now another valid value. The example configuration file (CoreConfig.example.xml) contains an example of how to configure the File-based backend."}),"\n",(0,i.jsx)(t.p,{children:"A utility for creating and maintaining that flat file is included in the release. Run"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:"sudo java -jar /opt/tak/utils/UserManager.jar\n"})}),"\n",(0,i.jsx)(t.p,{children:"and look at the various options for the 'offlineFileAuth'"}),"\n",(0,i.jsx)(t.h2,{id:"active-directory-ad--ldap",children:"Active Directory (AD) / LDAP"}),"\n",(0,i.jsx)(t.p,{children:"TAK Server can be configured to use an Active Directory or LDAP server to authenticate users, and assign groups. LDAP configuration for TAK Server varies depending on the configuration of the AD or LDAP server. Here is an example. Note that it contains credentials for a service account. This is required for group membership lookup using a client cert:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:'\n \n\n'})}),"\n",(0,i.jsx)(t.h2,{id:"configuring-ldap-through-web-interface",children:"Configuring LDAP Through Web Interface"}),"\n",(0,i.jsx)(t.p,{children:(0,i.jsx)(t.img,{alt:"Authentication Configuration Web Interface",src:n(8664).Z+"",width:"623",height:"242"})}),"\n",(0,i.jsx)(t.p,{children:'The LDAP configuration can be changed through an easy to use web page. To access this go to Configuration > Manage Security and Authentication. Under the Authentication heading will be the current LDAP configuration (the values will be empty if LDAP is not configured yet). Click on "Edit Authentication" to be directed to a form to enter desired LDAP settings. Note: Changes made here will only take effect after a server restart.'})]})}function l(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(d,{...e})}):d(e)}},8664:(e,t,n)=>{n.d(t,{Z:()=>i});const i=n.p+"assets/images/configure_ldap_web_interface-b413925576da0ff77673ac6fe0b48c61.png"},1151:(e,t,n)=>{n.d(t,{Z:()=>c,a:()=>r});var i=n(7294);const a={},o=i.createContext(a);function r(e){const t=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),i.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/21d68595.6f8e5ccd.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/21d68595.6f8e5ccd.js new file mode 100644 index 00000000..b8230e89 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/21d68595.6f8e5ccd.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8888],{3754:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>u,frontMatter:()=>s,metadata:()=>a,toc:()=>l});var r=n(5893),i=n(1151);const s={},o="Appendix E: Proper Use of Trusted CAs",a={id:"appendixe",title:"Appendix E: Proper Use of Trusted CAs",description:"TAK uses Mutual TLS (MTLS) authentication to establish secure communications channels between TAK clients and TAK Server. It's critical that deployments use a CA created by the TAK server scripts, or another private CA, to establish the root of trust. Failure to follow this guidance could result in exposing your deployment to a Man-In-The-Middle (MITM) attack.",source:"@site/docs/appendixe.md",sourceDirName:".",slug:"/appendixe",permalink:"/docs/appendixe",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/appendixe.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Appendix D: PostgreSQL TLS Configuration",permalink:"/docs/appendixd"}},c={},l=[];function d(e){const t={a:"a",code:"code",h1:"h1",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"appendix-e-proper-use-of-trusted-cas",children:"Appendix E: Proper Use of Trusted CAs"}),"\n",(0,r.jsxs)(t.p,{children:["TAK uses Mutual TLS (MTLS) authentication to establish secure communications channels between TAK clients and TAK Server. It's critical that deployments use a CA created by the TAK server scripts, or another private CA, to establish the root of trust. ",(0,r.jsx)(t.strong,{children:"Failure to follow this guidance could result in exposing your deployment to a Man-In-The-Middle (MITM) attack."})]}),"\n",(0,r.jsxs)(t.ul,{children:["\n",(0,r.jsx)(t.li,{children:"To ensure secure communications, it's critical that truststores deployed to TAK clients only contain CAs created by the TAK Server scripts or another private CA."}),"\n",(0,r.jsx)(t.li,{children:"There is never a need to add a LetsEncrypt, Digicert or any other public CA certificate to a truststore on a TAK client or TAK Server."}),"\n",(0,r.jsx)(t.li,{children:"When using Quick Connect, the LetsEncrypt or DigiCert server certificate should only be configured within your 8446 connector, and never within your configuration."}),"\n"]}),"\n",(0,r.jsxs)(t.p,{children:["Appendix B of the TAK Server Configuration Guide (see Downloadable Resources section here ",(0,r.jsx)(t.a,{href:"https://tak.gov/products/tak-server",children:"https://tak.gov/products/tak-server"}),") contains steps for creating a root CA to use in your TAK deployment. The makeRootCa.sh script creates a private key and self-signed certificate for the CA, and packages up the CA certificate within truststores that can be configured on TAK clients and on TAK Server."]}),"\n",(0,r.jsxs)(t.p,{children:["Appendix B contains additional steps for creating a client and server certificates, signed by the root CA. Appendix C describes TAK Server's Certificate Enrollment capability (",(0,r.jsx)(t.a,{href:"https://wiki.tak.gov/display/DEV/Certificate+Enrollment",children:"https://wiki.tak.gov/display/DEV/Certificate+Enrollment"}),") that automates the provisioning of client certificates. Users authenticate to the Certificate Enrollment endpoints with username/password, provide a CSR and receive a signed client certificate in return."]}),"\n",(0,r.jsx)(t.p,{children:"When enrolling using a server certificate from a self-signed TAK CA, clients must be bootstrapped with server's CA certificate prior to enrollment. To streamline provisioning of clients, TAK provides the Quick Connect feature that performs Certificate Enrollment using trust provided by LetsEncrypt or DigiCert CAs. TAK clients have embedded the LetsEncrypt and DigiCert CAs and will use these CAs when validating connections to the enrollment port. In this configuration, the TAK client will be able to leverage the embedded LetsEncrypt CA certificate, along with TLS hostname verification, to ensure the integrity of the connection."}),"\n",(0,r.jsx)(t.p,{children:"To configure Quick Connect, the TAK server admin adds the keystore, keystoreFile, and keystorePass attributes on the 8446 connector to use the trusted server cert for enrollment, as shown below."}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:'\t \n'})}),"\n",(0,r.jsx)(t.p,{children:"When using Quick Connect, you never have to do any configuration with LetsEncrypt or DigiCert CA itself. That is handled by embedding the CAs within the clients. The only reference to any key material from LetsEncrypt or DigiCert within your environment will be the server certificate contained in keystoreFile that's referenced by your 8446 connector."})]})}function u(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>o});var r=n(7294);const i={},s=r.createContext(i);function o(e){const t=r.useContext(s);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),r.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/24dd378c.aa6744bd.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/24dd378c.aa6744bd.js new file mode 100644 index 00000000..9f1c904d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/24dd378c.aa6744bd.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[191],{1187:(t,e,s)=>{s.r(e),s.d(e,{assets:()=>r,contentTitle:()=>a,default:()=>m,frontMatter:()=>o,metadata:()=>c,toc:()=>u});var n=s(5893),i=s(1151);const o={},a="Image Test",c={id:"imagetest",title:"Image Test",description:"This is just a section demonstrating images.",source:"@site/docs/imagetest.md",sourceDirName:".",slug:"/imagetest",permalink:"/docs/imagetest",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/imagetest.md",tags:[],version:"current",frontMatter:{}},r={},u=[];function d(t){const e={h1:"h1",img:"img",p:"p",...(0,i.a)(),...t.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(e.h1,{id:"image-test",children:"Image Test"}),"\n",(0,n.jsx)(e.p,{children:"This is just a section demonstrating images."}),"\n",(0,n.jsx)(e.p,{children:(0,n.jsx)(e.img,{alt:"Dog",src:s(2690).Z+"",width:"480",height:"480"})}),"\n",(0,n.jsx)(e.p,{children:"This is some text after the image."})]})}function m(t={}){const{wrapper:e}={...(0,i.a)(),...t.components};return e?(0,n.jsx)(e,{...t,children:(0,n.jsx)(d,{...t})}):d(t)}},2690:(t,e,s)=>{s.d(e,{Z:()=>n});const n=s.p+"assets/images/dog-818fa64239695f20feb4fd68b4396bd6.png"},1151:(t,e,s)=>{s.d(e,{Z:()=>c,a:()=>a});var n=s(7294);const i={},o=n.createContext(i);function a(t){const e=n.useContext(o);return n.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function c(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(i):t.components||i:a(t.components),n.createElement(o.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/25f60d21.02d9f023.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/25f60d21.02d9f023.js new file mode 100644 index 00000000..e8b3baed --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/25f60d21.02d9f023.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[278],{7822:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>s,contentTitle:()=>a,default:()=>u,frontMatter:()=>i,metadata:()=>c,toc:()=>d});var o=n(5893),r=n(1151);const i={},a="Make the Connection",c={id:"federation/maketheconnection",title:"Make the Connection",description:"Now that we have enabled federation and shared our CA with the other TAK server authority, we are ready to make the connection and start sharing. For this step, only ONE of the servers creates an outgoing connection to the other. If you are starting the connection, go back to the Manage Federates page where you enabled federation from step 1. You will now see three sections, Active Connections, Outgoing Connection Configuration, and Federate Configuration. To create an outgoing connection, click on the corresponding link, and enter in the address and port of the destination server. You can also pick the protocol version (make sure it is the right protocol for the port you are connecting to!), reconnection interval (how long between retries if the connection is lost), and whether or not the connection will be enabled on creation.",source:"@site/docs/federation/maketheconnection.md",sourceDirName:"federation",slug:"/federation/maketheconnection",permalink:"/docs/federation/maketheconnection",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/maketheconnection.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Upload Federate Certificate",permalink:"/docs/federation/uploadfederatecert"},next:{title:"Federated Group Mapping",permalink:"/docs/federation/federatedgroupmapping"}},s={},d=[];function h(e){const t={h1:"h1",img:"img",p:"p",...(0,r.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"make-the-connection",children:"Make the Connection"}),"\n",(0,o.jsx)(t.p,{children:"Now that we have enabled federation and shared our CA with the other TAK server authority, we are ready to make the connection and start sharing. For this step, only ONE of the servers creates an outgoing connection to the other. If you are starting the connection, go back to the Manage Federates page where you enabled federation from step 1. You will now see three sections, Active Connections, Outgoing Connection Configuration, and Federate Configuration. To create an outgoing connection, click on the corresponding link, and enter in the address and port of the destination server. You can also pick the protocol version (make sure it is the right protocol for the port you are connecting to!), reconnection interval (how long between retries if the connection is lost), and whether or not the connection will be enabled on creation."}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Active Connections",src:n(7272).Z+"",width:"1053",height:"609"})}),"\n",(0,o.jsx)(t.p,{children:"Now that you have created and started a connection, you will notice that no information is yet flowing between federates. This is because you and your fellow federate must specify which filtering groups you will allow to flow out of and into your server. To manage this, click on the Manage Groups link in the corresponding row of the Federate Configuration section. Here you can specify the groups, including the special __ANON__ group if you want. Once both servers have configured the groups, traffic will start to flow. A server restart is not necessary for these changes to take effect."}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Federate Groups",src:n(4807).Z+"",width:"1053",height:"742"})})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(h,{...e})}):h(e)}},7272:(e,t,n)=>{n.d(t,{Z:()=>o});const o=n.p+"assets/images/fed_3-f3c41c0b4d19555b0c4a046c4fda8d5b.png"},4807:(e,t,n)=>{n.d(t,{Z:()=>o});const o=n.p+"assets/images/fed_4-ac014207b1ba39bcf9cca69d8da9bd8f.png"},1151:(e,t,n)=>{n.d(t,{Z:()=>c,a:()=>a});var o=n(7294);const r={},i=o.createContext(r);function a(e){const t=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),o.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/2808364d.198e1c76.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/2808364d.198e1c76.js new file mode 100644 index 00000000..91ab9776 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/2808364d.198e1c76.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2981],{3868:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>u,contentTitle:()=>s,default:()=>l,frontMatter:()=>o,metadata:()=>r,toc:()=>c});var i=n(5893),a=n(1151);const o={},s="OAuth2 Authentication",r={id:"oath2authentication",title:"OAuth2 Authentication",description:"TAK Server provides OAuth2 Authorization and Resource server capabilities using the OAuth2 Password authentication flow. OAuth2 integration works with existing authentication back ends, allowing TAK Server to issue tokens backed by the File or LDAP authenticators. TAK Server issues JSON Web Tokens (JWT) signed by the server certificate, allowing external systems to validate tokens against the server\u2019s trust chain. The OAuth2 token endpoint is available at https8446/oauth/token.",source:"@site/docs/oath2authentication.md",sourceDirName:".",slug:"/oath2authentication",permalink:"/docs/oath2authentication",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/oath2authentication.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Group Filtering for Multicast Networks",permalink:"/docs/groupfilteringformulticast"},next:{title:"User Management UI",permalink:"/docs/usermanagementui"}},u={},c=[];function h(t){const e={h1:"h1",p:"p",...(0,a.a)(),...t.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(e.h1,{id:"oauth2-authentication",children:"OAuth2 Authentication"}),"\n",(0,i.jsx)(e.p,{children:"TAK Server provides OAuth2 Authorization and Resource server capabilities using the OAuth2 Password authentication flow. OAuth2 integration works with existing authentication back ends, allowing TAK Server to issue tokens backed by the File or LDAP authenticators. TAK Server issues JSON Web Tokens (JWT) signed by the server certificate, allowing external systems to validate tokens against the server\u2019s trust chain. The OAuth2 token endpoint is available at https://:8446/oauth/token."})]})}function l(t={}){const{wrapper:e}={...(0,a.a)(),...t.components};return e?(0,i.jsx)(e,{...t,children:(0,i.jsx)(h,{...t})}):h(t)}},1151:(t,e,n)=>{n.d(e,{Z:()=>r,a:()=>s});var i=n(7294);const a={},o=i.createContext(a);function s(t){const e=i.useContext(o);return i.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function r(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(a):t.components||a:s(t.components),i.createElement(o.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/2c26bb54.05cf3577.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/2c26bb54.05cf3577.js new file mode 100644 index 00000000..037cd1d1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/2c26bb54.05cf3577.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[308],{1537:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>a,contentTitle:()=>c,default:()=>p,frontMatter:()=>i,metadata:()=>o,toc:()=>d});var s=n(5893),r=n(1151);const i={},c="WebTAK",o={id:"webtak",title:"WebTAK",description:"The WebTAK front-end application is bundled with TAK Server. The WebTAK back-end WebSockets networking channel and APIs are provided by TAK Server. WebTAK must be accessed using https.",source:"@site/docs/webtak.md",sourceDirName:".",slug:"/webtak",permalink:"/docs/webtak",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/webtak.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"VBM Admin Configuration",permalink:"/docs/configuration/vbmadminconfig"},next:{title:"Device Profiles",permalink:"/docs/deviceprofiles"}},a={},d=[];function u(e){const t={h1:"h1",p:"p",strong:"strong",...(0,r.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"webtak",children:"WebTAK"}),"\n",(0,s.jsx)(t.p,{children:"The WebTAK front-end application is bundled with TAK Server. The WebTAK back-end WebSockets networking channel and APIs are provided by TAK Server. WebTAK must be accessed using https."}),"\n",(0,s.jsx)(t.p,{children:"WebTAK can be accessed either with X.509 client certificates (default https port 8443), or by username-password access using OAuth (https port 8446)."}),"\n",(0,s.jsx)(t.p,{children:"In either case, TAK Server must be configured with a server certificate and truststore (see Appendix B)."}),"\n",(0,s.jsxs)(t.p,{children:["In the Admin UI menu, use ",(0,s.jsx)(t.strong,{children:"Situation Awareness -> WebTAK"})," to access WebTAK."]})]})}function p(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>o,a:()=>c});var s=n(7294);const r={},i=s.createContext(r);function c(e){const t=s.useContext(i);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),s.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/30688810.40bb9634.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/30688810.40bb9634.js new file mode 100644 index 00000000..9c2fa9fb --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/30688810.40bb9634.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6317],{5633:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>c,contentTitle:()=>r,default:()=>u,frontMatter:()=>i,metadata:()=>s,toc:()=>l});var o=n(5893),a=n(1151);const i={},r="Data Retention Tool",s={id:"dataretentiontool",title:"Data Retention Tool",description:"Information regarding the use of the Data Retention Tool is available on the tak.gov wiki:",source:"@site/docs/dataretentiontool.md",sourceDirName:".",slug:"/dataretentiontool",permalink:"/docs/dataretentiontool",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/dataretentiontool.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"User Management UI",permalink:"/docs/usermanagementui"},next:{title:"Appendix A: Acronyms and Abbreviations",permalink:"/docs/appendixa"}},c={},l=[];function d(t){const e={a:"a",h1:"h1",p:"p",...(0,a.a)(),...t.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(e.h1,{id:"data-retention-tool",children:"Data Retention Tool"}),"\n",(0,o.jsx)(e.p,{children:"Information regarding the use of the Data Retention Tool is available on the tak.gov wiki:"}),"\n",(0,o.jsx)(e.p,{children:(0,o.jsx)(e.a,{href:"https://wiki.tak.gov/display/TPC/Data+Retention+Tool",children:"https://wiki.tak.gov/display/TPC/Data+Retention+Tool"})})]})}function u(t={}){const{wrapper:e}={...(0,a.a)(),...t.components};return e?(0,o.jsx)(e,{...t,children:(0,o.jsx)(d,{...t})}):d(t)}},1151:(t,e,n)=>{n.d(e,{Z:()=>s,a:()=>r});var o=n(7294);const a={},i=o.createContext(a);function r(t){const e=o.useContext(i);return o.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function s(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(a):t.components||a:r(t.components),o.createElement(i.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/30a24c52.a606a441.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/30a24c52.a606a441.js new file mode 100644 index 00000000..e832f0df --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/30a24c52.a606a441.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[453],{8605:s=>{s.exports=JSON.parse('{"label":"hello","permalink":"/blog/tags/hello","allTagsPath":"/blog/tags","count":3,"unlisted":false}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3262a34b.2fa47d2f.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3262a34b.2fa47d2f.js new file mode 100644 index 00000000..7ac951bf --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3262a34b.2fa47d2f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8444],{7648:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>u,contentTitle:()=>c,default:()=>f,frontMatter:()=>o,metadata:()=>s,toc:()=>a});var n=i(5893),r=i(1151);const o={},c="Configuring Security Through Web UI (Certificates/TLS)",s={id:"configuration/configurewebui",title:"Configuring Security Through Web UI (Certificates/TLS)",description:"Security Configuration",source:"@site/docs/configuration/configurewebui.md",sourceDirName:"configuration",slug:"/configuration/configurewebui",permalink:"/docs/configuration/configurewebui",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/configurewebui.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Overview",permalink:"/docs/configuration/overview"},next:{title:"Group Filtering",permalink:"/docs/configuration/groupfiltering"}},u={},a=[];function g(e){const t={em:"em",h1:"h1",img:"img",p:"p",strong:"strong",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"configuring-security-through-web-ui-certificatestls",children:"Configuring Security Through Web UI (Certificates/TLS)"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Security Configuration",src:i(7670).Z+"",width:"835",height:"198"})}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.strong,{children:"Security Configuration Web interface"})}),"\n",(0,n.jsx)(t.p,{children:'Security and authentication options for TAK Server can be set up using a web interface. To access this page in the menu bar go to Configuration > Manage Security and Authentication Configuration. This page will contain both Security and Authentication configuration current values. To modify the Security Configuration click "Edit Security". This will allow changes to the server\'s certificates, the version of TLS used, x509 Groups settings and x509 Add Anonymous settings.'}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.em,{children:"Note: Changes made here will only take effect after a server restart."})})]})}function f(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(g,{...e})}):g(e)}},7670:(e,t,i)=>{i.d(t,{Z:()=>n});const n=i.p+"assets/images/security_config_1-9e672c027ac08f2876117933ce923339.png"},1151:(e,t,i)=>{i.d(t,{Z:()=>s,a:()=>c});var n=i(7294);const r={},o=n.createContext(r);function c(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:c(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3660216b.15fd4814.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3660216b.15fd4814.js new file mode 100644 index 00000000..0045ae50 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3660216b.15fd4814.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7922],{1530:(t,a,e)=>{e.r(a),e.d(a,{assets:()=>r,contentTitle:()=>u,default:()=>m,frontMatter:()=>c,metadata:()=>i,toc:()=>n});var s=e(5893),o=e(1151);const c={tags:["hello","docusaurus-static"],authors:{name:"OctoSpacc",title:"Chief Executive Officer @ Spacc Inc.",url:"https://hub.octt.eu.org",image_url:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}},u="Welcome to Docusaurus-Static!",i={permalink:"/blog/2024/01/28/welcome-to-docusaurus-static",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2024-01-28-welcome-to-docusaurus-static.md",source:"@site/blog/2024-01-28-welcome-to-docusaurus-static.md",title:"Welcome to Docusaurus-Static!",description:"Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.",date:"2024-01-28T00:00:00.000Z",formattedDate:"January 28, 2024",tags:[{label:"hello",permalink:"/blog/tags/hello"},{label:"docusaurus-static",permalink:"/blog/tags/docusaurus-static"}],readingTime:.38,hasTruncateMarker:!1,authors:[{name:"OctoSpacc",title:"Chief Executive Officer @ Spacc Inc.",url:"https://hub.octt.eu.org",image_url:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png",imageURL:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}],frontMatter:{tags:["hello","docusaurus-static"],authors:{name:"OctoSpacc",title:"Chief Executive Officer @ Spacc Inc.",url:"https://hub.octt.eu.org",image_url:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png",imageURL:"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}},unlisted:!1,nextItem:{title:"Welcome to Docusaurus!",permalink:"/blog/welcome"}},r={authorsImageUrls:[void 0]},n=[];function l(t){const a={code:"code",p:"p",...(0,o.a)(),...t.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(a.p,{children:"Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server."}),"\n",(0,s.jsxs)(a.p,{children:["Not only can the built site be navigated as a collection of static HTML pages from ",(0,s.jsx)(a.code,{children:"file:///"})," locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening ",(0,s.jsx)("a",{href:"/docusaurus-static-single-file.html",children:"docusaurus-static-single-file.html"}),"..."]})]})}function m(t={}){const{wrapper:a}={...(0,o.a)(),...t.components};return a?(0,s.jsx)(a,{...t,children:(0,s.jsx)(l,{...t})}):l(t)}},1151:(t,a,e)=>{e.d(a,{Z:()=>i,a:()=>u});var s=e(7294);const o={},c=s.createContext(o);function u(t){const a=s.useContext(c);return s.useMemo((function(){return"function"==typeof t?t(a):{...a,...t}}),[a,t])}function i(t){let a;return a=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:u(t.components),s.createElement(c.Provider,{value:a},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3682177e.ee1f77aa.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3682177e.ee1f77aa.js new file mode 100644 index 00000000..023cc5b1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3682177e.ee1f77aa.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2537],{5558:(e,i,t)=>{t.r(i),t.d(i,{assets:()=>a,contentTitle:()=>s,default:()=>f,frontMatter:()=>r,metadata:()=>c,toc:()=>l});var n=t(5893),o=t(1151);const r={},s="Device Profiles",c={id:"deviceprofiles",title:"Device Profiles",description:"TAK Server can now assist in provisioning ATAK devices through Device Profiles. The Device Profile Manager (under Administrative Menu, Device Profiles) allows administrators to build profiles that can be applied to clients when enrolling for certificates, and when connecting to TAK server. The Profile consists of a sets of files, which can include settings and data in any file format that is supported by TAK Mission Packages. Profiles can be made public or restricted to individual Groups.",source:"@site/docs/deviceprofiles.md",sourceDirName:".",slug:"/deviceprofiles",permalink:"/docs/deviceprofiles",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/deviceprofiles.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"WebTAK",permalink:"/docs/webtak"},next:{title:"Overview",permalink:"/docs/federation/overview"}},a={},l=[];function d(e){const i={h1:"h1",p:"p",...(0,o.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(i.h1,{id:"device-profiles",children:"Device Profiles"}),"\n",(0,n.jsx)(i.p,{children:"TAK Server can now assist in provisioning ATAK devices through Device Profiles. The Device Profile Manager (under Administrative Menu, Device Profiles) allows administrators to build profiles that can be applied to clients when enrolling for certificates, and when connecting to TAK server. The Profile consists of a sets of files, which can include settings and data in any file format that is supported by TAK Mission Packages. Profiles can be made public or restricted to individual Groups."}),"\n",(0,n.jsx)(i.p,{children:"When an ATAK device enrolls for client certificate, or optionally after connecting to TAK server, TAK server will return all profiles that need to be applied to the device. The TAK server administrator can also push a profile to a connected user by clicking the Send link within the Device Profile Manager."})]})}function f(e={}){const{wrapper:i}={...(0,o.a)(),...e.components};return i?(0,n.jsx)(i,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,i,t)=>{t.d(i,{Z:()=>c,a:()=>s});var n=t(7294);const o={},r=n.createContext(o);function s(e){const i=n.useContext(r);return n.useMemo((function(){return"function"==typeof e?e(i):{...i,...e}}),[i,e])}function c(e){let i;return i=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),n.createElement(r.Provider,{value:i},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/393be207.08ff365d.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/393be207.08ff365d.js new file mode 100644 index 00000000..6747347a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/393be207.08ff365d.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7414],{1181:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>p,contentTitle:()=>s,default:()=>u,frontMatter:()=>r,metadata:()=>c,toc:()=>d});var a=n(5893),o=n(1151);const r={title:"Markdown page example"},s="Markdown page example",c={type:"mdx",permalink:"/markdown-page",source:"@site/src/pages/markdown-page.md",title:"Markdown page example",description:"You don't need React to write simple standalone pages.",frontMatter:{title:"Markdown page example"},unlisted:!1},p={},d=[];function i(e){const t={h1:"h1",p:"p",...(0,o.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.h1,{id:"markdown-page-example",children:"Markdown page example"}),"\n",(0,a.jsx)(t.p,{children:"You don't need React to write simple standalone pages."})]})}function u(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(i,{...e})}):i(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>c,a:()=>s});var a=n(7294);const o={},r=a.createContext(o);function s(e){const t=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),a.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3abe8fb9.a5b4885a.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3abe8fb9.a5b4885a.js new file mode 100644 index 00000000..a510337c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3abe8fb9.a5b4885a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[5521],{3438:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>l,contentTitle:()=>i,default:()=>g,frontMatter:()=>o,metadata:()=>a,toc:()=>c});var s=n(5893),r=n(1151);const o={},i="Logging",a={id:"logging",title:"Logging",description:"TAK Server provides several log files to provide information about relevant events that happen during execution. The log files are located in the /opt/tak/logs directory. This table shows the name of the log files, and their function.",source:"@site/docs/logging.md",sourceDirName:".",slug:"/logging",permalink:"/docs/logging",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/logging.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Metrics",permalink:"/docs/metrics"},next:{title:"Group Filtering for Multicast Networks",permalink:"/docs/groupfilteringformulticast"}},l={},c=[];function d(e){const t={h1:"h1",p:"p",table:"table",tbody:"tbody",td:"td",th:"th",thead:"thead",tr:"tr",...(0,r.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"logging",children:"Logging"}),"\n",(0,s.jsx)(t.p,{children:"TAK Server provides several log files to provide information about relevant events that happen during execution. The log files are located in the /opt/tak/logs directory. This table shows the name of the log files, and their function."}),"\n",(0,s.jsxs)(t.table,{children:[(0,s.jsx)(t.thead,{children:(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.th,{children:"Name of Log File"}),(0,s.jsx)(t.th,{children:"Purpose"})]})}),(0,s.jsxs)(t.tbody,{children:[(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"takserver-messaging.log"}),(0,s.jsx)(t.td,{children:"Execution-level information about the messaging process, including client connection events, error messages and warnings."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"takserver-api.log"}),(0,s.jsx)(t.td,{children:"Execution-level information about the API process, including error messages and warnings."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"takserver-messaging-console.log"}),(0,s.jsx)(t.td,{children:"Java Virtual Machine (JVM) informational messages and errors, for the messaging process."})]}),(0,s.jsxs)(t.tr,{children:[(0,s.jsx)(t.td,{children:"takserver-api-console.log"}),(0,s.jsx)(t.td,{children:"Java Virtual Machine (JVM) informational messages and errors, for the API process."})]})]})]})]})}function g(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(d,{...e})}):d(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>i});var s=n(7294);const r={},o=s.createContext(r);function i(e){const t=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),s.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3d8d21df.4c91e48b.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3d8d21df.4c91e48b.js new file mode 100644 index 00000000..70c56b7c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3d8d21df.4c91e48b.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6535],{4982:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>l,frontMatter:()=>a,metadata:()=>i,toc:()=>d});var o=n(5893),r=n(1151);const a={},s="About TAK Server",i={id:"about",title:"About TAK Server",description:"TAK Server is a situational awareness server, that provides a dynamic Common Operating Picture to users of the Team Awareness Kit, including ATAK (Android), WinTAK (Windows) and WebTAK. TAK enables sharing of geolocated information in real time for military forces, law enforcement, and emergency responders. It supports both wireless and wired networks, as well as cloud and data center deployment.",source:"@site/docs/about.md",sourceDirName:".",slug:"/about",permalink:"/docs/about",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/about.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",next:{title:"Change Log",permalink:"/docs/changelog"}},c={},d=[];function u(e){const t={h1:"h1",p:"p",...(0,r.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"about-tak-server",children:"About TAK Server"}),"\n",(0,o.jsx)(t.p,{children:"TAK Server is a situational awareness server, that provides a dynamic Common Operating Picture to users of the Team Awareness Kit, including ATAK (Android), WinTAK (Windows) and WebTAK. TAK enables sharing of geolocated information in real time for military forces, law enforcement, and emergency responders. It supports both wireless and wired networks, as well as cloud and data center deployment."})]})}function l(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(u,{...e})}):u(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>i,a:()=>s});var o=n(7294);const r={},a=o.createContext(r);function s(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3e56fa0f.954e0ae9.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3e56fa0f.954e0ae9.js new file mode 100644 index 00000000..b24fde94 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/3e56fa0f.954e0ae9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6425],{3650:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>d,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var r=n(5893),s=n(1151);const o={},i="Prerequisites",a={id:"installation/oneserver/prerequisite",title:"Prerequisites",description:"Start with a fresh install of a supported OS. For AWS / cloud installation, see recommended instance type on page 5. An OS install with a GUI is recommended, so that a web browser can be run locally to configure TAK Server.",source:"@site/docs/installation/oneserver/prerequisite.md",sourceDirName:"installation/oneserver",slug:"/installation/oneserver/prerequisite",permalink:"/docs/installation/oneserver/prerequisite",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/oneserver/prerequisite.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Overview and Installer Files",permalink:"/docs/installation/overview"},next:{title:"TAK Server Installation",permalink:"/docs/installation/oneserver/takserverinstallation"}},c={},l=[];function u(e){const t={code:"code",h1:"h1",p:"p",pre:"pre",...(0,s.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,r.jsx)(t.p,{children:"Start with a fresh install of a supported OS. For AWS / cloud installation, see recommended instance type on page 5. An OS install with a GUI is recommended, so that a web browser can be run locally to configure TAK Server."}),"\n",(0,r.jsx)(t.p,{children:"Increase system limit for number of concurrent TCP connections (do once):"}),"\n",(0,r.jsx)(t.pre,{children:(0,r.jsx)(t.code,{className:"language-bash",children:'echo -e "* soft nofile 32768\\n* hard nofile 32768" | sudo tee --append /etc/security/limits.conf > /dev/null\n'})})]})}function d(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(u,{...e})}):u(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>i});var r=n(7294);const s={},o=r.createContext(s);function i(e){const t=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),r.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4604b011.2ac8481c.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4604b011.2ac8481c.js new file mode 100644 index 00000000..1125231b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4604b011.2ac8481c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9647],{9019:s=>{s.exports=JSON.parse('{"permalink":"/blog/tags/docusaurus-static","page":1,"postsPerPage":10,"totalPages":1,"totalCount":1,"blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/46bbbc4b.bb245aaf.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/46bbbc4b.bb245aaf.js new file mode 100644 index 00000000..ef6bea13 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/46bbbc4b.bb245aaf.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3310],{3769:s=>{s.exports=JSON.parse('{"name":"docusaurus-plugin-content-docs","id":"default"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4a298a69.19965ada.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4a298a69.19965ada.js new file mode 100644 index 00000000..39edb7dd --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4a298a69.19965ada.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6203],{119:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>i,contentTitle:()=>o,default:()=>u,frontMatter:()=>a,metadata:()=>l,toc:()=>d});var t=n(5893),r=n(1151);const a={},o="Dependency Setup",l={id:"installation/twoserver/serverone/dependencysetup",title:"Dependency Setup",description:"First, update firewall rules to allow communication with server two, for TCP port 5432.",source:"@site/docs/installation/twoserver/serverone/dependencysetup.md",sourceDirName:"installation/twoserver/serverone",slug:"/installation/twoserver/serverone/dependencysetup",permalink:"/docs/installation/twoserver/serverone/dependencysetup",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/twoserver/serverone/dependencysetup.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Overview",permalink:"/docs/installation/twoserver/overview"},next:{title:"Prerequisites",permalink:"/docs/installation/twoserver/servertwo/prerequisites"}},i={},d=[{value:"Rocky Linux 8",id:"rocky-linux-8",level:2},{value:"RHEL 8",id:"rhel-8",level:2},{value:"RHEL 7",id:"rhel-7",level:2},{value:"Ubuntu & Raspberry Pi OS",id:"ubuntu--raspberry-pi-os",level:2},{value:"CentOS 7",id:"centos-7",level:2}];function p(e){const s={a:"a",code:"code",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,r.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.h1,{id:"dependency-setup",children:"Dependency Setup"}),"\n",(0,t.jsx)(s.p,{children:"First, update firewall rules to allow communication with server two, for TCP port 5432."}),"\n",(0,t.jsx)(s.h2,{id:"rocky-linux-8",children:"Rocky Linux 8"}),"\n",(0,t.jsx)(s.p,{children:"Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Enable the 'powertools' repo for postgis dependencies. Install TAK Server RPM database and its dependencies."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.strong,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf install epel-release -y\n\nsudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\n\nsudo dnf update -y\n\nsudo dnf module disable postgresql\n\nsudo dnf install java-17-openjdk-devel -y\n\nsudo dnf config-manager --set-enabled powertools\n\nMake sure the database RPM is in the current directory\n\nsudo dnf install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"rhel-8",children:"RHEL 8"}),"\n",(0,t.jsx)(s.p,{children:"Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.strong,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y\nsudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\n"})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y\nsudo dnf module disable postgresql\nsudo subscription-manager config --rhsm.manage_repos=1\nsudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms\n"})}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsx)(s.strong,{children:'Note: If you get the error \u2018This system has no repositories available through subscriptions\u2019, you need to subscribe your system with "sudo subscription-manager register --username --password --auto-attach"'})}),"\n",(0,t.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"rhel-7",children:"RHEL 7"}),"\n",(0,t.jsx)(s.p,{children:"Setup the extra postgres yum repo for the latest postgres and postgis. Install OpenJDK 17 and other dependencies. Install TAK Server RPM database."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.strong,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\n\nsudo yum update -y\n\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\n"})}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsx)(s.strong,{children:"Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."})}),"\n",(0,t.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo rpm -ivh takserver-database-5.2-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"ubuntu--raspberry-pi-os",children:"Ubuntu & Raspberry Pi OS"}),"\n",(0,t.jsx)(s.p,{children:"Install the postgres repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install TAK Server database. Use database DEB. Configure TAK Server Database installation"}),"\n",(0,t.jsxs)(s.p,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo mkdir -p /etc/apt/keyrings\n\nsudo curl https://www.postgresql.org/media/keys/ACCC4CF8.asc --output /etc/apt/keyrings/postgresql.asc\n\nsudo sh -c 'echo \"deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main\" > /etc/apt/sources.list.d/postgresql.list'\n\nsudo apt update\n\nsudo apt install takserver-database-5.2-RELEASEx_all.deb\n"})}),"\n",(0,t.jsx)(s.p,{children:"Open the file /opt/tak/CoreConfig.example.xml and look for the auto-generated password for the database. This password will be used to configure the Core Server."}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:'\n'})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"centos-7",children:"CentOS 7"}),"\n",(0,t.jsx)(s.p,{children:"Setup the extra postgres yum repo for the latest postgres and postgis. Install OpenJDK 17 and other dependencies. Install TAK Server RPM database."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.strong,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo yum install epel-release -y\n\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\n\nsudo yum update -y\n\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\n"})}),"\n",(0,t.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,t.jsx)(s.p,{children:"Make sure the database RPM is in the current directory"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo yum install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})})]})}function u(e={}){const{wrapper:s}={...(0,r.a)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(p,{...e})}):p(e)}},1151:(e,s,n)=>{n.d(s,{Z:()=>l,a:()=>o});var t=n(7294);const r={},a=t.createContext(r);function o(e){const s=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),t.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4b4e9c91.38590bd2.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4b4e9c91.38590bd2.js new file mode 100644 index 00000000..cb72bb34 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4b4e9c91.38590bd2.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2914],{7179:(e,s,r)=>{r.r(s),r.d(s,{assets:()=>c,contentTitle:()=>o,default:()=>l,frontMatter:()=>i,metadata:()=>a,toc:()=>u});var t=r(5893),n=r(1151);const i={},o="Server Requirements",a={id:"system-requirements/serverrequirements",title:"Server Requirements",description:"- 4 processor cores",source:"@site/docs/system-requirements/serverrequirements.md",sourceDirName:"system-requirements",slug:"/system-requirements/serverrequirements",permalink:"/docs/system-requirements/serverrequirements",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/system-requirements/serverrequirements.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Supported Operating Systems",permalink:"/docs/system-requirements/systemrequirements"},next:{title:"AWS / GovCloud Recommended Instance Type",permalink:"/docs/system-requirements/awsrequirements"}},c={},u=[];function m(e){const s={h1:"h1",li:"li",p:"p",strong:"strong",ul:"ul",...(0,n.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.h1,{id:"server-requirements",children:"Server Requirements"}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsx)(s.li,{children:"4 processor cores"}),"\n",(0,t.jsx)(s.li,{children:"8 GB RAM"}),"\n",(0,t.jsx)(s.li,{children:"40 GB disk storage"}),"\n"]}),"\n",(0,t.jsx)(s.p,{children:"For Raspberry Pi installations, a Pi 4, Model B, Quad-Core 64-bit 8GB RAM version is recommended for a minimal TAK Server setup (TAK Server messaging and api services with local PostgreSQL database)"}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsx)(s.strong,{children:"NOTE: Insecure ports are a potential security risk and may allow attackers to gain access to the system resulting in the disclosure of personal and sensitive information. Use of unencrypted ports should be avoided to ensure a secure TAK Server deployment."})})]})}function l(e={}){const{wrapper:s}={...(0,n.a)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(m,{...e})}):m(e)}},1151:(e,s,r)=>{r.d(s,{Z:()=>a,a:()=>o});var t=r(7294);const n={},i=t.createContext(n);function o(e){const s=t.useContext(i);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function a(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:o(e.components),t.createElement(i.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4c9e35b1.2f0ea90f.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4c9e35b1.2f0ea90f.js new file mode 100644 index 00000000..4a2e8469 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4c9e35b1.2f0ea90f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9035],{499:s=>{s.exports=JSON.parse('{"permalink":"/blog/tags/hola","page":1,"postsPerPage":10,"totalPages":1,"totalCount":1,"blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4cd5e786.bca5b41a.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4cd5e786.bca5b41a.js new file mode 100644 index 00000000..8e062e80 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4cd5e786.bca5b41a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[4020],{5745:s=>{s.exports=JSON.parse('{"name":"docusaurus-plugin-content-pages","id":"default"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4e810807.1baa3bb8.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4e810807.1baa3bb8.js new file mode 100644 index 00000000..0f9b57d1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/4e810807.1baa3bb8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9349],{5901:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>n,default:()=>h,frontMatter:()=>s,metadata:()=>i,toc:()=>d});var o=r(5893),a=r(1151);const s={},n="Upload Federate Certificate",i={id:"federation/uploadfederatecert",title:"Upload Federate Certificate",description:"In order for the federate servers to trust each other and their ATAK clients, they must share each others certificate authorities (CAs) in order to create a separate federate trust-store. One of the key components to how TAK Server satisfies all the restrictions is that we use one trust-store for local users, and one for Federates. The trust-store contains all the valid CAs that you will allow client certificates from. By having separate trust-stores, we can have the Federation channels allow connections with certificates from \u201coutside\u201d CAs, while not allowing ATAKs with certs from those \u201coutside\u201d CAs to make direct connections to our server.",source:"@site/docs/federation/uploadfederatecert.md",sourceDirName:"federation",slug:"/federation/uploadfederatecert",permalink:"/docs/federation/uploadfederatecert",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/uploadfederatecert.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Enable Federation",permalink:"/docs/federation/enablefederation"},next:{title:"Make the Connection",permalink:"/docs/federation/maketheconnection"}},c={},d=[];function l(e){const t={h1:"h1",img:"img",p:"p",...(0,a.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"upload-federate-certificate",children:"Upload Federate Certificate"}),"\n",(0,o.jsx)(t.p,{children:"In order for the federate servers to trust each other and their ATAK clients, they must share each others certificate authorities (CAs) in order to create a separate federate trust-store. One of the key components to how TAK Server satisfies all the restrictions is that we use one trust-store for local users, and one for Federates. The trust-store contains all the valid CAs that you will allow client certificates from. By having separate trust-stores, we can have the Federation channels allow connections with certificates from \u201coutside\u201d CAs, while not allowing ATAKs with certs from those \u201coutside\u201d CAs to make direct connections to our server."}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Federate Certificate Authorities Page",src:r(4010).Z+"",width:"1053",height:"321"})}),"\n",(0,o.jsx)(t.p,{children:"Generally, we share the public CA, which you can find at /opt/tak/cert/files/ca.pem, via some third channel such as email or a USB drive. Once you have traded CAs, go the the Manage Federate Certifate Authorities page and upload the CA of the federate you want to connect to."})]})}function h(e={}){const{wrapper:t}={...(0,a.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},4010:(e,t,r)=>{r.d(t,{Z:()=>o});const o=r.p+"assets/images/fed_2-6f47952affff8c0295409c5c4035b9e5.png"},1151:(e,t,r)=>{r.d(t,{Z:()=>i,a:()=>n});var o=r(7294);const a={},s=o.createContext(a);function n(e){const t=o.useContext(s);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function i(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:n(e.components),o.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/520e22a5.413648fc.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/520e22a5.413648fc.js new file mode 100644 index 00000000..527dfb6c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/520e22a5.413648fc.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9713],{2022:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>d,frontMatter:()=>o,metadata:()=>a,toc:()=>l});var s=r(5893),n=r(1151);const o={},i="Prerequisites",a={id:"installation/twoserver/servertwo/prerequisites",title:"Prerequisites",description:"Start with a fresh install of a supported OS. An install with a GUI is recommended, so that a web browser can be run locally to configure TAK Server.",source:"@site/docs/installation/twoserver/servertwo/prerequisites.md",sourceDirName:"installation/twoserver/servertwo",slug:"/installation/twoserver/servertwo/prerequisites",permalink:"/docs/installation/twoserver/servertwo/prerequisites",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/twoserver/servertwo/prerequisites.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Dependency Setup",permalink:"/docs/installation/twoserver/serverone/dependencysetup"},next:{title:"Install TAK Server",permalink:"/docs/installation/twoserver/servertwo/installtakserver"}},c={},l=[];function u(e){const t={code:"code",h1:"h1",p:"p",pre:"pre",...(0,n.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(t.h1,{id:"prerequisites",children:"Prerequisites"}),"\n",(0,s.jsx)(t.p,{children:"Start with a fresh install of a supported OS. An install with a GUI is recommended, so that a web browser can be run locally to configure TAK Server."}),"\n",(0,s.jsx)(t.p,{children:"Increase system limit for number of concurrent TCP connections (do once)"}),"\n",(0,s.jsx)(t.pre,{children:(0,s.jsx)(t.code,{className:"language-bash",children:'echo -e "* soft nofile 32768\\n* hard nofile 32768" | sudo tee --append /etc/security/limits.conf > /dev/null\n'})})]})}function d(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,s.jsx)(t,{...e,children:(0,s.jsx)(u,{...e})}):u(e)}},1151:(e,t,r)=>{r.d(t,{Z:()=>a,a:()=>i});var s=r(7294);const n={},o=s.createContext(n);function i(e){const t=s.useContext(o);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:i(e.components),s.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/55d8b2aa.ac42e502.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/55d8b2aa.ac42e502.js new file mode 100644 index 00000000..d281e5ca --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/55d8b2aa.ac42e502.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1506],{6863:(e,t,o)=>{o.r(t),o.d(t,{assets:()=>l,contentTitle:()=>s,default:()=>u,frontMatter:()=>i,metadata:()=>a,toc:()=>c});var r=o(5893),n=o(1151);const i={},s="Overview",a={id:"firewall/overview",title:"Overview",description:"One of the most common problems people have is the system default firewall blocking their traffic.",source:"@site/docs/firewall/overview.md",sourceDirName:"firewall",slug:"/firewall/overview",permalink:"/docs/firewall/overview",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/firewall/overview.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Building and Installing Container Images Using Docker",permalink:"/docs/dockerinstall/buildinstall"},next:{title:"RHEL, Rocky Linux, and CentOS",permalink:"/docs/firewall/rhelrockycentos"}},l={},c=[];function d(e){const t={a:"a",h1:"h1",p:"p",...(0,n.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(t.h1,{id:"overview",children:"Overview"}),"\n",(0,r.jsx)(t.p,{children:"One of the most common problems people have is the system default firewall blocking their traffic."}),"\n",(0,r.jsxs)(t.p,{children:["The full procedure for configuring the firewall is complex and beyond the scope of this guide, and is an important concern for system configuration. Consult your network administrator and/or the firewalld documentation at ",(0,r.jsx)(t.a,{href:"https://fedoraproject.org/wiki/FirewallD",children:"https://fedoraproject.org/wiki/FirewallD"}),"."]}),"\n",(0,r.jsx)(t.p,{children:"The following tips will get you started for lab/field environments."})]})}function u(e={}){const{wrapper:t}={...(0,n.a)(),...e.components};return t?(0,r.jsx)(t,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},1151:(e,t,o)=>{o.d(t,{Z:()=>a,a:()=>s});var r=o(7294);const n={},i=r.createContext(n);function s(e){const t=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:s(e.components),r.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5817ad8d.eb9b0de6.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5817ad8d.eb9b0de6.js new file mode 100644 index 00000000..7ef5d25a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5817ad8d.eb9b0de6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[5582],{4446:(e,r,n)=>{n.r(r),n.d(r,{assets:()=>o,contentTitle:()=>i,default:()=>h,frontMatter:()=>s,metadata:()=>c,toc:()=>d});var t=n(5893),a=n(1151);const s={},i="Building and Installing Container Images Using Docker",c={id:"dockerinstall/buildinstall",title:"Building and Installing Container Images Using Docker",description:"TAK Server can be installed using docker. Start by downloading container images from tak.gov. You will need the docker release which comes as a zip file called 'takserver-docker-\\.zip'.",source:"@site/docs/dockerinstall/buildinstall.md",sourceDirName:"dockerinstall",slug:"/dockerinstall/buildinstall",permalink:"/docs/dockerinstall/buildinstall",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/dockerinstall/buildinstall.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"IronBank",permalink:"/docs/dockerinstall/ironbank"},next:{title:"Overview",permalink:"/docs/firewall/overview"}},o={},d=[];function l(e){const r={a:"a",code:"code",em:"em",h1:"h1",li:"li",ol:"ol",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,a.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(r.h1,{id:"building-and-installing-container-images-using-docker",children:"Building and Installing Container Images Using Docker"}),"\n",(0,t.jsx)(r.p,{children:"TAK Server can be installed using docker. Start by downloading container images from tak.gov. You will need the docker release which comes as a zip file called 'takserver-docker-.zip'."}),"\n",(0,t.jsxs)(r.p,{children:["If you using CentOS 7, follow these instructions first to install docker, start the docker daemon and use it as a regular user:\n",(0,t.jsx)(r.a,{href:"https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-centos-7",children:"https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-centos-7"})]}),"\n",(0,t.jsxs)(r.p,{children:["Next, unzip the docker zip file. ",(0,t.jsx)(r.strong,{children:"All further commands should be run from this top level 'takserver-docker-' directory."})," If you are familiar with the rpm install, the 'tak' folder within the 'takserver-docker-' directory represents the '/opt/tak' directory installed by the rpm. When the takserver containers are built, the 'tak' directory will be mounted to '/opt/tak' within the containers. Therefore, any references to '/opt/tak' outside this section of the guide will be equivalent to the 'tak' directory you have on the host, or '/opt/tak' if you are working from inside the container. The 'tak' directory is where the coreconfig, certificates, logs and other TAK configuration/tools will live. This folder is shared between the host, the takserver container and the takserver database container. This means you can tail the logs or manually edit the coreconfig from the host without being inside the container."]}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"Notes for running in Windows Subsystem for Linux (WSL) 2:"})}),"\n",(0,t.jsxs)(r.p,{children:["When running TAK Server in the WSL2 environment, follow the steps outlined in the Best Practices section here ",(0,t.jsx)(r.a,{href:"https://docs.docker.com/desktop/windows/wsl/",children:"https://docs.docker.com/desktop/windows/wsl/"})," to maximize TAK Server performance. Specifically, it\u2019s recommended that you copy the 'takserver-docker-.zip' file into your WSL user\u2019s home directory and execute all docker commands from there (vs accessing your Windows filesystem from /mnt). From there, unzip the file and run the docker commands below for TAK Server. It\u2019s important to unzip the file from within WSL to ensure permissions are setup correctly."]}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"TAK Server CoreConfig Setup:"})}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Open tak/CoreConfig.example.xml and set a database password"}),"\n",(0,t.jsx)(r.li,{children:"Make any other configuration changes you need"}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"TAK Server Database Container Setup:"})}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Build TAK server database image:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker build -t takserver-db:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver-db .\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"2",children:["\n",(0,t.jsx)(r.li,{children:"Create a new docker network for the current tak version:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker network create takserver-"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"3",children:["\n",(0,t.jsx)(r.li,{children:"The TAK Server database container can be configured to persist data directly to the host or only within the container."}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:"a. To persist to the host, create an empty host directory (unless you have a directory from a previous docker install you want to reuse). For upgrading purposes, we recommend that you\tkeep the takserver database directory outside of the 'takserver-docker-' directory\tstructure."}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker run -d -v :/var/lib/postgresql/data:z -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsx)(r.p,{children:"b. To run TAK server database with container only persistence"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"TAK Server Container Setup:"})}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Build TAK Server image:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker build -t takserver:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver .\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"2",children:["\n",(0,t.jsxs)(r.li,{children:["Running TAK Server container: use -p : to map any additional ports you have configured. ",(0,t.jsx)(r.strong,{children:"Adding new inputs or changing ports while the container is running will require the container to be recreated so that the new port mapping can be added."})]}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -p 9000:9000 -p 9001:9001 --network takserver-"$(cat tak/version.txt)" --name takserver-"$(cat tak/version.txt)" takserver:"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"3",children:["\n",(0,t.jsxs)(r.li,{children:["\n",(0,t.jsxs)(r.p,{children:[(0,t.jsx)(r.strong,{children:"Before using the TAK Server, you must setup the certificates for secure operation."})," If you have already configured certificates you can skip this step. You can also copy existing certificates into 'tak/certs/files' and a UserAuthetication.xml file into 'tak/' to reuse existing certificate authentication settings. ",(0,t.jsx)(r.strong,{children:"Any change to certificates while the container is running will require either a TAK server restart or container restart."})," Additional certificate details can be found in Appendix B."]}),"\n",(0,t.jsx)(r.p,{children:"a. Edit tak/certs/cert-metadata.sh"}),"\n",(0,t.jsx)(r.p,{children:"b. Generate root ca"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeRootCa.sh"\n'})}),"\n",(0,t.jsx)(r.p,{children:"c. Generate server cert"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeCert.sh server takserver"\n'})}),"\n",(0,t.jsx)(r.p,{children:"d. Create client cert(s)"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeCert.sh client "\n'})}),"\n",(0,t.jsx)(r.p,{children:"e. Restart takserver to load new certificates"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -d takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && ./configureInDocker.sh"\n'})}),"\n",(0,t.jsxs)(r.p,{children:["f. Tail takserver logs from the host. ",(0,t.jsx)(r.strong,{children:"Once TAK server has successfully started, proceed to the next step."})]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"tail -f tak/logs/takserver-messaging.log\ntail -f tak/logs/takserver-api.log\n"})}),"\n"]}),"\n",(0,t.jsxs)(r.li,{children:["\n",(0,t.jsx)(r.p,{children:"Accessing takserver\nCreate admin client certificate for access on secure port 8443 (https):"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && java -jar utils/UserManager.jar certmod -A certs/files/.pem"\n'})}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"Hardened TAK Server Setup:"})}),"\n",(0,t.jsx)(r.p,{children:"The hardened TAK Database and Server containers provide additional security by including the use of secure Iron Bank base images, container health checks, and minimizing user privileges within the containers."}),"\n",(0,t.jsx)(r.p,{children:"The hardened TAK images are available in a zip file, takserver-docker-hardened-.zip. The steps for setting up the hardened containers are similar to the standard docker installation steps given above except for the following:"}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"Certificate Generation:"})}),"\n",(0,t.jsx)(r.p,{children:"The certificate generation container is only required to run once for TAK Server initialization. Run all commands in this section from the root of the unzipped hardened docker contents."}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Build the Certificate Authority Setup Image:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker build -t ca-setup-hardened --build-arg ARG_CA_NAME= --build-arg ARG_STATE= --build-arg ARG_CITY= --build-arg ARG_ORGANIZATIONAL_UNIT= -f docker/Dockerfile.ca .\n"})}),"\n",(0,t.jsxs)(r.ol,{start:"2",children:["\n",(0,t.jsx)(r.li,{children:"Run the Certificate Authority Setup Container: If certificates have previously been generated and exist in the tak/cert/files path when building the ca-setup-hardened image then certificate generation will be skipped at runtime."}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker run --name ca-setup-hardened -it -d ca-setup-hardened\n"})}),"\n",(0,t.jsxs)(r.ol,{start:"3",children:["\n",(0,t.jsx)(r.li,{children:"Copy the generated certificates for TAK Server:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker cp ca-setup-hardened:/tak/certs/files files\n\n[ -d tak/certs/files ] || mkdir tak/certs/files \\\n&& docker cp ca-setup-hardened:/tak/certs/files/takserver.jks tak/certs/files/ \\\n&& docker cp ca-setup-hardened:/tak/certs/files/truststore-root.jks tak/certs/files/ \\\n&& docker cp ca-setup-hardened:/tak/certs/files/fed-truststore.jks tak/certs/files/ \\\n&& docker cp ca-setup-hardened:/tak/certs/files/admin.pem tak/certs/files/ \\\n&& docker cp ca-setup-hardened:/tak/certs/files/config-takserver.cfg tak/certs/files/\n"})}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"TAK Server Database Hardened Container Setup:"})}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Building the hardened docker images requires creating an Iron Bank/Repo1 account to access the approved base images. To create an account, follow the instructions in the IronBank Getting Started page. To download the base images via the CLI, see the instructions in the Registry Access section. After obtaining the necessary credentials, run:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker login registry1.dso.mil\n"})}),"\n",(0,t.jsxs)(r.ol,{start:"2",children:["\n",(0,t.jsx)(r.li,{children:"Follow the instructions in the TAK Server CoreConfig Setup section and update the tag with the hardened TAK Database container name. For example:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'connection url="jdbc:postgresql://tak-database-hardened-:5432/cot" username="martiuser" password=/>\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"3",children:["\n",(0,t.jsx)(r.li,{children:"Create a new docker network for the current tak version:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker network create takserver-net-hardened-"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsx)(r.p,{children:"Ensure in the db-utils/pg_hba.conf file that there is an entry for the subnet of the hardened takserver network. To determine the subnet of the network:"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker network inspect takserver-net-hardened-"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsx)(r.p,{children:"Or to specify the subnet on network creation:"}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker network create takserver-net-hardened-"$(cat tak/version.txt)" --subnet=\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"4",children:["\n",(0,t.jsx)(r.li,{children:"Build the hardened TAK Database image:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker build -t tak-database-hardened:"$(cat tak/version.txt)" -f docker/Dockerfile.hardened-takserver-db .\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"5",children:["\n",(0,t.jsx)(r.li,{children:"Run the hardened TAK Database container:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker run --name tak-database-hardened-"$(cat tak/version.txt)" --network takserver-net-hardened-"$(cat tak/version.txt)" --network-alias tak-database -d tak-database-hardened:"$(cat tak/version.txt)" -p 5432:5432\n'})}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"TAK Server Hardened Container Setup"})}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Build the hardened TAK Server image:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker build -t takserver-hardened:"$(cat tak/version.txt)" -f docker/Dockerfile.hardened-takserver .\n'})}),"\n",(0,t.jsxs)(r.ol,{start:"2",children:["\n",(0,t.jsx)(r.li,{children:"Run the hardened TAK Server container:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker run --name takserver-hardened-"$(cat tak/version.txt)" --network takserver-net-hardened-"$(cat tak/version.txt)" -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -t -d takserver-hardened:"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"Configuring Certificates"})}),"\n",(0,t.jsxs)(r.ol,{children:["\n",(0,t.jsx)(r.li,{children:"Get the admin certificate fingerprint"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker exec -it ca-setup-hardened bash -c \"openssl x509 -noout -fingerprint -md5 -inform pem -in files/admin.pem | grep -oP 'MD5 Fingerprint=\\K.*'\"\n"})}),"\n",(0,t.jsxs)(r.ol,{start:"2",children:["\n",(0,t.jsx)(r.li,{children:"Add the certificate fingerprint as the admin after the hardened TAK server container has started (about 60 seconds)"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker exec -it takserver-hardened-\"$(cat tak/version.txt)\" bash -c 'java -jar /opt/tak/utils/UserManager.jar usermod -A -f admin'\n"})}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.strong,{children:"Useful Commands"})}),"\n",(0,t.jsx)(r.p,{children:(0,t.jsx)(r.em,{children:"To run these commands on the hardened containers, add the -hardened suffix to the container names."})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"View images:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"docker images takserver\ndocker images takserver-db\n"})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"View containers\nAll: 'docker ps -a'\nRunning: 'docker ps'\nStopped: 'docker ps -a | grep Exit'"}),"\n",(0,t.jsx)(r.li,{children:"Exec into container"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -it takserver-"$(cat tak/version.txt)" bash\ndocker exec -it takserver-db-"$(cat tak/version.txt)" bash\n'})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"Exec command in container"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -it takserver-"$(cat tak/version.txt)" bash -c ""\ndocker exec -it takserver-db-"$(cat tak/version.txt)" bash -c ""\n'})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"Tail takserver logs"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:"tail -f tak/logs/takserver-messaging.log\ntail -f tak/logs/takserver-api.log\n"})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"Restart TAK server"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker exec -d takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && ./configureInDocker.sh"\n'})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"Start/Stop container:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker takserver-"$(cat tak/version.txt)"\ndocker takserver-db-"$(cat tak/version.txt)"\n'})}),"\n",(0,t.jsxs)(r.ul,{children:["\n",(0,t.jsx)(r.li,{children:"Remove container:"}),"\n"]}),"\n",(0,t.jsx)(r.pre,{children:(0,t.jsx)(r.code,{className:"language-bash",children:'docker rm -f takserver-"$(cat tak/version.txt)"\ndocker rm -f takserver-db-"$(cat tak/version.txt)"\n'})})]})}function h(e={}){const{wrapper:r}={...(0,a.a)(),...e.components};return r?(0,t.jsx)(r,{...e,children:(0,t.jsx)(l,{...e})}):l(e)}},1151:(e,r,n)=>{n.d(r,{Z:()=>c,a:()=>i});var t=n(7294);const a={},s=t.createContext(a);function i(e){const r=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(r):{...r,...e}}),[r,e])}function c(e){let r;return r=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:i(e.components),t.createElement(s.Provider,{value:r},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/587818ee.428eef97.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/587818ee.428eef97.js new file mode 100644 index 00000000..9bc84e78 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/587818ee.428eef97.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9516],{1600:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>i,metadata:()=>a,toc:()=>l});var r=t(5893),s=t(1151);const i={},o="Configuration",a={id:"installation/twoserver/servertwo/configuretakserver",title:"Configuration",description:"Configure database connection by updating /opt/tak/CoreConfig.xml:",source:"@site/docs/installation/twoserver/servertwo/configuretakserver.md",sourceDirName:"installation/twoserver/servertwo",slug:"/installation/twoserver/servertwo/configuretakserver",permalink:"/docs/installation/twoserver/servertwo/configuretakserver",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/twoserver/servertwo/configuretakserver.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Install TAK Server",permalink:"/docs/installation/twoserver/servertwo/installtakserver"},next:{title:"Use Setup Wizard to Configure TAK Server",permalink:"/docs/installation/setup_wizard"}},c={},l=[];function d(e){const n={code:"code",h1:"h1",img:"img",p:"p",pre:"pre",strong:"strong",...(0,s.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"configuration",children:"Configuration"}),"\n",(0,r.jsx)(n.p,{children:"Configure database connection by updating /opt/tak/CoreConfig.xml:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:'\n\n\n'})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl daemon-reload\n"})}),"\n",(0,r.jsx)(n.p,{children:"Start/stop TAK Server services with:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl [start|stop] takserver\n"})}),"\n",(0,r.jsx)(n.p,{children:"Or on resource limited hosts:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl [start|stop] takserver-noplugins\n"})}),"\n",(0,r.jsx)(n.p,{children:"You can set TAK Server to start at boot by running"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl enable takserver\n"})}),"\n",(0,r.jsx)(n.p,{children:"For secure operation, TAK Server requires a keystore and truststore (X.509 certificates)."}),"\n",(0,r.jsx)(n.p,{children:"Next, follow the instructions in Appendix B to create these certificates. TAK Server by default is TLS only, so certificate generation, including an administrative certificate is required for configuration."}),"\n",(0,r.jsx)(n.p,{children:"Verify that the steps in Appendix B have been followed by checking the following items:"}),"\n",(0,r.jsx)(n.p,{children:"Certificates are present at:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"/opt/tak/certs/files\n"})}),"\n",(0,r.jsx)(n.p,{children:"The admin cert has been generated and an admin account in TAK Server was created with the command:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem\n"})}),"\n",(0,r.jsx)(n.p,{children:"Import this client certificate into your browser."}),"\n",(0,r.jsxs)(n.p,{children:["If you are using Firefox, go to ",(0,r.jsx)(n.code,{children:"Settings -> Preferences -> Privacy & Security -> Certificates -> View Certificates"})]}),"\n",(0,r.jsx)(n.p,{children:"Go to Import. Upload this file:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"/opt/tak/certs/files/admin.p12\n"})}),"\n",(0,r.jsxs)(n.p,{children:["Enter the certificate password. The default password is ",(0,r.jsx)(n.code,{children:"atakatak"})]}),"\n",(0,r.jsx)(n.p,{children:"Browse to:"}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"https://localhost:8443\n"})}),"\n",(0,r.jsx)(n.p,{children:"Select the admin certificate to log in."}),"\n",(0,r.jsx)(n.p,{children:"An error message similar to this indicates that the correct client certificate has not been imported into the browser:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Example Incorrect Client Certificate Error Message",src:t(1164).Z+"",width:"960",height:"460"})}),"\n",(0,r.jsx)(n.p,{children:"Once logged in with the admin certificate, configure the TAK Server with the following instructions:"}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Configure TAK Server to connect to the database. Access the Database configuration settings:"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Example Incorrect Client Certificate Error Message",src:t(448).Z+"",width:"170",height:"310"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Edit the database connection address, specifying the hostname or IP address of the database server:"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.img,{alt:"Example Incorrect Client Certificate Error Message",src:t(9780).Z+"",width:"382",height:"204"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Restart TAK Server"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl restart takserver\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"Or on resource limited hosts"})}),"\n",(0,r.jsx)(n.pre,{children:(0,r.jsx)(n.code,{className:"language-bash",children:"sudo systemctl restart takserver-noplugins\n"})}),"\n",(0,r.jsx)(n.p,{children:(0,r.jsx)(n.strong,{children:"If you would like to configure TLS for Postgres database connection, refer to Appendix D."})})]})}function h(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},9780:(e,n,t)=>{t.d(n,{Z:()=>r});const r=t.p+"assets/images/database_connection_address-e7a41b233e0f5b778432cb2d2b24f020.png"},448:(e,n,t)=>{t.d(n,{Z:()=>r});const r="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKoAAAE2CAYAAADxpQkOAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAACZJSURBVHhe7Z2Lv03V2sffP+W83XOcQjqIkkRtXuRSCdHNEYmUEpVKkSIiSkq5nJJLcSihmxC5X5JCN4STe3TqvJ/3fT/jnd9n72fuuZex9l5r7b2yxl7P/ny+nz3XmGOOMdacv/nMMff+PXP+h4t+zvnPcwyjYGnXvr0zoRoFjwnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNTKmXv36rn6DzLjwogu9beSKCdXImCeffNKNGDEiIzp26uRtI1dMqEbGlJSUuLZt22ZEg8saeNvIFROqEQQmVCNjHhh8v3vwoQcz4vrrS7xt5IoJ1ciYv/2tt7u7b5+MaNHyGm8buWJCNYLAhGpkTIcbbnA3dOyYEY2bNPG2kSsmVCNjsvnzFGL1tZErJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQRALdfSQoXnB16lhZEss1F/37k3Pvn3+cqhina9Tw8iWWKinv/02L/g6NYxsiYV66tvdaTm9x18OVa3zdWoY2VIu1N270nLaU5YJbOfr1DCypVyou77JC75ODSNbYqGejESVDgTnK4eq1vk6NYxsiYX6yzdfV+TrnVWXZVDH16lhZEt5RI0EFrPzq1LKPiO+1DL9LOtSyvQz63ydGka2lAv1qy/Tg/h85VDFOl+nhpEtsVBP7NgeczKxLJ8RXWqZ/o7WpZbFn6N1vk4NI1vKhbp9W3oQnq8cqljn6zSflLRpI3nlQ4c+7IY8PMQ98uijrn//fu6SSy/11ldatW7t2nfo4F1n5I/zzz/fXXDBBd51ScqFum2rcNz3G9GllunvKtb5Os0nPPIQof7lkkvisnbt2rkBA+6NP/NcJB6QwJfn4bSXNbxMHuzVv/898rjEunXrSjvUaX3dde6ccyv2YdQM5513nhyLVq1biWB9dZRYqMe3bkkPwvOVR4goPeVCtM7XaT7xCRWh8ZQPdsbA+waKIKl35513uIeGPCRPSC4Vaj9ZJhrffPPNUmfQ/YOkPNmHUTMg1OZXX52lULdEwkoHovOVQxXrfJ1WBZcCBEO0y+SykMQr1AiEqu2yY/ji3bp3d8Mff1zWI0Y+I+ormjaN6xCJHxv+WIW2jOrBcW3RooXAfmbapZ8vrnOxd5vMhFoNfJ1WBYPlEgzpBp4On1D/HF3KH3/iCVkmQrKeSErU1PJYqNHy0GFD4zpE3+HDh8dtGdWn4eWXy/NTAZECUyw+c6x828RCPbZ5U17wdVoVNRpRowjZrVs3ebYnO4h1zFlZxw5Sod5zT1/XvUcPqYNQL7zoIilnemBCrVk4pgiSewG99HO8KGM64NsmIdQNecHXaT7Ru37EBQiRaMlO4U0dLBMpuaT37ddXLuuIEoEzDWCHDB78gBs4cIDUYU7LtEGFa9QccjMll//Wmc9Rj21cnxd8nZ5t2Cm+HSNnc9kdPsKsaucZ1Yep3cV16njXJSkX6oZ1Z7L+i8o/Z1Dm69QwsiUW6tF1XwgisjKO8ju1LOWzryy5na9Tw8iWM4R69Iu1Zy6n/s5ina9Tw8iWcqGuXVMRxJZapqRb5yn3dWoY2RIL9ciaz6tPJMzUMl+nhpEt5UJdvboiCC21TKlsXQq+Tg0jW2KhHl69qvp8vvqMMl+nhpEt5UJdtTIv+Do1jGyJhXpq3/684OvUMLIlFur//e//5AVfp4aRLbFQ8/Xj6/SPIpN/zRlhUOuEivEEux4OqEH33ydGE5xT/N+eVBNSU6h3S7dbxJ2Vun0msC1egGR71QX74R133O5dV5vhuGTikKt1QsXTiHvqr40axWVY9ZpddaVY+EpKSiT1BIF17dpVPK+UaV22Yz3LOK7YQXFKSlSG04dtKdf2KMdcgQOLuvSl7bEdDiHK2VbLk7Adri4yCxpe3lDKaAMLHMuM8crmV8kyB1bbwQrJyULb9EFZ02bNHC/M1fFRRroHL9xlO4w3tEEZ7bO99gP6namf3IfNmzeXfhireka1jPrpfKSVIe6pbFNR8vXj6zSfkMRHWgmJfTfedFMFMy47lQjLAUZsXW68UaKquvy1DjuFHYdwOGD4V4l4HGh2LNuqKNmWHf7A4Ptdz549RbhE8muvvVbawyJ42+23Sf3SNlrFfQFjow72RMYz7JFhEmHoU7MS8NPynaiPIHv26uUurVdPbIy0i9i4giBQPLWcqPxGgIzl3nv7Sz1+9+7dW/qkzl2975Tvyrb0R198546dOkl97JCInX1I/4yRqI8FkhOKMuqxnylLfq9MYL+pHzVIobLDcjVOK0QWdiAHhwPaqHHjWFisLz0IpdHLJ1R2IikpHCTK8KYiCN2W+a+2R+TBt6pttLy2pYiAZcSm1kHqaxsKJwKi0QPOMoJAiCxzYiAI+iSyIloOLPuFqEtdhIWHlpMDgcaijralDcRF3126dJHxEDU5IXQMnGAYx6lPOXWBMtommrMP6ZsTEZOznii0zf5KTf2pjGqlouTrx9dpVTD/01SUbA3LCIE5abKMnU20UmFRlk6o1GOnxFE3OriIlaiYTqhEMk4IbYMdrvNNzSAA6iNM/Qy0hQAYIzB2Ii/rEArTFqYFTFP4zJUCkXJwGR9tskzKDAcdoWIIZ3s+I1Tm1ExBgD4QFf3qGJjT0wf1Kde67AdOVub9fEfGfnffPnHUp4wTDKFTVlVUVBAq0waiKSc1/bJMWbrjXZBCBQbMDvKtqwy+LFGDA4zAuKRxWUY8SaFSxgEkMrCTyZ9ipyEEdgrbUk5kRgg6DdBtOUDankwTossnoqY+B50DTd3KhIoIaEs/C1H0pV9EQOTju7AdY0N0KkIu4QiaNviORDfGnBQqJxNi7nN3H4mKnDxcGbj0+4RKfdohMhNdmTawP/muRGzExDJ1GJ9GccZGWdZXv+i76qWfK5i3ThkFK9TqwOWDL0ak6tS5c3wjolGBZS6bXGqbNL1CyomkfGanMU2gDstEMoQiZ3yEbkv9ZHtECaIV9dmOMkgKk/qgn+kbYetnhX4QIMsIg+iOCBgH20i96CBz2ac/vivbsA5hJfvnJKIN6vEb8dMWy1qH/cPJyDJXGfYD3w+xah3Wa+TXmyzKONk5yZM3XpmCOBkz/RelUI3ahwnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCAITqhEEtVao/K8ckwiGC0wbmTp7UuH//vwvm/99J/9vnw/4nz6/Uz0BRi0UKg4gvKAYeXEw4ezBhqb+0GzBvtfrtl5iIEbwvjo1AUYSfeI1J0cuJo/gOLfUBCPftcyzm45aJ1SiHna41Nf1iDso2hnsFKxu2Paw0+FAYj3LeDqxxWGBw9WESRibHusQKdtQl5OBE4HP6hGlXJ3/LONdxQbHMkZmLHlqj8PTiS2Qz0Rr6rCehwqLETnqS9vhN3UBZz99c9Coj1mbbfg+Vb2eqBDBMaXm6WDdU7n6UbGiIQDfOk37wKrGZ6x5HGhsgZSr2ZnIhuhYVk8my+otpQ92HMt4QNmWZU4QNVdjydM2WM+Jgr0O/yYmbKYiiEvHKja6soiqvlWiOG2wH9gfnBjY6lhPXxjLES7LOp4Q4PtolgBWQr47y5Slm6IVpFA5KNVx+HPgUk28YgaOzl7WxZ7OCC7tXGoRk85BEYIarH1CZZvkHFLrphNq0jyN+DA9E4VpW9f5hIovlIit2yJSojHrk20mxx4CXO45wZma8b2BZco0vy2VghQqIss1ZwohcOAwCmsZwiRyIXzEpJdVzmwu6/zORqg9br01nq9yArCtrucGjmUyDFSo2haUpprcJ5GDbNekUHHns6xCxaDNJZ/LIvWZSjBVSI4PQhMqxxRBYuJWhz/RlLJ0U4CCvfRXBy4jiAERcnARjIoNYTEHldfyRAebDADKsxEql2zmiERF0j60nBOAqQTrtF/Kk6JC4Jw0pH4wz6U+42XuTD3mmypU6hNFmeMC29B36EJVyueoWbxsIl8/vk7/KJg2MIdLt85XngncZKl4uRnTmylg52dyFfD1z7a+iEJ7VR3IUMn6ZRP5+vF1GjrMsZgrEpn5nZzzGvnBhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCIJaK1ScOZg3Kvu/O+v1EZNVUVV6COsqNYacW55qkim4p/gff1V9FwO1Vqi4onBPJZ8EnUo2huOq0kNYX1mqCi4r9ZtmAtY3xodho2hSUyqhdgo1il644fE6crDV6cRBxw/KOix2KlTAmoelD9udWuuw45H+wbbJ9BDMKKlpIKxXFxVPeNZUE8SJtRArIPX1Ofn0x3oiZTI1hXHhw2U94+NE077JQCB/S75jBL5YcsLwtWJbpF1+Zxu5Q6BghYoNLpdUFOAAIhaWiWQ46llGgPhEmQ4QpVSoTAFY5oBz4BEN9jPqqdc06RHF/5maBqIeUaYcCBLxsr36XfGSakRle4zVrE9NTdH28GiyTB/aN/uEttmOfYPflf4YI9+NtsmWZRzU4XMhwpgxuONCS8L+TzfughQqByTXVBR18XMQiXxEGYSBg5y0DnXgAwdYRaaCRMCISuuoQTkpVK0LalpWoVJGCgmCAjJgifCpQtW2UlNTGDvpLD6hUp+TkLQNvgvRnohMPb4n3xdoq5CjKtMYvh/TmySctEG9FYWzKtdUFFI1NOpoGSkeRBqS8hAD5uRk9EqKDKEm85SyFSonBH2xju/ACUOkoE1NNWEbza1KTU1JFSrpGcm+yY7lBRWs42aL6MSynoA45umnkCNqLhSkUHMmilySdlL2UgiFqEM5KR/MAbn8c7CTEbWmhEoZ81bEV5o+co+Mizkm6zVrVYWamppCNCTfi6hDP7SR7Bs0+upnvi+RlPksv/XlGrWJ2iXUDJFoE4nHt66m4BKWmj4iaSZp+k03xckmBSXbaVJIFKVQjfAwoRpBYEI1gsCEagSBCdUIguCF2rFjW/fcc4+5sWOGG7UQji3HOGih8gV+/fU799tv3+eFCRPHujqXXmGcRerWb+ZGjX7KTZ06MVyhcrb5BFZTmFALA8T6r9/2hStULg0+gdUUJtTC4b//fdCEmg4TauHw++8m1LSYUAuH337bb0JNRzqhPjt2ohs/4SVhzLhJ7m997/PWS/KXBle6zjf18q5Txoyf7C03LKJWSjqhHj582E2cNNU98dSzItpvvtnlJrz4ireusnnLVvfmnHe865STJ096yw0TaqVUJtSSdjfFn1u36eyOHz8hyx0693BfR8L9+eef3fFIeLPenOseeewp98upX6I6x92QoU+4Bx9+PK6zb98+9+BDw2Vb6qzbsMkdPHjIbf9yh+t1Rz8pn/bGLHfo8M/u6LHjUr9Dp24SoVeuWuMO/TPqJ+p7zvyFUvfpZ56X8cG6DZvdjV1vk/LQ+f33n4pPqDu++sxNfGGUG/f8CFn21YFMhQqI7NqSjm7IsCfd0EdGSNmgwY+6EydKo+S2bdslota7/Gq35IPlcZ0Fi5a4I0eOyvKpX065J0aMluWp02ZKm1df2969t2S5q9+oRVz/uSiK39VnQLTdMdemfVfXqFlrN+nlae6qlv8l/XXv2Ufqvvb6LInSrOdzyBTlXT8i9S2nkqlQm1x5vYiMv/d17X6XXOYPHvpn9HtLfDlXof65XtMo6o2L6+zduzcSV2k0Pl72G4iyp0+fdrfedreb+fc57qefDgjUf+75SVJn+qzZEmWPHTvm3l242PXpNygax2m3f/8BYd/+n+R3l663x+2GSlFG1OkzXnTLlr3t5r/zhiz76kAmQuUSPHvuu+7TFavk82crP49vip4aNSYS6i+yvGXLNrk894wu5whQ6yxb/nEcdYmgt/ToLcvMgRH/wPuHiqAbNm4Z10eod/d7wL0+Y7YIHyEePnzUDX54uJwYLa+7QeoOGjzMzY36/GvTVvI5ZIp2jrphw/JKL/uQTqgy/4zmm0RChMi8snVJZ1k35oXJUSTb77Zv+9KtXbfBHTl6zDVt0UZuthAkvzds2hLX+eqrnSJI6rAeURJ9f/j+e/lrwhVXt3Hf7onGyzb79kv96TNLBUpUZq67d+8+90XU1yXRSTNj1tsSVbdu3R5F0/3uvvuHnTH+ECnKiJop6YRaFQ2btHQNGl9zRnn9aH5KBK6sDjSNxIkQk2VXXtO2wmelRav28fxVYdsro/lqsix07O+olZCrUI2a57/tX6jpMaEWDhZRK8GEWjjYH/wrwYRaONjNVCWYUAsHi6iVsHLV+yJW4+xjN1NGEFhENQLB7vqNALCIagSBCdUIAhNqDnz66SL34YcLhI8/Xuh27vzcWy/Jr79+7/bu2+xd52PFivI+gD5/PrzdW1f517++y6qPJMnxffTRwjPWn23+/fsBE2q2jHlutPskEs7atUtFUJNeHO8++WSxt64y7fUpbsGC2d51Pp6L+liyZJ7bvOljt379h+6t2dPlydLf7FrjrQ/r1i3Pqo8kyfFxUqSuP9tYRM0BhHrwn+XR7VC0PGrUSFnet3+zmzxpgtShbPHiuW7Dho/c008/5UY9M1KEt2XLJ3GdcePHRp8/jdtSEOqOHavjz9u2fSZCXb16iTty9Cs3b97f3TOjn5F68+fPckeP7ZQThj4QNXVeffUl+UydNWuWSjuIkPLRzz7jRo58Wk6G1PGNHfuc1CWCT5nyonymjVWrlqRtQ8eZL4pSqJmmoqQjVagw4qkR7vCRr+Sgb9jwoZR9uWNVLODXXiuNWKdP73Fz5syM68ydN1Pa03YUhAETJoyLxjlGRLFo0dvu5Kk9bvGiOdLu4SM73PETX0cCGyViWfvFsjgqUucf/3hblqVOVP/nwzuiy/oCETLThO9/3OBGpoyPZcTH73cXvOWWL39XljkRnhk9yv24b2PaNvJJUV76M01FSUeqUE+c+MY9FQmVA7fn23Vu6tTJbszY0RJ1ni476CoE6hCZtM64cWNFaNqWgkgRPWL84IN33OTJE0UsrHv9janSLtFYiIQ8/503KwiVOhqxtc6mTZ+IyJJzUBWlT6iMPznVYMwff/yPtG3kE0tFqSQVJR1JoXITsnDhbDdj5qvyecbM1+I53rpozsglleVpr78sEQohcwnXOm++9UYcdZMg1OSl/41IeNQ7cHCbiBJxn/xll0RoBLZx40ciVPqgPnUQOMvUoZzpACLjBlDbVZHp+JJlc+fNcitXvi/LtPHss6XfO10b+aVI/+CfSSpKOhAqQkE4RDaiEfNU1q1YsVii5CtTJklU43J57PjX7rPP3pMDyk0Y9bXOSy9NFOFSJ9lHqlAPHIrmwVGfCHbf/i0S7YiUL4x/XtrY/9NWt2fPF9LHK69Mkjqsnzr1JanDXJl20olMx8dcVsu4zDNOxvtC1NbSMuGfDaHazVQeYB4JqeVEJX1MZro62eBrgz5AP3MCZNpPcnwKU5UjR3dWaPPsYP9CNQLAIqoRBPYHfyMILKIaQWDGaSMQ7GbKCACboxqBYBHVCAC7mTKCwC79RhBYRK0G/GsS6xzGFN/6fJGPfnGAqTurEDGh5gC+U9xGmD4wK+MV3b59pawjbQTjCXY4ylO3zQR8pD7RfP/jxrT9Zoq2nRxfqRtrpJs9e7qYaFK3qYof9m6sMsOhutilPwdwK61cWep2B5xLkydPkEinQv32u/Xixmf98RO7xIOK6yjpiFq7dpnbvPkT2QaLHmXr1i0X535qOghZALis0vXLZ9qmD/qiT8qoj2sfIa1Z80Hcto4PxxUOfkS6e/faeMxALhjt0Ya2t3v3F2Un4kJxaFFGtsGrr04W/yyfdRz0o9txcnz22WI5QbZuXSFmF8ozpWifPZWrze/goe0imHQvC1ahcqCwAXJAMD3PmjVNvJ0vRyLH50k5ZmsEQjmpKeRGIVwinaZ9KNj0yCJI1y9iJMrS1rz5s+Rkog+2wTL4XrQ9nlVtW8eHQx+hile2zCZIe6S30AZ1sSPOmTNDlvnumKbJICCyU5dtsQIiQB0HIl26dL60gUixQ5IVwL4pPeFKfa6ZU4R/nqqOcZq5IQc/XURIFeoPezeJeZrPgMGaA/XTga0iVBUe26nHEwEwvUi2+95780RE6fp9863p0XeZGveDMOiDsaqPFLRtHR9lnEjbtpVOIegD7ytjXL9+uZRh8cPwzXacTJ9//oF7//15cfZCMrPANw6mLGQYTJwwTr4ngk53wqWjKOeo1U1FIQJxCdTPp09/5yZOHB8dkE1nCJUDTE4RGavA5Y88JKJM0nBM/cqEijgQT7p+ySwg6mk/jIM+ECqXW90mE6Ey56QvoiNl3LQRjREn68kcWEf2Qtn4k0L1jYP+DhzcJu0tXPi2nLiaz5U5RRhRq5vcx1wMxzuXQiLM9BmvivOedalCJRohbNJVSPbjZojcI+aV6YRK5MFxr+uAdkgETNcv80ic/fSxZMl8cebTR6pQte3KhMoJQFvTpk2R7FfE9+abr8v0hWRD5q4Loss9bbMNiYp8J9ooH8fqaL67VMbBXJp2GT9zbco07SVT7K4/R5hLLlo0WyIJ6Sd6Q7Nr11qJevxW4RFNEDBZpKR66OU7mSBXut1aWS4Vmz8FOV2/tMnNGX1wopC2TTn9JqOwtp0cHzdQiIllHRPtMVZOMNpgysOUgLbpgxOF/vlu5G4xvYDkOJij6jiYhrCecuavbEN5pthdvxEEFlGNIDA/qhEI5p4yAsDmqEYgWEQ1AsBupowgsEu/EQQWUY0gMKEaQWCXfiMI7F2oWYKLif99p4KX01c/+f98ozrYn6eyAucQtjaMzFjmWAZMF776SYeUkTs2R80R0i2ws+lnrHOkeWDpw/LGI80pR6iYhImsWPR4gQN+Tx7EywOB+a0OI4Q/c+Zr4sJnmT5wI2Hlw6XPg3yx12mfxYX5Ub11qiIpVCxwiEsfl/7BB/NiYzAmYbyciBS3v6Zl6Pr33p8vYj5xcpc4/vF2Ik4EyaPIeZI1JwAWO6xxy5aVWunYtpgoyohaXYc/JIXKMu9p0nV4PtXQjPgQGk58nv6MDxT3vL4IYvz4MZKmQVTV5/1D0thMGgrRlHbIZfqj07MLgaK86yeS+pazISnU737YIC55XYepmBRklhEfGZ+Ik5su3o7HMqZj1jNF4EaM6UFyPpvMEKB9oihvNaG9XN/OFzJFGVFr+tKPy545JA52yl977WVxubNOxYdIqcObSV5++UXZtqrUFITKpZ/oi7N/XVkqtb7YopgwP2qOMGfUt+EBOe4k7ZFqgaC0PPnnKcRLWkgyXaOy1BRNFSFrk6Q4kup27MjugRO1BbvrNwLB/o5qBIBFVCMITKhGENj/+o0gsLt+IwgsohpBYHNUIwgsohqBYH9HNQLAbqaMQLCImjU8NlFTUPh/PK5/X72qSD5qMld49in2Qd+62oTdTOUAbiaeE8oTlVesWCTPrM/lrSDY+5LPLs0F3maiL32ozdjNVA4gVHXzA7Y7NTlj49M0E8AdxSPBEdSP+zZKHex9+FeJzOpLpR4Gamx8bI8/NV3KSpJ0QiU1hu1SU2Po8/mxz8rLLXh6NJ8ZD2XYEDFz029qe2ebooyo1fWjpgoVeEw4z6rnbSGaZoIVEAEjXlJVsOmdOrVbxEhKiT4OnUs3Dn69hOPox9qXLmVF+wSfUDFq05++OkdTYxAgYjz5S6kZGwM3Y8DkjUAZL6L9ZteaCu0VAkV5M1XdVJRUofLWO1JO8JW+/sZUWc+BF8rSTDj4LONVRSAHDm6LhcqjyTFTJ/uoLGUlWc8nVITuS43B/8r4tJyTgzHwCh+SCvlMykwhCrUoI2p1Xt8DSaGSv8Rz7nktD5+JTkROlolovFRBn1fPu5iYz2qUVKGS9EciH2knlJOKzaUboWpbOPzffffvZzz73idUsgzGjKW9UkM282fa/PrrNaUR9dQemYZoROVNLfTHiUaWAq/gSbZXGBTpXX+uL0QDhEr04bLO5RkBanoIoiECMi/kEo9AdDvmoQil3NFf/iYUXo1DpipCnvTiCyJaLv/aFr+3f3mmux+hAn0B73iinJOB9pjbMhflsk45Uw7GwBy5VKgL42kGZazjc7KPQsDu+vNELn8yIlqSTq1CVnL98xPt6TwVuKGaO3emRFr+4oBQk+k0R47ulJs4/VxImFCLDKYSRNgpU16Uv8EWqjBTsT9PGUFg/0I1gsAiqhEENkc1gsAiqhEI5p4yAsBupoxAsIhqBIDdTBlBYDdThvgKDh7aHptiChGLqDmAKQVDMrY7fJ+YUtT0kQ38C5NMAd+6TGEsagVkTBMnjJOXX/jq+uAlGRhsMK9gDWQ8pNhgcPHVP1vYzVQOJG1+gEteH+oL2OxwRq1atURMIRiWSVnBNM16bHZ4UDE4A2WYUVaufF9EwvapbSXLkjAWvKv6GZseZhPGh2DJLsDmh+EaLytCZLxqdJn/zixxTTE+0mIYT1KojJ/vkToG+qQebfkyDGqaoo2o1bX5JYWKJxWHP5dOhIFVD9HNmz/LvfTSRPf9j6XeT0zT1OflE7NmTatg88M4jWEEbyiREb/o7NnT47ZejtqZO2/WGc4qxrJly6cidMzY0994RSx7mLnpk5ST9xbPlfdj8RkhL1kyL7YS8lKLV6ZMkhOHBxEzHhUqfSFixpocA31xJUHAtMUYUsdV8xThXX9NGqeBg4TDnwOI6RhPKCIERMPlFdFxwNWwvHHDx7FQMTRPeOH5uL39P22VMuppWxiz+Zz6PivGAuPGlXpZeRkFgmUd9YneLM+Y+ZqITLejLsmJGLKx/lGm41GhYujmnQH6XXQMnHis50TEcO3L5appijKi1nQqyq5dpc55lhHEnDkzRATAQUfARC+24xHnRMzSd0+VCoNIS+TT9shnogxRaFtEWiIebWk9oM3kpT9J8p0ApKDQt34m0nOyVCbUPd+ucyNHjYy/i46B3DCmCEwXOAHphzJtOx8UpVBrIrmPSyjzs2XLFkg049LKupUrl8QvkliyZL6s06hGzhLiI1LxWYXBesSLiXnjxo8kbwljc7qXUug4IFOhMlaiPn1wmefmiylLZULl5GJcpNokx0DEp98/8gUY9uepHOCgczCBO/fkAyiYBuiLJLhxSV4WuelAJPoZgekDKDj4pKOQQoJYKUu2hcB880DGwp+WUssh+fIKtt22baX0wUmmf6VgfMxxWdbx8Fv/GnEgmkbwPVPH8Ee/AMPu+o0gsIhqBIH9wd8IAouoRiCYe8oIgKBvpoYNHeD9UkbtY/fu9eEK9c9167ilS2cHk5tuZA/HlmPcs1ePcIUKf/rTn4wioF379mEL1SgOTKhGEJhQjSAwoRpBYEI1gsCEagSBCdUIAhOqEQQmVCMITKhGEJhQjSAwoRpBYEI1gsCEagRBrRPqXxs1cjd07OiaN29eobxJ0yvkyybLMoX2GjdpIsvNrrrSnX/++WfUSUeHG27wlhvZUeuE2rZtW3nIw5CHh1Qov/fe/u6RRx+tUJYp7CROgFatW0nbF9ep463no127dt5yI+Lcc1zDyxvKvmXZW6eMWivUgQMHuEaNG0tZ3bp13aD7B8VC/XP0ecCAe93QoQ+7ocOGupI2baScndG//z1Sb/jjj7suN94o5dShXdqgbURfWRvUGz58uKtXv76sp3zYI8O8bVNf2ohgzLRFeTFw3nnnuRYtWkgAYNlXRylYoV540UURF3rXVYYKlUvuzTffLGUsd+zUKRZqt+7d5TPL9PPY8Mdcg8sayCX+jjtul3LOdATF8pNPPilttGrdOo6olbVx5513uAsuuEDWPf7EE3EbqW3TDsK8uM7FUn5X7zulHsu1GY7rXy65xF1ar55r0fIa2a8sU5ZuWlWQQuXAc9CBZV+ddKhQL7n00jg6EeHYESrU/v37yZxVtyFCtu/QQUSWnFMmRZYq1FzbSJYz3727b5+4jO2LQaicqNdfX+JaX3ed7FNgmTKuVL5tClKoRKP6DerLpVMjU6aoUNmub7++EqUQA4JXofbs1UvqscwZzGWZszlTobIzK2sDwaVrI1lOfU4mPRmJuMUgVI4N+5ApWfOrr472ayvZF5SlmwIU7KU/V5JCZd7IMmcqlxsVKpH6oSEPyZzxwYcedJ06d5byqoTKpJ8ytqmsjUyFym+2Y9rAzR/t6HSjGCifo7au8i8ptU6o2cDcMJs/NSnJbXJtAziZet3WK75q9O7dW6YQqfVqM+y/TP6KUtRCLQS6dOniBt43UKJz9x49qrz7LVZMqEYQmFCNIDChGkFgQjWCwIRqBIEJ1QgCE6oRBCZUIwhMqEYQmFCNIKh1QlUjCkZkjB64kwYPfsBd1vAyMYxgAPFtVxW5bmfUDLVOqOqewjamZTiUetx6awWhlpSUxOtxReGHZBmrGekj7BgsaA0vv7y0fpmDn/abNmsm63H9aBvkVKlzCnuilhs1Q60VKuZcXDnY8fCj3njTTRWEmrTTUS4WwHNLTdbY8RArHlPWUSdp1yNKs+Oom/S6qsDZTts2aoaCFCq2t+oap7n040jCmUQ0xY9alVCJnkmbHakmPqFqCgrryI1ijGzf5+4+UsbYtQ2jZihIodZEKkry0q+kEyqJdgiN1JKkUKnvE6oaoEWotFOWTcl04pZut0hZJh5LI3MK9tKPQKuT3FeVULk8k/zX8tqWctOFUPGCEoWZZ9IOl/hMhMq8lvbIJGBeS1ukWVDHqBkKVqi5wuUbAfmmDNzwaJoIy0RS5q7k7LAjECoJd9QB8qIop35SnPowCn5rOQ+8QPhdu3aVNigzao5aJ9TqgFCJhsxB+UsBEZXph6+u8cdiQk2BG7h77ukrGaz8GcpXx/jjMaEaQWBCNYLAhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEgQnVCAITqhEEJlQjCEyoRhCYUI0gMKEaQWBCNYLAhGoEQd6Faj/2UzM/zv0/ac6B2EwMlmcAAAAASUVORK5CYII="},1164:(e,n,t)=>{t.d(n,{Z:()=>r});const r=t.p+"assets/images/secure_connection_failed-56cfeb544f1c53afe1d119d408897cd4.png"},1151:(e,n,t)=>{t.d(n,{Z:()=>a,a:()=>o});var r=t(7294);const s={},i=r.createContext(s);function o(e){const n=r.useContext(i);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function a(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),r.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/59362658.1df61e98.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/59362658.1df61e98.js new file mode 100644 index 00000000..33b558cd --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/59362658.1df61e98.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2267],{7797:(t,o,e)=>{e.r(o),e.d(o,{assets:()=>u,contentTitle:()=>a,default:()=>d,frontMatter:()=>n,metadata:()=>c,toc:()=>l});var s=e(5893),r=e(1151);const n={slug:"mdx-blog-post",title:"MDX Blog Post",authors:["slorber"],tags:["docusaurus"]},a=void 0,c={permalink:"/blog/mdx-blog-post",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2021-08-01-mdx-blog-post.mdx",source:"@site/blog/2021-08-01-mdx-blog-post.mdx",title:"MDX Blog Post",description:"Blog posts support Docusaurus Markdown features, such as MDX.",date:"2021-08-01T00:00:00.000Z",formattedDate:"August 1, 2021",tags:[{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:.175,hasTruncateMarker:!1,authors:[{name:"S\xe9bastien Lorber",title:"Docusaurus maintainer",url:"https://sebastienlorber.com",imageURL:"https://github.com/slorber.png",key:"slorber"}],frontMatter:{slug:"mdx-blog-post",title:"MDX Blog Post",authors:["slorber"],tags:["docusaurus"]},unlisted:!1,prevItem:{title:"Welcome to Docusaurus!",permalink:"/blog/welcome"},nextItem:{title:"Long Blog Post",permalink:"/blog/long-blog-post"}},u={authorsImageUrls:[void 0]},l=[];function i(t){const o={a:"a",admonition:"admonition",code:"code",p:"p",pre:"pre",...(0,r.a)(),...t.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(o.p,{children:["Blog posts support ",(0,s.jsx)(o.a,{href:"https://docusaurus.io/docs/markdown-features",children:"Docusaurus Markdown features"}),", such as ",(0,s.jsx)(o.a,{href:"https://mdxjs.com/",children:"MDX"}),"."]}),"\n",(0,s.jsxs)(o.admonition,{type:"tip",children:[(0,s.jsx)(o.p,{children:"Use the power of React to create interactive blog posts."}),(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-js",children:"\n"})}),(0,s.jsx)("button",{onClick:()=>alert("button clicked!"),children:"Click me!"})]})]})}function d(t={}){const{wrapper:o}={...(0,r.a)(),...t.components};return o?(0,s.jsx)(o,{...t,children:(0,s.jsx)(i,{...t})}):i(t)}},1151:(t,o,e)=>{e.d(o,{Z:()=>c,a:()=>a});var s=e(7294);const r={},n=s.createContext(r);function a(t){const o=s.useContext(n);return s.useMemo((function(){return"function"==typeof t?t(o):{...o,...t}}),[o,t])}function c(t){let o;return o=t.disableParentContext?"function"==typeof t.components?t.components(r):t.components||r:a(t.components),s.createElement(n.Provider,{value:o},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5e95c892.4aa2b89a.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5e95c892.4aa2b89a.js new file mode 100644 index 00000000..8105da39 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5e95c892.4aa2b89a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9661],{1892:(s,u,e)=>{e.r(u),e.d(u,{default:()=>i});e(7294);var r=e(512),a=e(1944),c=e(5281),t=e(8790),d=e(6040),n=e(5893);function i(s){return(0,n.jsx)(a.FG,{className:(0,r.Z)(c.k.wrapper.docsPages),children:(0,n.jsx)(d.Z,{children:(0,t.H)(s.route.routes)})})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5f4ff9a9.86c60a80.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5f4ff9a9.86c60a80.js new file mode 100644 index 00000000..bf666131 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/5f4ff9a9.86c60a80.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[722],{8107:(n,e,r)=>{r.r(e),r.d(e,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>t,metadata:()=>a,toc:()=>l});var s=r(5893),i=r(1151);const t={},o="Appendix A: Acronyms and Abbreviations",a={id:"appendixa",title:"Appendix A: Acronyms and Abbreviations",description:"- ATAK Android Team Awareness Kit",source:"@site/docs/appendixa.md",sourceDirName:".",slug:"/appendixa",permalink:"/docs/appendixa",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/appendixa.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Data Retention Tool",permalink:"/docs/dataretentiontool"},next:{title:"Appendix B: Certificate Generation",permalink:"/docs/appendixb"}},c={},l=[];function d(n){const e={h1:"h1",li:"li",strong:"strong",ul:"ul",...(0,i.a)(),...n.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(e.h1,{id:"appendix-a-acronyms-and-abbreviations",children:"Appendix A: Acronyms and Abbreviations"}),"\n",(0,s.jsxs)(e.ul,{children:["\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"ATAK"})," Android Team Awareness Kit"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"CA"})," Certificate Authority (for digital certificates)"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"CN"})," Common Name (of a digital certificate)"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"CoT"})," Cursor-on-Target, an XML-based data interchange format"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"CRL"})," Certificate Revocation List"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"DoD"})," Department of Defense (United States)"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"DISA"})," Defense Information Systems Agency"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"ESAPI"})," Enterprise Security Application Programming Interface"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"EPL"})," Evaluated Products List"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"HTTP"})," Hypertext Transfer Protocol"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"IA"})," Information Assurance"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"IP"})," Internet Protocol"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"IPv4"})," Internet Protocol, version 4. The commonly-used version of IP, in which addresses consist of\tfour integers from zero to 255 (inclusive), separated by periods, such as 192.168.123.4"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"JCE"})," Java Cryptography Extensions"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"JDK"})," Java Development Kit, a JRE with additional tools and libraries."]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"JKS"})," Java Key Store"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"JRE"})," Java Runtime Environment"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"KML"})," Keyhole Markup Language, the XML-based data format used by Google Earth"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"OS"})," Operating System"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"OWASP"})," Open Web Application Security Project"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"NIAP"})," National Information Assurance Partnership"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"PKCS12"})," Public-Key Cryptography Standard #12"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"TCP"})," Transmission Control Protocol"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"RHEL"})," Red Hat Enterprise Linux"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"UDP"})," User Datagram Protocol"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"SSL"})," Secure Sockets Layer"]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"TAK"})," Team Awareness Kit, a mobile or desktop application that sends and receives real-time information through TAK Server."]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"TLS"})," Transport Layer Security, a newer and more-secure protocol derived from SSL. The terms SSL and TLS are often used interchangeably. Technically, TLS provides a superset of SSL's capabilities and should always be preferred."]}),"\n",(0,s.jsxs)(e.li,{children:[(0,s.jsx)(e.strong,{children:"XML"})," Extensible Markup Language"]}),"\n"]})]})}function h(n={}){const{wrapper:e}={...(0,i.a)(),...n.components};return e?(0,s.jsx)(e,{...n,children:(0,s.jsx)(d,{...n})}):d(n)}},1151:(n,e,r)=>{r.d(e,{Z:()=>a,a:()=>o});var s=r(7294);const i={},t=s.createContext(i);function o(n){const e=s.useContext(t);return s.useMemo((function(){return"function"==typeof n?n(e):{...e,...n}}),[e,n])}function a(n){let e;return e=n.disableParentContext?"function"==typeof n.components?n.components(i):n.components||i:o(n.components),s.createElement(t.Provider,{value:e},n.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/608ae6a4.777bc2ca.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/608ae6a4.777bc2ca.js new file mode 100644 index 00000000..2a4de104 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/608ae6a4.777bc2ca.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6938],{4545:s=>{s.exports=JSON.parse('{"permalink":"/blog/tags/docusaurus","page":1,"postsPerPage":10,"totalPages":1,"totalCount":4,"blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/642e9f94.e1061e45.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/642e9f94.e1061e45.js new file mode 100644 index 00000000..400b8904 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/642e9f94.e1061e45.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7861],{8111:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>u,frontMatter:()=>r,metadata:()=>a,toc:()=>l});var o=n(5893),s=n(1151);const r={},i="Overview",a={id:"installation/twoserver/overview",title:"Overview",description:"Follow the procedures in the following two sections to install the database server, and the messaging server. For AWS / cloud installation, see recommended instance type on page 4. Use this instance type for both servers.",source:"@site/docs/installation/twoserver/overview.md",sourceDirName:"installation/twoserver",slug:"/installation/twoserver/overview",permalink:"/docs/installation/twoserver/overview",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/twoserver/overview.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Configure TAK Server Installation",permalink:"/docs/installation/oneserver/takserverconfiguration"},next:{title:"Dependency Setup",permalink:"/docs/installation/twoserver/serverone/dependencysetup"}},c={},l=[];function d(e){const t={h1:"h1",p:"p",...(0,s.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"overview",children:"Overview"}),"\n",(0,o.jsx)(t.p,{children:"Follow the procedures in the following two sections to install the database server, and the messaging server. For AWS / cloud installation, see recommended instance type on page 4. Use this instance type for both servers."})]})}function u(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>i});var o=n(7294);const s={},r=o.createContext(s);function i(e){const t=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:i(e.components),o.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/660a8848.59981709.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/660a8848.59981709.js new file mode 100644 index 00000000..c7a40034 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/660a8848.59981709.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1881],{682:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>i,default:()=>p,frontMatter:()=>s,metadata:()=>a,toc:()=>u});var n=r(5893),o=r(1151);const s={},i="Overview",a={id:"upgrade/overview",title:"Overview",description:"Follow this procedure to upgrade a system running TAK Server.",source:"@site/docs/upgrade/overview.md",sourceDirName:"upgrade",slug:"/upgrade/overview",permalink:"/docs/upgrade/overview",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/upgrade/overview.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Use Setup Wizard to Configure TAK Server",permalink:"/docs/installation/setup_wizard"},next:{title:"Single-Server Upgrade",permalink:"/docs/upgrade/singleserver"}},c={},u=[];function d(e){const t={h1:"h1",p:"p",...(0,o.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"overview",children:"Overview"}),"\n",(0,n.jsx)(t.p,{children:"Follow this procedure to upgrade a system running TAK Server."})]})}function p(e={}){const{wrapper:t}={...(0,o.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,r)=>{r.d(t,{Z:()=>a,a:()=>i});var n=r(7294);const o={},s=n.createContext(o);function i(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:i(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/66406991.83a2d198.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/66406991.83a2d198.js new file mode 100644 index 00000000..803eb979 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/66406991.83a2d198.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[110],{711:s=>{s.exports=JSON.parse('{"permalink":"/blog/tags/hello","page":1,"postsPerPage":10,"totalPages":1,"totalCount":3,"blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/6875c492.133c1817.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/6875c492.133c1817.js new file mode 100644 index 00000000..e2d71b5f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/6875c492.133c1817.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8610],{9703:(e,t,n)=>{n.d(t,{Z:()=>r});n(7294);var s=n(5999),a=n(2244),i=n(5893);function r(e){const{metadata:t}=e,{previousPage:n,nextPage:r}=t;return(0,i.jsxs)("nav",{className:"pagination-nav","aria-label":(0,s.I)({id:"theme.blog.paginator.navAriaLabel",message:"Blog list page navigation",description:"The ARIA label for the blog pagination"}),children:[n&&(0,i.jsx)(a.Z,{permalink:n,title:(0,i.jsx)(s.Z,{id:"theme.blog.paginator.newerEntries",description:"The label used to navigate to the newer blog posts page (previous page)",children:"Newer Entries"})}),r&&(0,i.jsx)(a.Z,{permalink:r,title:(0,i.jsx)(s.Z,{id:"theme.blog.paginator.olderEntries",description:"The label used to navigate to the older blog posts page (next page)",children:"Older Entries"}),isNext:!0})]})}},9985:(e,t,n)=>{n.d(t,{Z:()=>r});n(7294);var s=n(9460),a=n(390),i=n(5893);function r(e){let{items:t,component:n=a.Z}=e;return(0,i.jsx)(i.Fragment,{children:t.map((e=>{let{content:t}=e;return(0,i.jsx)(s.n,{content:t,children:(0,i.jsx)(n,{children:(0,i.jsx)(t,{})})},t.metadata.permalink)}))})}},1714:(e,t,n)=>{n.r(t),n.d(t,{default:()=>f});n(7294);var s=n(512),a=n(5999),i=n(8824),r=n(1944),l=n(5281),o=n(3692),c=n(1460),d=n(9703),g=n(197),u=n(9985),h=n(2212),p=n(2503),m=n(5893);function x(e){const t=function(){const{selectMessage:e}=(0,i.c)();return t=>e(t,(0,a.I)({id:"theme.blog.post.plurals",description:'Pluralized label for "{count} posts". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',message:"One post|{count} posts"},{count:t}))}();return(0,a.I)({id:"theme.blog.tagTitle",description:"The title of the page for a blog tag",message:'{nPosts} tagged with "{tagName}"'},{nPosts:t(e.count),tagName:e.label})}function j(e){let{tag:t}=e;const n=x(t);return(0,m.jsxs)(m.Fragment,{children:[(0,m.jsx)(r.d,{title:n}),(0,m.jsx)(g.Z,{tag:"blog_tags_posts"})]})}function b(e){let{tag:t,items:n,sidebar:s,listMetadata:i}=e;const r=x(t);return(0,m.jsxs)(c.Z,{sidebar:s,children:[t.unlisted&&(0,m.jsx)(h.Z,{}),(0,m.jsxs)("header",{className:"margin-bottom--xl",children:[(0,m.jsx)(p.Z,{as:"h1",children:r}),(0,m.jsx)(o.Z,{href:t.allTagsPath,children:(0,m.jsx)(a.Z,{id:"theme.tags.tagsPageLink",description:"The label of the link targeting the tag list page",children:"View All Tags"})})]}),(0,m.jsx)(u.Z,{items:n}),(0,m.jsx)(d.Z,{metadata:i})]})}function f(e){return(0,m.jsxs)(r.FG,{className:(0,s.Z)(l.k.wrapper.blogPages,l.k.page.blogTagPostListPage),children:[(0,m.jsx)(j,{...e}),(0,m.jsx)(b,{...e})]})}},2212:(e,t,n)=>{n.d(t,{Z:()=>h});n(7294);var s=n(512),a=n(5999),i=n(5742),r=n(5893);function l(){return(0,r.jsx)(a.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function o(){return(0,r.jsx)(a.Z,{id:"theme.unlistedContent.message",description:"The unlisted content banner message",children:"This page is unlisted. Search engines will not index it, and only users having a direct link can access it."})}function c(){return(0,r.jsx)(i.Z,{children:(0,r.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})}var d=n(5281),g=n(9047);function u(e){let{className:t}=e;return(0,r.jsx)(g.Z,{type:"caution",title:(0,r.jsx)(l,{}),className:(0,s.Z)(t,d.k.common.unlistedBanner),children:(0,r.jsx)(o,{})})}function h(e){return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(c,{}),(0,r.jsx)(u,{...e})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/73664a40.5bf78c3a.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/73664a40.5bf78c3a.js new file mode 100644 index 00000000..2409468a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/73664a40.5bf78c3a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3514],{1985:(e,s,i)=>{i.r(s),i.d(s,{assets:()=>m,contentTitle:()=>n,default:()=>a,frontMatter:()=>u,metadata:()=>r,toc:()=>l});var t=i(5893),o=i(1151);const u={slug:"long-blog-post",title:"Long Blog Post",authors:"endi",tags:["hello","docusaurus"]},n=void 0,r={permalink:"/blog/long-blog-post",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2019-05-29-long-blog-post.md",source:"@site/blog/2019-05-29-long-blog-post.md",title:"Long Blog Post",description:"This is the summary of a very long blog post,",date:"2019-05-29T00:00:00.000Z",formattedDate:"May 29, 2019",tags:[{label:"hello",permalink:"/blog/tags/hello"},{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:2.05,hasTruncateMarker:!0,authors:[{name:"Endilie Yacop Sucipto",title:"Maintainer of Docusaurus",url:"https://github.com/endiliey",imageURL:"https://github.com/endiliey.png",key:"endi"}],frontMatter:{slug:"long-blog-post",title:"Long Blog Post",authors:"endi",tags:["hello","docusaurus"]},unlisted:!1,prevItem:{title:"MDX Blog Post",permalink:"/blog/mdx-blog-post"},nextItem:{title:"First Blog Post",permalink:"/blog/first-blog-post"}},m={authorsImageUrls:[void 0]},l=[];function c(e){const s={code:"code",p:"p",...(0,o.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.p,{children:"This is the summary of a very long blog post,"}),"\n",(0,t.jsxs)(s.p,{children:["Use a ",(0,t.jsx)(s.code,{children:"\x3c!--"})," ",(0,t.jsx)(s.code,{children:"truncate"})," ",(0,t.jsx)(s.code,{children:"--\x3e"})," comment to limit blog post size in the list view."]}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}),"\n",(0,t.jsx)(s.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"})]})}function a(e={}){const{wrapper:s}={...(0,o.a)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},1151:(e,s,i)=>{i.d(s,{Z:()=>r,a:()=>n});var t=i(7294);const o={},u=t.createContext(o);function n(e){const s=t.useContext(u);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function r(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:n(e.components),t.createElement(u.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7395.6a5d886e.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7395.6a5d886e.js new file mode 100644 index 00000000..56da0063 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7395.6a5d886e.js @@ -0,0 +1 @@ +(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7395],{9047:(e,t,n)=>{"use strict";n.d(t,{Z:()=>Z});var s=n(7294),o=n(5893);function c(e){const{mdxAdmonitionTitle:t,rest:n}=function(e){const t=s.Children.toArray(e),n=t.find((e=>s.isValidElement(e)&&"mdxAdmonitionTitle"===e.type)),c=t.filter((e=>e!==n)),a=n?.props.children;return{mdxAdmonitionTitle:a,rest:c.length>0?(0,o.jsx)(o.Fragment,{children:c}):null}}(e.children),c=e.title??t;return{...e,...c&&{title:c},children:n}}var a=n(512),r=n(5999),i=n(5281);const l={admonition:"admonition_xJq3",admonitionHeading:"admonitionHeading_Gvgb",admonitionIcon:"admonitionIcon_Rf37",admonitionContent:"admonitionContent_BuS1"};function d(e){let{type:t,className:n,children:s}=e;return(0,o.jsx)("div",{className:(0,a.Z)(i.k.common.admonition,i.k.common.admonitionType(t),l.admonition,n),children:s})}function u(e){let{icon:t,title:n}=e;return(0,o.jsxs)("div",{className:l.admonitionHeading,children:[(0,o.jsx)("span",{className:l.admonitionIcon,children:t}),n]})}function m(e){let{children:t}=e;return t?(0,o.jsx)("div",{className:l.admonitionContent,children:t}):null}function h(e){const{type:t,icon:n,title:s,children:c,className:a}=e;return(0,o.jsxs)(d,{type:t,className:a,children:[(0,o.jsx)(u,{title:s,icon:n}),(0,o.jsx)(m,{children:c})]})}function f(e){return(0,o.jsx)("svg",{viewBox:"0 0 14 16",...e,children:(0,o.jsx)("path",{fillRule:"evenodd",d:"M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"})})}const p={icon:(0,o.jsx)(f,{}),title:(0,o.jsx)(r.Z,{id:"theme.admonition.note",description:"The default label used for the Note admonition (:::note)",children:"note"})};function x(e){return(0,o.jsx)(h,{...p,...e,className:(0,a.Z)("alert alert--secondary",e.className),children:e.children})}function b(e){return(0,o.jsx)("svg",{viewBox:"0 0 12 16",...e,children:(0,o.jsx)("path",{fillRule:"evenodd",d:"M6.5 0C3.48 0 1 2.19 1 5c0 .92.55 2.25 1 3 1.34 2.25 1.78 2.78 2 4v1h5v-1c.22-1.22.66-1.75 2-4 .45-.75 1-2.08 1-3 0-2.81-2.48-5-5.5-5zm3.64 7.48c-.25.44-.47.8-.67 1.11-.86 1.41-1.25 2.06-1.45 3.23-.02.05-.02.11-.02.17H5c0-.06 0-.13-.02-.17-.2-1.17-.59-1.83-1.45-3.23-.2-.31-.42-.67-.67-1.11C2.44 6.78 2 5.65 2 5c0-2.2 2.02-4 4.5-4 1.22 0 2.36.42 3.22 1.19C10.55 2.94 11 3.94 11 5c0 .66-.44 1.78-.86 2.48zM4 14h5c-.23 1.14-1.3 2-2.5 2s-2.27-.86-2.5-2z"})})}const g={icon:(0,o.jsx)(b,{}),title:(0,o.jsx)(r.Z,{id:"theme.admonition.tip",description:"The default label used for the Tip admonition (:::tip)",children:"tip"})};function j(e){return(0,o.jsx)(h,{...g,...e,className:(0,a.Z)("alert alert--success",e.className),children:e.children})}function v(e){return(0,o.jsx)("svg",{viewBox:"0 0 14 16",...e,children:(0,o.jsx)("path",{fillRule:"evenodd",d:"M7 2.3c3.14 0 5.7 2.56 5.7 5.7s-2.56 5.7-5.7 5.7A5.71 5.71 0 0 1 1.3 8c0-3.14 2.56-5.7 5.7-5.7zM7 1C3.14 1 0 4.14 0 8s3.14 7 7 7 7-3.14 7-7-3.14-7-7-7zm1 3H6v5h2V4zm0 6H6v2h2v-2z"})})}const y={icon:(0,o.jsx)(v,{}),title:(0,o.jsx)(r.Z,{id:"theme.admonition.info",description:"The default label used for the Info admonition (:::info)",children:"info"})};function N(e){return(0,o.jsx)(h,{...y,...e,className:(0,a.Z)("alert alert--info",e.className),children:e.children})}function k(e){return(0,o.jsx)("svg",{viewBox:"0 0 16 16",...e,children:(0,o.jsx)("path",{fillRule:"evenodd",d:"M8.893 1.5c-.183-.31-.52-.5-.887-.5s-.703.19-.886.5L.138 13.499a.98.98 0 0 0 0 1.001c.193.31.53.501.886.501h13.964c.367 0 .704-.19.877-.5a1.03 1.03 0 0 0 .01-1.002L8.893 1.5zm.133 11.497H6.987v-2.003h2.039v2.003zm0-3.004H6.987V5.987h2.039v4.006z"})})}const B={icon:(0,o.jsx)(k,{}),title:(0,o.jsx)(r.Z,{id:"theme.admonition.warning",description:"The default label used for the Warning admonition (:::warning)",children:"warning"})};function C(e){return(0,o.jsx)("svg",{viewBox:"0 0 12 16",...e,children:(0,o.jsx)("path",{fillRule:"evenodd",d:"M5.05.31c.81 2.17.41 3.38-.52 4.31C3.55 5.67 1.98 6.45.9 7.98c-1.45 2.05-1.7 6.53 3.53 7.7-2.2-1.16-2.67-4.52-.3-6.61-.61 2.03.53 3.33 1.94 2.86 1.39-.47 2.3.53 2.27 1.67-.02.78-.31 1.44-1.13 1.81 3.42-.59 4.78-3.42 4.78-5.56 0-2.84-2.53-3.22-1.25-5.61-1.52.13-2.03 1.13-1.89 2.75.09 1.08-1.02 1.8-1.86 1.33-.67-.41-.66-1.19-.06-1.78C8.18 5.31 8.68 2.45 5.05.32L5.03.3l.02.01z"})})}const w={icon:(0,o.jsx)(C,{}),title:(0,o.jsx)(r.Z,{id:"theme.admonition.danger",description:"The default label used for the Danger admonition (:::danger)",children:"danger"})};const L={icon:(0,o.jsx)(k,{}),title:(0,o.jsx)(r.Z,{id:"theme.admonition.caution",description:"The default label used for the Caution admonition (:::caution)",children:"caution"})};const E={...{note:x,tip:j,info:N,warning:function(e){return(0,o.jsx)(h,{...B,...e,className:(0,a.Z)("alert alert--warning",e.className),children:e.children})},danger:function(e){return(0,o.jsx)(h,{...w,...e,className:(0,a.Z)("alert alert--danger",e.className),children:e.children})}},...{secondary:e=>(0,o.jsx)(x,{title:"secondary",...e}),important:e=>(0,o.jsx)(N,{title:"important",...e}),success:e=>(0,o.jsx)(j,{title:"success",...e}),caution:function(e){return(0,o.jsx)(h,{...L,...e,className:(0,a.Z)("alert alert--warning",e.className),children:e.children})}}};function Z(e){const t=c(e),n=(s=t.type,E[s]||(console.warn(`No admonition component found for admonition type "${s}". Using Info as fallback.`),E.info));var s;return(0,o.jsx)(n,{...t})}},7395:(e,t,n)=>{"use strict";n.d(t,{Z:()=>ie});var s=n(7294),o=n(1151),c=n(5742),a=n(2389),r=n(512),i=n(2949),l=n(6668);function d(){const{prism:e}=(0,l.L)(),{colorMode:t}=(0,i.I)(),n=e.theme,s=e.darkTheme||n;return"dark"===t?s:n}var u=n(5281),m=n(7594),h=n.n(m);const f=/title=(?["'])(?.*?)\1/,p=/\{(?<range>[\d,-]+)\}/,x={js:{start:"\\/\\/",end:""},jsBlock:{start:"\\/\\*",end:"\\*\\/"},jsx:{start:"\\{\\s*\\/\\*",end:"\\*\\/\\s*\\}"},bash:{start:"#",end:""},html:{start:"\x3c!--",end:"--\x3e"}},b={...x,lua:{start:"--",end:""},wasm:{start:"\\;\\;",end:""},tex:{start:"%",end:""},vb:{start:"['\u2018\u2019]",end:""},vbnet:{start:"(?:_\\s*)?['\u2018\u2019]",end:""},rem:{start:"[Rr][Ee][Mm]\\b",end:""},f90:{start:"!",end:""},ml:{start:"\\(\\*",end:"\\*\\)"},cobol:{start:"\\*>",end:""}},g=Object.keys(x);function j(e,t){const n=e.map((e=>{const{start:n,end:s}=b[e];return`(?:${n}\\s*(${t.flatMap((e=>[e.line,e.block?.start,e.block?.end].filter(Boolean))).join("|")})\\s*${s})`})).join("|");return new RegExp(`^\\s*(?:${n})\\s*$`)}function v(e,t){let n=e.replace(/\n$/,"");const{language:s,magicComments:o,metastring:c}=t;if(c&&p.test(c)){const e=c.match(p).groups.range;if(0===o.length)throw new Error(`A highlight range has been given in code block's metastring (\`\`\` ${c}), but no magic comment config is available. Docusaurus applies the first magic comment entry's className for metastring ranges.`);const t=o[0].className,s=h()(e).filter((e=>e>0)).map((e=>[e-1,[t]]));return{lineClassNames:Object.fromEntries(s),code:n}}if(void 0===s)return{lineClassNames:{},code:n};const a=function(e,t){switch(e){case"js":case"javascript":case"ts":case"typescript":return j(["js","jsBlock"],t);case"jsx":case"tsx":return j(["js","jsBlock","jsx"],t);case"html":return j(["js","jsBlock","html"],t);case"python":case"py":case"bash":return j(["bash"],t);case"markdown":case"md":return j(["html","jsx","bash"],t);case"tex":case"latex":case"matlab":return j(["tex"],t);case"lua":case"haskell":case"sql":return j(["lua"],t);case"wasm":return j(["wasm"],t);case"vb":case"vba":case"visual-basic":return j(["vb","rem"],t);case"vbnet":return j(["vbnet","rem"],t);case"batch":return j(["rem"],t);case"basic":return j(["rem","f90"],t);case"fsharp":return j(["js","ml"],t);case"ocaml":case"sml":return j(["ml"],t);case"fortran":return j(["f90"],t);case"cobol":return j(["cobol"],t);default:return j(g,t)}}(s,o),r=n.split("\n"),i=Object.fromEntries(o.map((e=>[e.className,{start:0,range:""}]))),l=Object.fromEntries(o.filter((e=>e.line)).map((e=>{let{className:t,line:n}=e;return[n,t]}))),d=Object.fromEntries(o.filter((e=>e.block)).map((e=>{let{className:t,block:n}=e;return[n.start,t]}))),u=Object.fromEntries(o.filter((e=>e.block)).map((e=>{let{className:t,block:n}=e;return[n.end,t]})));for(let h=0;h<r.length;){const e=r[h].match(a);if(!e){h+=1;continue}const t=e.slice(1).find((e=>void 0!==e));l[t]?i[l[t]].range+=`${h},`:d[t]?i[d[t]].start=h:u[t]&&(i[u[t]].range+=`${i[u[t]].start}-${h-1},`),r.splice(h,1)}n=r.join("\n");const m={};return Object.entries(i).forEach((e=>{let[t,{range:n}]=e;h()(n).forEach((e=>{m[e]??=[],m[e].push(t)}))})),{lineClassNames:m,code:n}}const y={codeBlockContainer:"codeBlockContainer_Ckt0"};var N=n(5893);function k(e){let{as:t,...n}=e;const s=function(e){const t={color:"--prism-color",backgroundColor:"--prism-background-color"},n={};return Object.entries(e.plain).forEach((e=>{let[s,o]=e;const c=t[s];c&&"string"==typeof o&&(n[c]=o)})),n}(d());return(0,N.jsx)(t,{...n,style:s,className:(0,r.Z)(n.className,y.codeBlockContainer,u.k.common.codeBlock)})}const B={codeBlockContent:"codeBlockContent_biex",codeBlockTitle:"codeBlockTitle_Ktv7",codeBlock:"codeBlock_bY9V",codeBlockStandalone:"codeBlockStandalone_MEMb",codeBlockLines:"codeBlockLines_e6Vv",codeBlockLinesWithNumbering:"codeBlockLinesWithNumbering_o6Pm",buttonGroup:"buttonGroup__atx"};function C(e){let{children:t,className:n}=e;return(0,N.jsx)(k,{as:"pre",tabIndex:0,className:(0,r.Z)(B.codeBlockStandalone,"thin-scrollbar",n),children:(0,N.jsx)("code",{className:B.codeBlockLines,children:t})})}var w=n(902);const L={attributes:!0,characterData:!0,childList:!0,subtree:!0};function E(e,t){const[n,o]=(0,s.useState)(),c=(0,s.useCallback)((()=>{o(e.current?.closest("[role=tabpanel][hidden]"))}),[e,o]);(0,s.useEffect)((()=>{c()}),[c]),function(e,t,n){void 0===n&&(n=L);const o=(0,w.zX)(t),c=(0,w.Ql)(n);(0,s.useEffect)((()=>{const t=new MutationObserver(o);return e&&t.observe(e,c),()=>t.disconnect()}),[e,o,c])}(n,(e=>{e.forEach((e=>{"attributes"===e.type&&"hidden"===e.attributeName&&(t(),c())}))}),{attributes:!0,characterData:!1,childList:!1,subtree:!1})}var Z=n(2573);const _={codeLine:"codeLine_lJS_",codeLineNumber:"codeLineNumber_Tfdd",codeLineContent:"codeLineContent_feaV"};function T(e){let{line:t,classNames:n,showLineNumbers:s,getLineProps:o,getTokenProps:c}=e;1===t.length&&"\n"===t[0].content&&(t[0].content="");const a=o({line:t,className:(0,r.Z)(n,s&&_.codeLine)}),i=t.map(((e,t)=>(0,N.jsx)("span",{...c({token:e,key:t})},t)));return(0,N.jsxs)("span",{...a,children:[s?(0,N.jsxs)(N.Fragment,{children:[(0,N.jsx)("span",{className:_.codeLineNumber}),(0,N.jsx)("span",{className:_.codeLineContent,children:i})]}):i,(0,N.jsx)("br",{})]})}var S=n(5999);function A(e){return(0,N.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,N.jsx)("path",{fill:"currentColor",d:"M19,21H8V7H19M19,5H8A2,2 0 0,0 6,7V21A2,2 0 0,0 8,23H19A2,2 0 0,0 21,21V7A2,2 0 0,0 19,5M16,1H4A2,2 0 0,0 2,3V17H4V3H16V1Z"})})}function I(e){return(0,N.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,N.jsx)("path",{fill:"currentColor",d:"M21,7L9,19L3.5,13.5L4.91,12.09L9,16.17L19.59,5.59L21,7Z"})})}const M={copyButtonCopied:"copyButtonCopied_obH4",copyButtonIcons:"copyButtonIcons_eSgA",copyButtonIcon:"copyButtonIcon_y97N",copyButtonSuccessIcon:"copyButtonSuccessIcon_LjdS"};function z(e){let{code:t,className:n}=e;const[o,c]=(0,s.useState)(!1),a=(0,s.useRef)(void 0),i=(0,s.useCallback)((()=>{!function(e,t){let{target:n=document.body}=void 0===t?{}:t;if("string"!=typeof e)throw new TypeError(`Expected parameter \`text\` to be a \`string\`, got \`${typeof e}\`.`);const s=document.createElement("textarea"),o=document.activeElement;s.value=e,s.setAttribute("readonly",""),s.style.contain="strict",s.style.position="absolute",s.style.left="-9999px",s.style.fontSize="12pt";const c=document.getSelection(),a=c.rangeCount>0&&c.getRangeAt(0);n.append(s),s.select(),s.selectionStart=0,s.selectionEnd=e.length;let r=!1;try{r=document.execCommand("copy")}catch{}s.remove(),a&&(c.removeAllRanges(),c.addRange(a)),o&&o.focus()}(t),c(!0),a.current=window.setTimeout((()=>{c(!1)}),1e3)}),[t]);return(0,s.useEffect)((()=>()=>window.clearTimeout(a.current)),[]),(0,N.jsx)("button",{type:"button","aria-label":o?(0,S.I)({id:"theme.CodeBlock.copied",message:"Copied",description:"The copied button label on code blocks"}):(0,S.I)({id:"theme.CodeBlock.copyButtonAriaLabel",message:"Copy code to clipboard",description:"The ARIA label for copy code blocks button"}),title:(0,S.I)({id:"theme.CodeBlock.copy",message:"Copy",description:"The copy button label on code blocks"}),className:(0,r.Z)("clean-btn",n,M.copyButton,o&&M.copyButtonCopied),onClick:i,children:(0,N.jsxs)("span",{className:M.copyButtonIcons,"aria-hidden":"true",children:[(0,N.jsx)(A,{className:M.copyButtonIcon}),(0,N.jsx)(I,{className:M.copyButtonSuccessIcon})]})})}function H(e){return(0,N.jsx)("svg",{viewBox:"0 0 24 24",...e,children:(0,N.jsx)("path",{fill:"currentColor",d:"M4 19h6v-2H4v2zM20 5H4v2h16V5zm-3 6H4v2h13.25c1.1 0 2 .9 2 2s-.9 2-2 2H15v-2l-3 3l3 3v-2h2c2.21 0 4-1.79 4-4s-1.79-4-4-4z"})})}const R={wordWrapButtonIcon:"wordWrapButtonIcon_Bwma",wordWrapButtonEnabled:"wordWrapButtonEnabled_EoeP"};function V(e){let{className:t,onClick:n,isEnabled:s}=e;const o=(0,S.I)({id:"theme.CodeBlock.wordWrapToggle",message:"Toggle word wrap",description:"The title attribute for toggle word wrapping button of code block lines"});return(0,N.jsx)("button",{type:"button",onClick:n,className:(0,r.Z)("clean-btn",t,s&&R.wordWrapButtonEnabled),"aria-label":o,title:o,children:(0,N.jsx)(H,{className:R.wordWrapButtonIcon,"aria-hidden":"true"})})}function $(e){let{children:t,className:n="",metastring:o,title:c,showLineNumbers:a,language:i}=e;const{prism:{defaultLanguage:u,magicComments:m}}=(0,l.L)(),h=function(e){return e?.toLowerCase()}(i??function(e){const t=e.split(" ").find((e=>e.startsWith("language-")));return t?.replace(/language-/,"")}(n)??u),p=d(),x=function(){const[e,t]=(0,s.useState)(!1),[n,o]=(0,s.useState)(!1),c=(0,s.useRef)(null),a=(0,s.useCallback)((()=>{const n=c.current.querySelector("code");e?n.removeAttribute("style"):(n.style.whiteSpace="pre-wrap",n.style.overflowWrap="anywhere"),t((e=>!e))}),[c,e]),r=(0,s.useCallback)((()=>{const{scrollWidth:e,clientWidth:t}=c.current,n=e>t||c.current.querySelector("code").hasAttribute("style");o(n)}),[c]);return E(c,r),(0,s.useEffect)((()=>{r()}),[e,r]),(0,s.useEffect)((()=>(window.addEventListener("resize",r,{passive:!0}),()=>{window.removeEventListener("resize",r)})),[r]),{codeBlockRef:c,isEnabled:e,isCodeScrollable:n,toggle:a}}(),b=function(e){return e?.match(f)?.groups.title??""}(o)||c,{lineClassNames:g,code:j}=v(t,{metastring:o,language:h,magicComments:m}),y=a??function(e){return Boolean(e?.includes("showLineNumbers"))}(o);return(0,N.jsxs)(k,{as:"div",className:(0,r.Z)(n,h&&!n.includes(`language-${h}`)&&`language-${h}`),children:[b&&(0,N.jsx)("div",{className:B.codeBlockTitle,children:b}),(0,N.jsxs)("div",{className:B.codeBlockContent,children:[(0,N.jsx)(Z.y$,{theme:p,code:j,language:h??"text",children:e=>{let{className:t,style:n,tokens:s,getLineProps:o,getTokenProps:c}=e;return(0,N.jsx)("pre",{tabIndex:0,ref:x.codeBlockRef,className:(0,r.Z)(t,B.codeBlock,"thin-scrollbar"),style:n,children:(0,N.jsx)("code",{className:(0,r.Z)(B.codeBlockLines,y&&B.codeBlockLinesWithNumbering),children:s.map(((e,t)=>(0,N.jsx)(T,{line:e,getLineProps:o,getTokenProps:c,classNames:g[t],showLineNumbers:y},t)))})})}}),(0,N.jsxs)("div",{className:B.buttonGroup,children:[(x.isEnabled||x.isCodeScrollable)&&(0,N.jsx)(V,{className:B.codeButton,onClick:()=>x.toggle(),isEnabled:x.isEnabled}),(0,N.jsx)(z,{className:B.codeButton,code:j})]})]})]})}function W(e){let{children:t,...n}=e;const o=(0,a.Z)(),c=function(e){return s.Children.toArray(e).some((e=>(0,s.isValidElement)(e)))?e:Array.isArray(e)?e.join(""):e}(t),r="string"==typeof c?$:C;return(0,N.jsx)(r,{...n,children:c},String(o))}function P(e){return(0,N.jsx)("code",{...e})}var D=n(3692);var O=n(8138),q=n(6043);const F={details:"details_lb9f",isBrowser:"isBrowser_bmU9",collapsibleContent:"collapsibleContent_i85q"};function G(e){return!!e&&("SUMMARY"===e.tagName||G(e.parentElement))}function U(e,t){return!!e&&(e===t||U(e.parentElement,t))}function J(e){let{summary:t,children:n,...o}=e;(0,O.Z)().collectAnchor(o.id);const c=(0,a.Z)(),i=(0,s.useRef)(null),{collapsed:l,setCollapsed:d}=(0,q.u)({initialState:!o.open}),[u,m]=(0,s.useState)(o.open),h=s.isValidElement(t)?t:(0,N.jsx)("summary",{children:t??"Details"});return(0,N.jsxs)("details",{...o,ref:i,open:u,"data-collapsed":l,className:(0,r.Z)(F.details,c&&F.isBrowser,o.className),onMouseDown:e=>{G(e.target)&&e.detail>1&&e.preventDefault()},onClick:e=>{e.stopPropagation();const t=e.target;G(t)&&U(t,i.current)&&(e.preventDefault(),l?(d(!1),m(!0)):d(!0))},children:[h,(0,N.jsx)(q.z,{lazy:!1,collapsed:l,disableSSRStyle:!0,onCollapseTransitionEnd:e=>{d(e),m(!e)},children:(0,N.jsx)("div",{className:F.collapsibleContent,children:n})})]})}const Y={details:"details_b_Ee"},K="alert alert--info";function Q(e){let{...t}=e;return(0,N.jsx)(J,{...t,className:(0,r.Z)(K,Y.details,t.className)})}function X(e){const t=s.Children.toArray(e.children),n=t.find((e=>s.isValidElement(e)&&"summary"===e.type)),o=(0,N.jsx)(N.Fragment,{children:t.filter((e=>e!==n))});return(0,N.jsx)(Q,{...e,summary:n,children:o})}var ee=n(2503);function te(e){return(0,N.jsx)(ee.Z,{...e})}const ne={containsTaskList:"containsTaskList_mC6p"};function se(e){if(void 0!==e)return(0,r.Z)(e,e?.includes("contains-task-list")&&ne.containsTaskList)}const oe={img:"img_ev3q"};var ce=n(9047),ae=n(1875);const re={Head:c.Z,details:X,Details:X,code:function(e){return function(e){return void 0!==e.children&&s.Children.toArray(e.children).every((e=>"string"==typeof e&&!e.includes("\n")))}(e)?(0,N.jsx)(P,{...e}):(0,N.jsx)(W,{...e})},a:function(e){return(0,N.jsx)(D.Z,{...e})},pre:function(e){return(0,N.jsx)(N.Fragment,{children:e.children})},ul:function(e){return(0,N.jsx)("ul",{...e,className:se(e.className)})},li:function(e){return(0,O.Z)().collectAnchor(e.id),(0,N.jsx)("li",{...e})},img:function(e){return(0,N.jsx)("img",{decoding:"async",loading:"lazy",...e,className:(t=e.className,(0,r.Z)(t,oe.img))});var t},h1:e=>(0,N.jsx)(te,{as:"h1",...e}),h2:e=>(0,N.jsx)(te,{as:"h2",...e}),h3:e=>(0,N.jsx)(te,{as:"h3",...e}),h4:e=>(0,N.jsx)(te,{as:"h4",...e}),h5:e=>(0,N.jsx)(te,{as:"h5",...e}),h6:e=>(0,N.jsx)(te,{as:"h6",...e}),admonition:ce.Z,mermaid:ae.Z};function ie(e){let{children:t}=e;return(0,N.jsx)(o.Z,{components:re,children:t})}},7594:(e,t)=>{function n(e){let t,n=[];for(let s of e.split(",").map((e=>e.trim())))if(/^-?\d+$/.test(s))n.push(parseInt(s,10));else if(t=s.match(/^(-?\d+)(-|\.\.\.?|\u2025|\u2026|\u22EF)(-?\d+)$/)){let[e,s,o,c]=t;if(s&&c){s=parseInt(s),c=parseInt(c);const e=s<c?1:-1;"-"!==o&&".."!==o&&"\u2025"!==o||(c+=e);for(let t=s;t!==c;t+=e)n.push(t)}}return n}t.default=n,e.exports=n},1151:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r,a:()=>a});var s=n(7294);const o={},c=s.createContext(o);function a(e){const t=s.useContext(c);return s.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),s.createElement(c.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7661071f.9856c1fe.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7661071f.9856c1fe.js new file mode 100644 index 00000000..f622ebcd --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7661071f.9856c1fe.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9642],{3174:(e,o,s)=>{s.r(o),s.d(o,{assets:()=>c,contentTitle:()=>l,default:()=>d,frontMatter:()=>a,metadata:()=>r,toc:()=>u});var t=s(5893),n=s(1151);const a={slug:"welcome",title:"Welcome to Docusaurus!",authors:["slorber","yangshun"],tags:["facebook","hello","docusaurus"]},l=void 0,r={permalink:"/blog/welcome",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2021-08-26-welcome/index.md",source:"@site/blog/2021-08-26-welcome/index.md",title:"Welcome to Docusaurus!",description:"Docusaurus blogging features are powered by the blog plugin.",date:"2021-08-26T00:00:00.000Z",formattedDate:"August 26, 2021",tags:[{label:"facebook",permalink:"/blog/tags/facebook"},{label:"hello",permalink:"/blog/tags/hello"},{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:.405,hasTruncateMarker:!1,authors:[{name:"S\xe9bastien Lorber",title:"Docusaurus maintainer",url:"https://sebastienlorber.com",imageURL:"https://github.com/slorber.png",key:"slorber"},{name:"Yangshun Tay",title:"Front End Engineer @ Facebook",url:"https://github.com/yangshun",imageURL:"https://github.com/yangshun.png",key:"yangshun"}],frontMatter:{slug:"welcome",title:"Welcome to Docusaurus!",authors:["slorber","yangshun"],tags:["facebook","hello","docusaurus"]},unlisted:!1,prevItem:{title:"Welcome to Docusaurus-Static!",permalink:"/blog/2024/01/28/welcome-to-docusaurus-static"},nextItem:{title:"MDX Blog Post",permalink:"/blog/mdx-blog-post"}},c={authorsImageUrls:[void 0,void 0]},u=[];function i(e){const o={a:"a",code:"code",img:"img",li:"li",p:"p",strong:"strong",ul:"ul",...(0,n.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(o.p,{children:[(0,t.jsx)(o.a,{href:"https://docusaurus.io/docs/blog",children:"Docusaurus blogging features"})," are powered by the ",(0,t.jsx)(o.a,{href:"https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog",children:"blog plugin"}),"."]}),"\n",(0,t.jsxs)(o.p,{children:["Simply add Markdown files (or folders) to the ",(0,t.jsx)(o.code,{children:"blog"})," directory."]}),"\n",(0,t.jsxs)(o.p,{children:["Regular blog authors can be added to ",(0,t.jsx)(o.code,{children:"authors.yml"}),"."]}),"\n",(0,t.jsx)(o.p,{children:"The blog post date can be extracted from filenames, such as:"}),"\n",(0,t.jsxs)(o.ul,{children:["\n",(0,t.jsx)(o.li,{children:(0,t.jsx)(o.code,{children:"2019-05-30-welcome.md"})}),"\n",(0,t.jsx)(o.li,{children:(0,t.jsx)(o.code,{children:"2019-05-30-welcome/index.md"})}),"\n"]}),"\n",(0,t.jsx)(o.p,{children:"A blog post folder can be convenient to co-locate blog post images:"}),"\n",(0,t.jsx)(o.p,{children:(0,t.jsx)(o.img,{alt:"Docusaurus Plushie",src:s(6853).Z+"",width:"1500",height:"500"})}),"\n",(0,t.jsx)(o.p,{children:"The blog supports tags as well!"}),"\n",(0,t.jsxs)(o.p,{children:[(0,t.jsx)(o.strong,{children:"And if you don't want a blog"}),": just delete this directory, and use ",(0,t.jsx)(o.code,{children:"blog: false"})," in your Docusaurus config."]})]})}function d(e={}){const{wrapper:o}={...(0,n.a)(),...e.components};return o?(0,t.jsx)(o,{...e,children:(0,t.jsx)(i,{...e})}):i(e)}},6853:(e,o,s)=>{s.d(o,{Z:()=>t});const t=s.p+"assets/images/docusaurus-plushie-banner-a60f7593abca1e3eef26a9afa244e4fb.jpeg"},1151:(e,o,s)=>{s.d(o,{Z:()=>r,a:()=>l});var t=s(7294);const n={},a=t.createContext(n);function l(e){const o=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(o):{...o,...e}}),[o,e])}function r(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:l(e.components),t.createElement(a.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7c7b5a9b.993c3eca.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7c7b5a9b.993c3eca.js new file mode 100644 index 00000000..b69d6aa6 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7c7b5a9b.993c3eca.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9317],{8621:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>c,contentTitle:()=>l,default:()=>u,frontMatter:()=>a,metadata:()=>i,toc:()=>o});var t=n(5893),r=n(1151);const a={},l="Appendix D: PostgreSQL TLS Configuration",i={id:"appendixd",title:"Appendix D: PostgreSQL TLS Configuration",description:"Configure PostgreSQL server to use TLS",source:"@site/docs/appendixd.md",sourceDirName:".",slug:"/appendixd",permalink:"/docs/appendixd",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/appendixd.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Appendix C: Certificate Signing",permalink:"/docs/appendixc"},next:{title:"Appendix E: Proper Use of Trusted CAs",permalink:"/docs/appendixe"}},c={},o=[{value:"Configure PostgreSQL server to use TLS",id:"configure-postgresql-server-to-use-tls",level:2},{value:"Generate Client keys and certificates",id:"generate-client-keys-and-certificates",level:2},{value:"Configure TAK Server to use SSL",id:"configure-tak-server-to-use-ssl",level:2}];function d(e){const s={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.h1,{id:"appendix-d-postgresql-tls-configuration",children:"Appendix D: PostgreSQL TLS Configuration"}),"\n",(0,t.jsx)(s.h2,{id:"configure-postgresql-server-to-use-tls",children:"Configure PostgreSQL server to use TLS"}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["\n",(0,t.jsx)(s.p,{children:"Follow the steps in Appendix B (Certificate Generation) to generate CA keys and certificates if not already done so."}),"\n"]}),"\n",(0,t.jsxs)(s.li,{children:["\n",(0,t.jsx)(s.p,{children:"Generate PostgreSQL server keys and certificates:"}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"cd /opt/tak/certs\nsudo su tak\n./makeCert.sh server takdb\n"})}),"\n",(0,t.jsx)(s.p,{children:"Become a normal user"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"exit\nsudo chown postgres /opt/tak/certs/files/takdb.key\n"})}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["Update postgresql.conf. The file location can be different depending on your PostgreSQL installation:","\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["RHEL/Rocky/CentOS: ",(0,t.jsx)(s.code,{children:"/var/lib/pgsql/15/data/postgresql.conf"})]}),"\n",(0,t.jsxs)(s.li,{children:["Ubuntu/RaspPi: ",(0,t.jsx)(s.code,{children:"/etc/postgresql/15/main/postgresql.conf"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo vim /var/lib/pgsql/15/data/postgresql.conf\nssl = on\nssl_ca_file = '/opt/tak/certs/files/ca.pem'\nssl_cert_file = '/opt/tak/certs/files/takdb.pem'\nssl_key_file = '/opt/tak/certs/files/takdb.key'\n# Make sure to update the next line to use the correct passphrase as configured in cert-metadata.sh.\nssl_passphrase_command = 'echo \"atakatak\"'\nssl_passphrase_command_supports_reload = on\n"})}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["Update pg_hba.conf. The file location can be different depending on your PostgreSQL installation:","\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["RHEL/Rocky/CentOS: ",(0,t.jsx)(s.code,{children:"/var/lib/pgsql/15/data/pg_hba.conf"})]}),"\n",(0,t.jsxs)(s.li,{children:["Ubuntu/RaspPi: ",(0,t.jsx)(s.code,{children:"/etc/postgresql/15/main/pg_hba.conf"})]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo vim /var/lib/pgsql/15/data/pg_hba.conf\n"})}),"\n",(0,t.jsx)(s.p,{children:"Add this new line:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"hostssl\t all all all cert\nComment out the following lines if you also require SSL authentication for IPv4/IPv6 local connections\n# host all all 127.0.0.1/32 trust\n# host all all ::1/128 trust\n"})}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["Restart PostgreSQL server. Make sure it starts successfully.","\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["RHEL, Rocky Linux, and CentOS installations:","\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo systemctl restart postgresql-15\nsudo systemctl status postgresql-15\n"})}),"\n"]}),"\n",(0,t.jsxs)(s.li,{children:["Ubuntu and Raspberry Pi installations:","\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo systemctl restart postgresql\nsudo systemctl status postgresql\n"})}),"\n"]}),"\n"]}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(s.h2,{id:"generate-client-keys-and-certificates",children:"Generate Client keys and certificates"}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsx)(s.li,{children:"Generate client keys and certificates:"}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"cd /opt/tak/certs\nsudo su tak\n./makeCert.sh dbclient\n"})}),"\n",(0,t.jsx)(s.p,{children:'Client keys and certificates named "martiuser" (by default) will be created in the "files" directory.'}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsx)(s.li,{children:"Test SSL connection using the generated client certificate:"}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:'psql "host=127.0.0.1 port=5432 user=martiuser dbname=cot sslmode=verify-ca sslcert=files/martiuser.pem sslkey=files/martiuser.key sslrootcert=files/ca.pem"\n'})}),"\n",(0,t.jsx)(s.p,{children:"If you don\u2019t want to verify the server\u2019s credential:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:'psql "host=127.0.0.1 port=5432 user=martiuser dbname=cot sslmode=require sslcert=files/martiuser.pem sslkey=files/martiuser.key"\n'})}),"\n",(0,t.jsx)(s.p,{children:'The sslmode "verify-ca" means "I want to be sure that I connect to a server that I trust." The sslmode "require" means "I trust that the network will make sure I always connect to the server I want."'}),"\n",(0,t.jsxs)(s.p,{children:["More information on the sslmode can be found here: ",(0,t.jsx)(s.a,{href:"https://www.postgresql.org/docs/current/libpq-ssl.html",children:"https://www.postgresql.org/docs/current/libpq-ssl.html"})]}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsx)(s.li,{children:"Test database permission from the psql prompt:"}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"select count(*) from cot_router;\n"})}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsx)(s.em,{children:'NOTE: If you want to use a different name for certificates, you would also need to add a new user to the PostgreSQL database and grant permissions for the user. For example, following these steps to create a certificate named "takdbuser"'})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"./makeCert.sh dbclient takdbuser\nsudo su - postgres\n"})}),"\n",(0,t.jsx)(s.p,{children:"Connect to Postgres:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:'psql -d cot\n# List all users/roles:\n\\du\nSELECT * FROM pg_roles;\n# Create a new user ("takdbuser") and grant the user necessary roles ("martiuser"). The name of the user must match the CN in the client certificate.\nCREATE USER takdbuser;\ngrant martiuser to takdbuser;\n# Optional: Double check using \\du and "SELECT * FROM pg_roles;"\n'})}),"\n",(0,t.jsx)(s.h2,{id:"configure-tak-server-to-use-ssl",children:"Configure TAK Server to use SSL"}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsxs)(s.li,{children:["\n",(0,t.jsx)(s.p,{children:"Note that when you created a database client certificate (./makeCert.sh dbclient), an additional private key file in PKCS#8 format was created. Use this file for the param sslkey in CoreConfig.xml instead of using the files with .key extension."}),"\n"]}),"\n",(0,t.jsxs)(s.li,{children:["\n",(0,t.jsx)(s.p,{children:"Update CoreConfig.xml:\nUpdate the <connection> tag in <repository> (Remember to use a correct hostname/IP)"}),"\n"]}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:'<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" sslEnabled="true" sslMode="verify-ca" sslCert="certs/files/martiuser.pem" sslKey="certs/files/martiuser.key.pk8" sslRootCert="certs/files/ca.pem"/>\n'})}),"\n",(0,t.jsx)(s.p,{children:"If you don\u2019t want to verify the server\u2019s credential (not recommended in production):"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:'<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" sslEnabled="true" sslMode="require" sslCert="certs/files/martiuser.pem" sslKey="certs/files/martiuser.key.pk8" />\n'})}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsx)(s.li,{children:"Start/Restart TAK server."}),"\n"]}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:" sudo systemctl restart takserver\n"})})]})}function u(e={}){const{wrapper:s}={...(0,r.a)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},1151:(e,s,n)=>{n.d(s,{Z:()=>i,a:()=>l});var t=n(7294);const r={},a=t.createContext(r);function l(e){const s=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function i(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:l(e.components),t.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7feddd0b.d9cbe026.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7feddd0b.d9cbe026.js new file mode 100644 index 00000000..7be42856 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/7feddd0b.d9cbe026.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9882],{7246:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>o,toc:()=>l});var n=i(5893),r=i(1151);const s={},a="Appendix C: Certificate Signing",o={id:"appendixc",title:"Appendix C: Certificate Signing",description:"TAK Clients can enroll for new client certificates by submitting a Certificate Signing Request (CSR) to TAK Server. The Certificate Signing endpoint resides on port 8446 and requires HTTP Basic Authentication backed by either File or LDAP authentication. Ensure that the tomcat connector for port 8446 is active within tomcat-home/conf/server.xml.",source:"@site/docs/appendixc.md",sourceDirName:".",slug:"/appendixc",permalink:"/docs/appendixc",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/appendixc.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Appendix B: Certificate Generation",permalink:"/docs/appendixb"},next:{title:"Appendix D: PostgreSQL TLS Configuration",permalink:"/docs/appendixd"}},c={},l=[];function d(e){const t={code:"code",h1:"h1",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"appendix-c-certificate-signing",children:"Appendix C: Certificate Signing"}),"\n",(0,n.jsx)(t.p,{children:"TAK Clients can enroll for new client certificates by submitting a Certificate Signing Request (CSR) to TAK Server. The Certificate Signing endpoint resides on port 8446 and requires HTTP Basic Authentication backed by either File or LDAP authentication. Ensure that the tomcat connector for port 8446 is active within tomcat-home/conf/server.xml."}),"\n",(0,n.jsx)(t.p,{children:'The CertificateSigning section in CoreConfig.xml specifies how CSRs are processed. TAK Server can be configured to sign certificates directly, or proxy CSRs to a Microsoft CA instance running Certificate Enrollment Services. To configure TAK Server to sign certificates, set the CA attribute to "TAKServer". To configure TAK Server to proxy the CSR to MS CA, set the CA attribute to "MicrosoftCA".'}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:'<certificateSigning CA="{TAKServer | MicrosoftCA}">\n <certificateConfig>\n <nameEntries>\n <nameEntry name="O" value="Test Org Name"/>\n <nameEntry name="OU" value="Test Org Unit Name"/>\n </nameEntries>\n </certificateConfig>\n <TAKServerCAConfig\n keystore="JKS"\n keystoreFile="../certs/files_intCA/intermediate-ca-signing.jks"\n keystorePass="atakatak"\n validityDays="30"\n signatureAlg="SHA256WithRSA" />\n <MicrosoftCAConfig\n username="{MS CA Username}"\n password="{MS CA Password}"\n truststore="/opt/tak/certs/files_MSCA/keystore.jks"\n truststorePass="atakatak"\n svcUrl="https://win-kbtud3n1hjl.tak.net/tak-WIN-KBTUD3N1HJL-CA_CES_UsernamePassword/service.svc"\n templateName="Copy of User"/>\n</certificateSigning>\n'})}),"\n",(0,n.jsx)(t.p,{children:"Prior to submitting a CSR, Clients query TAK Server for Relative Distinguished Names (RDNs) that need to go into the CSR. The nameEntries element in CoreConfig.xml specifies the required RDNs, giving the administrator control over generated certificates. The CN value in the CSR will be equal to the HTTP username. TAK Server validates all required fields in the CSR prior to signing."}),"\n",(0,n.jsx)(t.p,{children:"The extra step of having client query TAK Server for RDNs wouldn't be required if TAK Server were signing certificates exclusively, since TAK Server could just add these names to the certificate. However, when proxying the CSR to an external CA, this allows additional flexibility in controlling the subject name within the certificate."}),"\n",(0,n.jsx)(t.p,{children:"The TAKServerCAConfig element specifies the keystore that TAK Server will use for signing certificates. The keystore must hold the CA's private key along with it's full trust chain. The makeCert.sh script will produce a signing keystore when generating an intermediate CA certificate. Certificates signed by TAK Server will be valid for the specified validityDays, and will be signed using the algorithm specified by signatureAlg."}),"\n",(0,n.jsx)(t.p,{children:"The MicrosoftCAConfig element defines how TAK Server will connect to the Certificate Enrollment Services (CES) endpoint. The CES endpoint is defined by the svcUrl attribute. The CES endpoint must be configured to use Username/Password authentication, and by default will include 'UsernamePassword' in the URL. The username and password attributes refer to an account configured on the MS CA Server to access the the CES endpoint. The truststore and truststorePass attrbitues point to a Java keystore (.jks) file that contains the trust chain for the svcUrl endpoint. Lastly, the templateName defines the certificate template that will be used to sign CSRs sent from TAK Server."})]})}function u(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,i)=>{i.d(t,{Z:()=>o,a:()=>a});var n=i(7294);const r={},s=n.createContext(r);function a(e){const t=n.useContext(s);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(s.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/814f3328.78f850a6.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/814f3328.78f850a6.js new file mode 100644 index 00000000..fca11c67 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/814f3328.78f850a6.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2535],{5641:t=>{t.exports=JSON.parse('{"title":"Recent posts","items":[{"title":"Welcome to Docusaurus-Static!","permalink":"/blog/2024/01/28/welcome-to-docusaurus-static","unlisted":false},{"title":"Welcome to Docusaurus!","permalink":"/blog/welcome","unlisted":false},{"title":"MDX Blog Post","permalink":"/blog/mdx-blog-post","unlisted":false},{"title":"Long Blog Post","permalink":"/blog/long-blog-post","unlisted":false},{"title":"First Blog Post","permalink":"/blog/first-blog-post","unlisted":false}]}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/867bb2d0.ebab9f45.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/867bb2d0.ebab9f45.js new file mode 100644 index 00000000..7b608dc8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/867bb2d0.ebab9f45.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[5243],{3998:(t,n,e)=>{e.r(n),e.d(n,{assets:()=>u,contentTitle:()=>s,default:()=>d,frontMatter:()=>r,metadata:()=>a,toc:()=>c});var i=e(5893),o=e(1151);const r={},s="Group Assignment by Input",a={id:"configuration/groupassignmentbyinput",title:"Group Assignment by Input",description:"\\ can drive group filtering, even without authentication messages. Version 1.3.0 added group filtering based on LDAP groups. This necessitated a new authentication message from ATAK. This worked for the streaming connections, but wouldn't work for the connection-less UDP traffic.",source:"@site/docs/configuration/groupassignmentbyinput.md",sourceDirName:"configuration",slug:"/configuration/groupassignmentbyinput",permalink:"/docs/configuration/groupassignmentbyinput",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/groupassignmentbyinput.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Group Filtering",permalink:"/docs/configuration/groupfiltering"},next:{title:"Group Assignment Using Authentication Messages",permalink:"/docs/configuration/groupassignmentusingauth"}},u={},c=[{value:"Input Configuration UI",id:"input-configuration-ui",level:2}];function p(t){const n={code:"code",em:"em",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,o.a)(),...t.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(n.h1,{id:"group-assignment-by-input",children:"Group Assignment by Input"}),"\n",(0,i.jsx)(n.p,{children:"<inputs> can drive group filtering, even without authentication messages. Version 1.3.0 added group filtering based on LDAP groups. This necessitated a new authentication message from ATAK. This worked for the streaming connections, but wouldn't work for the connection-less UDP traffic."}),"\n",(0,i.jsx)(n.p,{children:"We added an additional configuration option for inputs to allow the connection-less traffic to be routed according to the group filtering. An input definition like this:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:' <input _name="stdudp" protocol="udp" port="8087">\n <filtergroup>TEST1</filtergroup>\n </input>\n\n'})}),"\n",(0,i.jsx)(n.p,{children:"would have the effect of making every CoT event that came into the 'stdudp' input be associated with the \u201cTEST1\u201d group instead of the anonymous group. If there is no filtergroup specified, the default is the old behavior, which is a special anonymous group. The anonymous group has a name \u201c__ANON__\u201d that can be used to explicitly add it back in if needed. The filtergroup option can be used with the streaming input protocols as well (stcp, tls), the effect of which is that any subscriptions made by connecting to that port inherit the filter group from the input. <filtergroup> cannot be used in conjunction with the \u201cauth\u201d attribute on the same input. You can however use them on separate inputs, for example:"}),"\n",(0,i.jsx)(n.pre,{children:(0,i.jsx)(n.code,{className:"language-bash",children:' <input _name="stdudp" protocol="udp" port="8087">\n <filtergroup>CN=TAK1,DC=...</filtergroup>\n </input>\n <input _name="sec" protocol="tls" port="8089" auth="ldap" />\n'})}),"\n",(0,i.jsx)(n.p,{children:(0,i.jsx)(n.em,{children:"Note that when trying to interact with LDAP groups, you need to use the fully qualified group name that LDAP/ActiveDirectory reports."})}),"\n",(0,i.jsx)(n.h2,{id:"input-configuration-ui",children:"Input Configuration UI"}),"\n",(0,i.jsxs)(n.p,{children:["Inputs can be dynamically added, modified and deleted in the TAK Server user interface, under the menu heading ",(0,i.jsx)(n.strong,{children:"Configuration \u2192 Input Definitions"}),". The UI also shows activity for each input, in terms of number of reads and messages. For the streaming protocols (stcp, tls), the activity is the sum for all connections made using that particular input port."]})]})}function d(t={}){const{wrapper:n}={...(0,o.a)(),...t.components};return n?(0,i.jsx)(n,{...t,children:(0,i.jsx)(p,{...t})}):p(t)}},1151:(t,n,e)=>{e.d(n,{Z:()=>a,a:()=>s});var i=e(7294);const o={},r=i.createContext(o);function s(t){const n=i.useContext(r);return i.useMemo((function(){return"function"==typeof t?t(n):{...n,...t}}),[n,t])}function a(t){let n;return n=t.disableParentContext?"function"==typeof t.components?t.components(o):t.components||o:s(t.components),i.createElement(r.Provider,{value:n},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8717b14a.78f07aea.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8717b14a.78f07aea.js new file mode 100644 index 00000000..3d0fbb8d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8717b14a.78f07aea.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[948],{7106:(t,o,e)=>{e.r(o),e.d(o,{assets:()=>r,contentTitle:()=>i,default:()=>g,frontMatter:()=>l,metadata:()=>a,toc:()=>c});var s=e(5893),n=e(1151);const l={slug:"long-blog-post",title:"Long Blog Post",authors:"endi",tags:["hello","docusaurus"]},i=void 0,a={permalink:"/blog/long-blog-post",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2019-05-29-long-blog-post.md",source:"@site/blog/2019-05-29-long-blog-post.md",title:"Long Blog Post",description:"This is the summary of a very long blog post,",date:"2019-05-29T00:00:00.000Z",formattedDate:"May 29, 2019",tags:[{label:"hello",permalink:"/blog/tags/hello"},{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:2.05,hasTruncateMarker:!0,authors:[{name:"Endilie Yacop Sucipto",title:"Maintainer of Docusaurus",url:"https://github.com/endiliey",imageURL:"https://github.com/endiliey.png",key:"endi"}],frontMatter:{slug:"long-blog-post",title:"Long Blog Post",authors:"endi",tags:["hello","docusaurus"]},unlisted:!1,prevItem:{title:"MDX Blog Post",permalink:"/blog/mdx-blog-post"},nextItem:{title:"First Blog Post",permalink:"/blog/first-blog-post"}},r={authorsImageUrls:[void 0]},c=[];function u(t){const o={code:"code",p:"p",...(0,n.a)(),...t.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(o.p,{children:"This is the summary of a very long blog post,"}),"\n",(0,s.jsxs)(o.p,{children:["Use a ",(0,s.jsx)(o.code,{children:"\x3c!--"})," ",(0,s.jsx)(o.code,{children:"truncate"})," ",(0,s.jsx)(o.code,{children:"--\x3e"})," comment to limit blog post size in the list view."]})]})}function g(t={}){const{wrapper:o}={...(0,n.a)(),...t.components};return o?(0,s.jsx)(o,{...t,children:(0,s.jsx)(u,{...t})}):u(t)}},1151:(t,o,e)=>{e.d(o,{Z:()=>a,a:()=>i});var s=e(7294);const n={},l=s.createContext(n);function i(t){const o=s.useContext(l);return s.useMemo((function(){return"function"==typeof t?t(o):{...o,...t}}),[o,t])}function a(t){let o;return o=t.disableParentContext?"function"==typeof t.components?t.components(n):t.components||n:i(t.components),s.createElement(l.Provider,{value:o},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8838a7ac.11272d03.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8838a7ac.11272d03.js new file mode 100644 index 00000000..5b01780a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8838a7ac.11272d03.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3268],{7217:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>r,default:()=>u,frontMatter:()=>i,metadata:()=>s,toc:()=>l});var o=t(5893),a=t(1151);const i={},r="Optionally Disabling UI and WebTAK on HTTPS Ports",s={id:"configuration/optionallydisableui",title:"Optionally Disabling UI and WebTAK on HTTPS Ports",description:"TAK Server can be configured to optionally disable the Admin UI, non-admin UI or WebTAK on any HTTPS connector (port). These options can be used to fine-tune the security profile for each HTTPS connector. For example, the admin web interface can be moved to an alternate port that is protected by a firewall from access on the public Internet.",source:"@site/docs/configuration/optionallydisableui.md",sourceDirName:"configuration",slug:"/configuration/optionallydisableui",permalink:"/docs/configuration/optionallydisableui",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/optionallydisableui.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Configuring Messaging and Repository Settings through Web UI",permalink:"/docs/configuration/configuremessagingrepositorywebui"},next:{title:"VBM Admin Configuration",permalink:"/docs/configuration/vbmadminconfig"}},c={},l=[];function d(e){const n={code:"code",h1:"h1",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(n.h1,{id:"optionally-disabling-ui-and-webtak-on-https-ports",children:"Optionally Disabling UI and WebTAK on HTTPS Ports"}),"\n",(0,o.jsx)(n.p,{children:"TAK Server can be configured to optionally disable the Admin UI, non-admin UI or WebTAK on any HTTPS connector (port). These options can be used to fine-tune the security profile for each HTTPS connector. For example, the admin web interface can be moved to an alternate port that is protected by a firewall from access on the public Internet."}),"\n",(0,o.jsx)(n.p,{children:"In the CoreConfig.xml, the enableAdminUI, enableWebtak, and enableNonAdminUI attributes on each <connector> can be used to optionally disable access to any of these three functions for a given HTTPS connector port. The default value for each of these attributes is true, so by default these functions are available."}),"\n",(0,o.jsx)(n.p,{children:"Usage Examples:\nDisable webtak on port 8443:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'<connector port="8443" _name="https" enableWebtak="false" />\n'})}),"\n",(0,o.jsx)(n.p,{children:"On port 8452, disable admin UI, but enable WebTAK and non-admin UI:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'<connector port="8452" _name="https" enableAdminUI="false" />\n'})}),"\n",(0,o.jsx)(n.p,{children:"Disable WebTAK on OAuth port 8446:"}),"\n",(0,o.jsx)(n.pre,{children:(0,o.jsx)(n.code,{className:"language-bash",children:'<connector port="8446" clientAuth="false" _name="cert_https" enableWebtak="false"/>\n'})})]})}function u(e={}){const{wrapper:n}={...(0,a.a)(),...e.components};return n?(0,o.jsx)(n,{...e,children:(0,o.jsx)(d,{...e})}):d(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>s,a:()=>r});var o=t(7294);const a={},i=o.createContext(a);function r(e){const n=o.useContext(i);return o.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function s(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:r(e.components),o.createElement(i.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/89d39a78.3f495133.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/89d39a78.3f495133.js new file mode 100644 index 00000000..171d74ea --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/89d39a78.3f495133.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8954],{9215:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>l});var n=s(5893),r=s(1151);const i={},a="Configure TAK Server Installation",o={id:"installation/oneserver/takserverconfiguration",title:"Configure TAK Server Installation",description:"On resource limited hosts, such as a Raspberry Pi, you may start/stop only essential api and messaging TAK Server services with:",source:"@site/docs/installation/oneserver/takserverconfiguration.md",sourceDirName:"installation/oneserver",slug:"/installation/oneserver/takserverconfiguration",permalink:"/docs/installation/oneserver/takserverconfiguration",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/oneserver/takserverconfiguration.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"TAK Server Installation",permalink:"/docs/installation/oneserver/takserverinstallation"},next:{title:"Overview",permalink:"/docs/installation/twoserver/overview"}},c={},l=[];function d(e){const t={code:"code",em:"em",h1:"h1",img:"img",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"configure-tak-server-installation",children:"Configure TAK Server Installation"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo systemctl daemon-reload\n"})}),"\n",(0,n.jsx)(t.p,{children:"On resource limited hosts, such as a Raspberry Pi, you may start/stop only essential api and messaging TAK Server services with:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo systemctl [start|stop] takserver-noplugins\n"})}),"\n",(0,n.jsx)(t.p,{children:"Otherwise, to start/stop all TAK Server service:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo systemctl [start|stop] takserver\n"})}),"\n",(0,n.jsx)(t.p,{children:"You can set TAK Server to start at boot by running"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo systemctl enable takserver\n"})}),"\n",(0,n.jsx)(t.p,{children:"or with resource limited hosts:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo systemctl enable takserver-noplugins\n"})}),"\n",(0,n.jsx)(t.p,{children:"For secure operation, TAK Server requires a keystore and truststore (X.509 certificates)."}),"\n",(0,n.jsx)(t.p,{children:"Next, follow the instructions in Appendix B to create these certificates. TAK Server by default is TLS only, so certificate generation, including an administrative certificate is required for configuration. In addition, if you would like to configure TLS for Postgres database connection, follow additional steps in Appendix D."}),"\n",(0,n.jsx)(t.p,{children:"Verify that the steps in Appendix B have been followed by checking the following items:"}),"\n",(0,n.jsx)(t.p,{children:"Certificates are present at:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"/opt/tak/certs/files\n"})}),"\n",(0,n.jsx)(t.p,{children:"The TAK Server was restarted, the admin cert has been generated, and an admin account in TAK Server was created with the command:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem\n"})}),"\n",(0,n.jsx)(t.p,{children:"While following the instructions in Appendix B, you will have created an admin certificate. Import this certificate into your browser, so that you can access the Admin. It will be located here on your TAK Server machine:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"/opt/tak/certs/files/admin.pem\n"})}),"\n",(0,n.jsx)(t.p,{children:"Import this client certificate into your browser."}),"\n",(0,n.jsxs)(t.p,{children:["If you are using Firefox, go to ",(0,n.jsx)(t.em,{children:"Settings -> Preferences -> Privacy & Security -> Certificates -> View Certificates"})]}),"\n",(0,n.jsx)(t.p,{children:"Go to Import. Upload this file:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"/opt/tak/certs/files/admin.p12\n"})}),"\n",(0,n.jsxs)(t.p,{children:["Enter the certificate password. The default password is ",(0,n.jsx)(t.code,{children:"atakatak"})]}),"\n",(0,n.jsx)(t.p,{children:"Browse to:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"https://localhost:8443\n"})}),"\n",(0,n.jsx)(t.p,{children:"Select the admin certificate to log in."}),"\n",(0,n.jsx)(t.p,{children:"An error message similar to this indicates that the correct client certificate has not been imported into the browser:"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Example Incorrect Client Certificate Error Message",src:s(1164).Z+"",width:"960",height:"460"})})]})}function h(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1164:(e,t,s)=>{s.d(t,{Z:()=>n});const n=s.p+"assets/images/secure_connection_failed-56cfeb544f1c53afe1d119d408897cd4.png"},1151:(e,t,s)=>{s.d(t,{Z:()=>o,a:()=>a});var n=s(7294);const r={},i=n.createContext(r);function a(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:a(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8c65a213.65611896.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8c65a213.65611896.js new file mode 100644 index 00000000..0abc1b39 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/8c65a213.65611896.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[5097],{8752:(e,t,i)=>{i.r(t),i.d(t,{assets:()=>d,contentTitle:()=>a,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>c});var n=i(5893),s=i(1151);const o={},a="Mission Federation Disruption Tolerance",r={id:"federation/federationdisruptiontolerance",title:"Mission Federation Disruption Tolerance",description:"Traffic between federated servers may be disrupted, and updates to missions could happen during that disruption. Mission federation disruption tolerance will update each server with changes to federated missions that occured during the disruption. To enable this feature, check the box in the Federation Configuration page:",source:"@site/docs/federation/federationdisruptiontolerance.md",sourceDirName:"federation",slug:"/federation/federationdisruptiontolerance",permalink:"/docs/federation/federationdisruptiontolerance",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/federationdisruptiontolerance.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Federated Group Mapping",permalink:"/docs/federation/federatedgroupmapping"},next:{title:"Data Package and Mission File Blocker",permalink:"/docs/federation/datapackagemissionfileblocker"}},d={},c=[];function l(e){const t={h1:"h1",img:"img",p:"p",...(0,s.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"mission-federation-disruption-tolerance",children:"Mission Federation Disruption Tolerance"}),"\n",(0,n.jsx)(t.p,{children:"Traffic between federated servers may be disrupted, and updates to missions could happen during that disruption. Mission federation disruption tolerance will update each server with changes to federated missions that occured during the disruption. To enable this feature, check the box in the Federation Configuration page:"}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Enable Disruption Tolerance",src:i(1009).Z+"",width:"896",height:"468"})}),"\n",(0,n.jsx)(t.p,{children:"Sending all the changes that occured between disruptions could potentially take a lot of bandwidth, so by default, we limit the changes to those that occured within the last 2 days. For example, if a disruption lasted 3 weeks, we would only send the changes from the previous 2 days. However, if the disruption only lasted a few hours, only the changes since the last disruption would be sent. If the Unlimited checkbox is checked, then all changes since the last disruption would be sent. The 2 day limit can be changed to any length with the Send Changes Newer Than setting, and the period can be selected as days, hours, or seconds."}),"\n",(0,n.jsx)(t.p,{children:"It is also possible to override the global setting for a particular mission, if so desired."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Specific Mission Settings",src:i(6024).Z+"",width:"896",height:"252"})}),"\n",(0,n.jsx)(t.p,{children:"For example, in the above image, we see that mission_2 will send updates up to over the last 10 days, mission_2 only over the last 12 hours, and mission_4 will send all updates since the last disruption. Any mission that is not listed, and any subsequently added mission, will follow the global setting of 2 days, as set above."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Clear Federation Events",src:i(1417).Z+"",width:"581",height:"536"})}),"\n",(0,n.jsx)(t.p,{children:"The Clear Federation Events button will reset the disruption history for federation. This means that on the next reconnection, the server will send the max allowed mission changes according to the Mission Federation Disruption Tolerance settings. In the above case, that would be 10 days for mission_2, 12 hours for mission_3, and the entire change history for mission_4. For all other missions this would be 2 days worth of mission changes."})]})}function h(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(l,{...e})}):l(e)}},1009:(e,t,i)=>{i.d(t,{Z:()=>n});const n=i.p+"assets/images/fed_6-75c9a98399cadc644a00467b1b7ee85d.png"},6024:(e,t,i)=>{i.d(t,{Z:()=>n});const n=i.p+"assets/images/fed_7-5ffb2096420eeae91eed62c1079a5e85.png"},1417:(e,t,i)=>{i.d(t,{Z:()=>n});const n=i.p+"assets/images/fed_8-cdef0c0440722b6bfb3b193633393887.png"},1151:(e,t,i)=>{i.d(t,{Z:()=>r,a:()=>a});var n=i(7294);const s={},o=n.createContext(s);function a(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/925b3f96.17aa319a.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/925b3f96.17aa319a.js new file mode 100644 index 00000000..3b2dacc8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/925b3f96.17aa319a.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9003],{3902:(t,o,e)=>{e.r(o),e.d(o,{assets:()=>n,contentTitle:()=>u,default:()=>m,frontMatter:()=>i,metadata:()=>r,toc:()=>l});var s=e(5893),a=e(1151);const i={slug:"first-blog-post",title:"First Blog Post",authors:{name:"Gao Wei",title:"Docusaurus Core Team",url:"https://github.com/wgao19",image_url:"https://github.com/wgao19.png"},tags:["hola","docusaurus"]},u=void 0,r={permalink:"/blog/first-blog-post",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2019-05-28-first-blog-post.md",source:"@site/blog/2019-05-28-first-blog-post.md",title:"First Blog Post",description:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet",date:"2019-05-28T00:00:00.000Z",formattedDate:"May 28, 2019",tags:[{label:"hola",permalink:"/blog/tags/hola"},{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:.12,hasTruncateMarker:!1,authors:[{name:"Gao Wei",title:"Docusaurus Core Team",url:"https://github.com/wgao19",image_url:"https://github.com/wgao19.png",imageURL:"https://github.com/wgao19.png"}],frontMatter:{slug:"first-blog-post",title:"First Blog Post",authors:{name:"Gao Wei",title:"Docusaurus Core Team",url:"https://github.com/wgao19",image_url:"https://github.com/wgao19.png",imageURL:"https://github.com/wgao19.png"},tags:["hola","docusaurus"]},unlisted:!1,prevItem:{title:"Long Blog Post",permalink:"/blog/long-blog-post"}},n={authorsImageUrls:[void 0]},l=[];function c(t){const o={p:"p",...(0,a.a)(),...t.components};return(0,s.jsx)(o.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"})}function m(t={}){const{wrapper:o}={...(0,a.a)(),...t.components};return o?(0,s.jsx)(o,{...t,children:(0,s.jsx)(c,{...t})}):c(t)}},1151:(t,o,e)=>{e.d(o,{Z:()=>r,a:()=>u});var s=e(7294);const a={},i=s.createContext(a);function u(t){const o=s.useContext(i);return s.useMemo((function(){return"function"==typeof t?t(o):{...o,...t}}),[o,t])}function r(t){let o;return o=t.disableParentContext?"function"==typeof t.components?t.components(a):t.components||a:u(t.components),s.createElement(i.Provider,{value:o},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/935f2afb.6924f0c5.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/935f2afb.6924f0c5.js new file mode 100644 index 00000000..7f0b9846 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/935f2afb.6924f0c5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[53],{1109:e=>{e.exports=JSON.parse('{"pluginId":"default","version":"current","label":"Next","banner":null,"badge":false,"noIndex":false,"className":"docs-version-current","isLast":true,"docsSidebars":{"tutorialSidebar":[{"type":"link","label":"About TAK Server","href":"/docs/about","docId":"about","unlisted":false},{"type":"link","label":"Change Log","href":"/docs/changelog","docId":"changelog","unlisted":false},{"type":"category","label":"System Requirements","items":[{"type":"link","label":"Supported Operating Systems","href":"/docs/system-requirements/systemrequirements","docId":"system-requirements/systemrequirements","unlisted":false},{"type":"link","label":"Server Requirements","href":"/docs/system-requirements/serverrequirements","docId":"system-requirements/serverrequirements","unlisted":false},{"type":"link","label":"AWS / GovCloud Recommended Instance Type","href":"/docs/system-requirements/awsrequirements","docId":"system-requirements/awsrequirements","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Installation","items":[{"type":"link","label":"Overview and Installer Files","href":"/docs/installation/overview","docId":"installation/overview","unlisted":false},{"type":"category","label":"New Installation: One Server","items":[{"type":"link","label":"Prerequisites","href":"/docs/installation/oneserver/prerequisite","docId":"installation/oneserver/prerequisite","unlisted":false},{"type":"link","label":"TAK Server Installation","href":"/docs/installation/oneserver/takserverinstallation","docId":"installation/oneserver/takserverinstallation","unlisted":false},{"type":"link","label":"Configure TAK Server Installation","href":"/docs/installation/oneserver/takserverconfiguration","docId":"installation/oneserver/takserverconfiguration","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"New Installation: Two Servers","items":[{"type":"link","label":"Overview","href":"/docs/installation/twoserver/overview","docId":"installation/twoserver/overview","unlisted":false},{"type":"category","label":"Server One: Database Server","items":[{"type":"link","label":"Dependency Setup","href":"/docs/installation/twoserver/serverone/dependencysetup","docId":"installation/twoserver/serverone/dependencysetup","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Server Two: Core Server","items":[{"type":"link","label":"Prerequisites","href":"/docs/installation/twoserver/servertwo/prerequisites","docId":"installation/twoserver/servertwo/prerequisites","unlisted":false},{"type":"link","label":"Install TAK Server","href":"/docs/installation/twoserver/servertwo/installtakserver","docId":"installation/twoserver/servertwo/installtakserver","unlisted":false},{"type":"link","label":"Configuration","href":"/docs/installation/twoserver/servertwo/configuretakserver","docId":"installation/twoserver/servertwo/configuretakserver","unlisted":false}],"collapsed":true,"collapsible":true}],"collapsed":true,"collapsible":true},{"type":"link","label":"Use Setup Wizard to Configure TAK Server","href":"/docs/installation/setup_wizard","docId":"installation/setup_wizard","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Upgrade Existing TAK Server Installation","items":[{"type":"link","label":"Overview","href":"/docs/upgrade/overview","docId":"upgrade/overview","unlisted":false},{"type":"link","label":"Single-Server Upgrade","href":"/docs/upgrade/singleserver","docId":"upgrade/singleserver","unlisted":false},{"type":"link","label":"Two-Server Upgrade","href":"/docs/upgrade/twoserver","docId":"upgrade/twoserver","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Containerized Installation (Docker)","items":[{"type":"link","label":"IronBank","href":"/docs/dockerinstall/ironbank","docId":"dockerinstall/ironbank","unlisted":false},{"type":"link","label":"Building and Installing Container Images Using Docker","href":"/docs/dockerinstall/buildinstall","docId":"dockerinstall/buildinstall","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"category","label":"Configure System Firewall","items":[{"type":"link","label":"Overview","href":"/docs/firewall/overview","docId":"firewall/overview","unlisted":false},{"type":"link","label":"RHEL, Rocky Linux, and CentOS","href":"/docs/firewall/rhelrockycentos","docId":"firewall/rhelrockycentos","unlisted":false},{"type":"link","label":"Ubuntu and Raspberry Pi","href":"/docs/firewall/ubunturaspberrypi","docId":"firewall/ubunturaspberrypi","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","label":"Software Installation Location","href":"/docs/softwareinstallationlocation","docId":"softwareinstallationlocation","unlisted":false},{"type":"category","label":"Configuration","items":[{"type":"link","label":"Overview","href":"/docs/configuration/overview","docId":"configuration/overview","unlisted":false},{"type":"link","label":"Configuring Security Through Web UI (Certificates/TLS)","href":"/docs/configuration/configurewebui","docId":"configuration/configurewebui","unlisted":false},{"type":"link","label":"Group Filtering","href":"/docs/configuration/groupfiltering","docId":"configuration/groupfiltering","unlisted":false},{"type":"link","label":"Group Assignment by Input","href":"/docs/configuration/groupassignmentbyinput","docId":"configuration/groupassignmentbyinput","unlisted":false},{"type":"link","label":"Group Assignment Using Authentication Messages","href":"/docs/configuration/groupassignmentusingauth","docId":"configuration/groupassignmentusingauth","unlisted":false},{"type":"link","label":"Group Assignment using Client Certificates","href":"/docs/configuration/groupassignmentusingclientcerts","docId":"configuration/groupassignmentusingclientcerts","unlisted":false},{"type":"link","label":"Authentication Backends","href":"/docs/configuration/authenticationbackends","docId":"configuration/authenticationbackends","unlisted":false},{"type":"link","label":"Configuring Messaging and Repository Settings through Web UI","href":"/docs/configuration/configuremessagingrepositorywebui","docId":"configuration/configuremessagingrepositorywebui","unlisted":false},{"type":"link","label":"Optionally Disabling UI and WebTAK on HTTPS Ports","href":"/docs/configuration/optionallydisableui","docId":"configuration/optionallydisableui","unlisted":false},{"type":"link","label":"VBM Admin Configuration","href":"/docs/configuration/vbmadminconfig","docId":"configuration/vbmadminconfig","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","label":"WebTAK","href":"/docs/webtak","docId":"webtak","unlisted":false},{"type":"link","label":"Device Profiles","href":"/docs/deviceprofiles","docId":"deviceprofiles","unlisted":false},{"type":"category","label":"Federation","items":[{"type":"link","label":"Overview","href":"/docs/federation/overview","docId":"federation/overview","unlisted":false},{"type":"link","label":"Enable Federation","href":"/docs/federation/enablefederation","docId":"federation/enablefederation","unlisted":false},{"type":"link","label":"Upload Federate Certificate","href":"/docs/federation/uploadfederatecert","docId":"federation/uploadfederatecert","unlisted":false},{"type":"link","label":"Make the Connection","href":"/docs/federation/maketheconnection","docId":"federation/maketheconnection","unlisted":false},{"type":"link","label":"Federated Group Mapping","href":"/docs/federation/federatedgroupmapping","docId":"federation/federatedgroupmapping","unlisted":false},{"type":"link","label":"Mission Federation Disruption Tolerance","href":"/docs/federation/federationdisruptiontolerance","docId":"federation/federationdisruptiontolerance","unlisted":false},{"type":"link","label":"Data Package and Mission File Blocker","href":"/docs/federation/datapackagemissionfileblocker","docId":"federation/datapackagemissionfileblocker","unlisted":false},{"type":"link","label":"Federation Example","href":"/docs/federation/federationexample","docId":"federation/federationexample","unlisted":false}],"collapsed":true,"collapsible":true},{"type":"link","label":"Metrics","href":"/docs/metrics","docId":"metrics","unlisted":false},{"type":"link","label":"Logging","href":"/docs/logging","docId":"logging","unlisted":false},{"type":"link","label":"Group Filtering for Multicast Networks","href":"/docs/groupfilteringformulticast","docId":"groupfilteringformulticast","unlisted":false},{"type":"link","label":"OAuth2 Authentication","href":"/docs/oath2authentication","docId":"oath2authentication","unlisted":false},{"type":"link","label":"User Management UI","href":"/docs/usermanagementui","docId":"usermanagementui","unlisted":false},{"type":"link","label":"Data Retention Tool","href":"/docs/dataretentiontool","docId":"dataretentiontool","unlisted":false},{"type":"link","label":"Appendix A: Acronyms and Abbreviations","href":"/docs/appendixa","docId":"appendixa","unlisted":false},{"type":"link","label":"Appendix B: Certificate Generation","href":"/docs/appendixb","docId":"appendixb","unlisted":false},{"type":"link","label":"Appendix C: Certificate Signing","href":"/docs/appendixc","docId":"appendixc","unlisted":false},{"type":"link","label":"Appendix D: PostgreSQL TLS Configuration","href":"/docs/appendixd","docId":"appendixd","unlisted":false},{"type":"link","label":"Appendix E: Proper Use of Trusted CAs","href":"/docs/appendixe","docId":"appendixe","unlisted":false}]},"docs":{"about":{"id":"about","title":"About TAK Server","description":"TAK Server is a situational awareness server, that provides a dynamic Common Operating Picture to users of the Team Awareness Kit, including ATAK (Android), WinTAK (Windows) and WebTAK. TAK enables sharing of geolocated information in real time for military forces, law enforcement, and emergency responders. It supports both wireless and wired networks, as well as cloud and data center deployment.","sidebar":"tutorialSidebar"},"appendixa":{"id":"appendixa","title":"Appendix A: Acronyms and Abbreviations","description":"- ATAK Android Team Awareness Kit","sidebar":"tutorialSidebar"},"appendixb":{"id":"appendixb","title":"Appendix B: Certificate Generation","description":"TAK Server includes scripts for generating a private security enclave, which will create a Certificate Authority (CA) as well as server and client certificates.","sidebar":"tutorialSidebar"},"appendixc":{"id":"appendixc","title":"Appendix C: Certificate Signing","description":"TAK Clients can enroll for new client certificates by submitting a Certificate Signing Request (CSR) to TAK Server. The Certificate Signing endpoint resides on port 8446 and requires HTTP Basic Authentication backed by either File or LDAP authentication. Ensure that the tomcat connector for port 8446 is active within tomcat-home/conf/server.xml.","sidebar":"tutorialSidebar"},"appendixd":{"id":"appendixd","title":"Appendix D: PostgreSQL TLS Configuration","description":"Configure PostgreSQL server to use TLS","sidebar":"tutorialSidebar"},"appendixe":{"id":"appendixe","title":"Appendix E: Proper Use of Trusted CAs","description":"TAK uses Mutual TLS (MTLS) authentication to establish secure communications channels between TAK clients and TAK Server. It\'s critical that deployments use a CA created by the TAK server scripts, or another private CA, to establish the root of trust. Failure to follow this guidance could result in exposing your deployment to a Man-In-The-Middle (MITM) attack.","sidebar":"tutorialSidebar"},"changelog":{"id":"changelog","title":"Change Log","description":"See https://wiki.tak.gov/display/DEV/TAK+Server+Change+Log","sidebar":"tutorialSidebar"},"configuration/authenticationbackends":{"id":"configuration/authenticationbackends","title":"Authentication Backends","description":"File-Based","sidebar":"tutorialSidebar"},"configuration/configuremessagingrepositorywebui":{"id":"configuration/configuremessagingrepositorywebui","title":"Configuring Messaging and Repository Settings through Web UI","description":"Messaging Configuration Web Interface","sidebar":"tutorialSidebar"},"configuration/configurewebui":{"id":"configuration/configurewebui","title":"Configuring Security Through Web UI (Certificates/TLS)","description":"Security Configuration","sidebar":"tutorialSidebar"},"configuration/groupassignmentbyinput":{"id":"configuration/groupassignmentbyinput","title":"Group Assignment by Input","description":"\\\\ can drive group filtering, even without authentication messages. Version 1.3.0 added group filtering based on LDAP groups. This necessitated a new authentication message from ATAK. This worked for the streaming connections, but wouldn\'t work for the connection-less UDP traffic.","sidebar":"tutorialSidebar"},"configuration/groupassignmentusingauth":{"id":"configuration/groupassignmentusingauth","title":"Group Assignment Using Authentication Messages","description":"If ATAK or another TAK client is configured to send an authentication message after establishing a connection to TAK Server, the username and password credentials contained in that message will be used, in conjunction with an authentication backend, to determine the group membership of a user. TAK Server will then filter messages according to common group membership, in a similar fashion to filtering configured by \\\\ for a given \\\\.","sidebar":"tutorialSidebar"},"configuration/groupassignmentusingclientcerts":{"id":"configuration/groupassignmentusingclientcerts","title":"Group Assignment using Client Certificates","description":"TAK Server can be configured to use only the information contained in a client certificate, when looking up group membership for a user. In this case, the TAK client is configured to use TLS and a client certificate when connecting to TAK server, but does not send an authentication message. This eliminates the requirement to cache credentials on the device, or enter credentials prior to establishment of each new connection. TAK Server will then filter messages according to common group membership, in a similar fashion to input-level filtering with filter groups. When analyzing the X.509 client certificate presented by the TAK client, TAK Server will look at the DN attribute in the certificate, extract the CN value from the DN (if present). The CN is regarded as the username, and is used to look up group membership in authentication backends. For example, consider this DN in a client certificate:","sidebar":"tutorialSidebar"},"configuration/groupfiltering":{"id":"configuration/groupfiltering","title":"Group Filtering","description":"TAK Server has the ability to segment users so they only see a subset of the other users on the system. This is achieved by assigning groups to individual connections. If ATAK-A shares common group membership in at least one group with ATAK-B, they share data with each other. If not otherwise specified, all connections default to being in the special \u201cANON\u201d group (note 2 underscores as prefix and postfix). There are three ways to assign groups to a connection:","sidebar":"tutorialSidebar"},"configuration/optionallydisableui":{"id":"configuration/optionallydisableui","title":"Optionally Disabling UI and WebTAK on HTTPS Ports","description":"TAK Server can be configured to optionally disable the Admin UI, non-admin UI or WebTAK on any HTTPS connector (port). These options can be used to fine-tune the security profile for each HTTPS connector. For example, the admin web interface can be moved to an alternate port that is protected by a firewall from access on the public Internet.","sidebar":"tutorialSidebar"},"configuration/overview":{"id":"configuration/overview","title":"Overview","description":"Configuration is primarily done through the web interface. Changes made in the interface will be reflected in the /opt/tak/CoreConfig.xml file. If that file does not exist (e.g. on a fresh install), then when TAK Server starts up it will copy /opt/tak/CoreConfig.example.xml. The example has many commented out options. Notable configuration options:","sidebar":"tutorialSidebar"},"configuration/vbmadminconfig":{"id":"configuration/vbmadminconfig","title":"VBM Admin Configuration","description":"TAK Server can allow for additional filtering of cot messages recieved from inputs (server ports) and data feeds by using the VBM Configuration page in the Admin UI. To navigate there, go to Administative \\\\> VBM Configuration as shown below.","sidebar":"tutorialSidebar"},"dataretentiontool":{"id":"dataretentiontool","title":"Data Retention Tool","description":"Information regarding the use of the Data Retention Tool is available on the tak.gov wiki:","sidebar":"tutorialSidebar"},"deviceprofiles":{"id":"deviceprofiles","title":"Device Profiles","description":"TAK Server can now assist in provisioning ATAK devices through Device Profiles. The Device Profile Manager (under Administrative Menu, Device Profiles) allows administrators to build profiles that can be applied to clients when enrolling for certificates, and when connecting to TAK server. The Profile consists of a sets of files, which can include settings and data in any file format that is supported by TAK Mission Packages. Profiles can be made public or restricted to individual Groups.","sidebar":"tutorialSidebar"},"dockerinstall/buildinstall":{"id":"dockerinstall/buildinstall","title":"Building and Installing Container Images Using Docker","description":"TAK Server can be installed using docker. Start by downloading container images from tak.gov. You will need the docker release which comes as a zip file called \'takserver-docker-\\\\.zip\'.","sidebar":"tutorialSidebar"},"dockerinstall/ironbank":{"id":"dockerinstall/ironbank","title":"IronBank","description":"See https://ironbank.dso.mil/repomap?searchText=tak%20server (Platform One account required)","sidebar":"tutorialSidebar"},"federation/datapackagemissionfileblocker":{"id":"federation/datapackagemissionfileblocker","title":"Data Package and Mission File Blocker","description":"Data packages, mission packages, and missions can be federated between servers and their respective ATAK clients. These packages may contain configuration files such as ATAK .pref files that can result in the distribution of unwanted configuration changes to ATAK devices. A filter can be enabled to block files by file-type. To enable this feature, check the Data Package and Mission File Blocker box in the Federation Configuration page. The default file extension value is \'pref\'. This can be changed by entering a new file type, clicking on the \'Add\' button to add the entry, and clicking on the \'Save\' buton at the bottom of the Federation Configuration page.","sidebar":"tutorialSidebar"},"federation/enablefederation":{"id":"federation/enablefederation","title":"Enable Federation","description":"The first step is to enable federation on your TAK server. To do this, first go the Configuration > Manage Federates page. If federation is not yet enabled, click on the Edit Configuration button. This is also where you can pick the ports for each federation protocol.","sidebar":"tutorialSidebar"},"federation/federatedgroupmapping":{"id":"federation/federatedgroupmapping","title":"Federated Group Mapping","description":"The flow of traffic between Federates may be directed using end-to-end group mapping. The Federated Group Mapping section is on the Federate Groups page.","sidebar":"tutorialSidebar"},"federation/federationdisruptiontolerance":{"id":"federation/federationdisruptiontolerance","title":"Mission Federation Disruption Tolerance","description":"Traffic between federated servers may be disrupted, and updates to missions could happen during that disruption. Mission federation disruption tolerance will update each server with changes to federated missions that occured during the disruption. To enable this feature, check the box in the Federation Configuration page:","sidebar":"tutorialSidebar"},"federation/federationexample":{"id":"federation/federationexample","title":"Federation Example","description":"The figure below shows a connectivity graph of two distinct administrative domains. Each administrative domain has multiple sub-groups (e.g. \\"CN=Alpha\\") utilizing the group-filtering. The color coding indicates the CA that is used to sign the certificate used for connections. Enclave 1\'s CA signs ATAK client certs and a server certificate. Enclave 2\'s CA also signs ATAK client certs and a server cert. The trust-store listing the allowed CAs for the \\"User Port\\" only contains a single CA (i.e. Enclave 1 CA for Enclave 1). To federate the servers, Enclave 1 and Enclave 2 send each other the \\"public\\" CA cert. Those certificates are put in a separate trust store that is used only for federation connections. The \u201cFed. Port\u201d is configured with this separate trust-store.","sidebar":"tutorialSidebar"},"federation/maketheconnection":{"id":"federation/maketheconnection","title":"Make the Connection","description":"Now that we have enabled federation and shared our CA with the other TAK server authority, we are ready to make the connection and start sharing. For this step, only ONE of the servers creates an outgoing connection to the other. If you are starting the connection, go back to the Manage Federates page where you enabled federation from step 1. You will now see three sections, Active Connections, Outgoing Connection Configuration, and Federate Configuration. To create an outgoing connection, click on the corresponding link, and enter in the address and port of the destination server. You can also pick the protocol version (make sure it is the right protocol for the port you are connecting to!), reconnection interval (how long between retries if the connection is lost), and whether or not the connection will be enabled on creation.","sidebar":"tutorialSidebar"},"federation/overview":{"id":"federation/overview","title":"Overview","description":"Federation will allow subsets of ATAK users who are connected to different servers to work together, even though each TAK server instance (hereafter refered to as \'federates\') may be run by independent organizations / administrative domains. It brings some of the following benefits/restrictions:","sidebar":"tutorialSidebar"},"federation/uploadfederatecert":{"id":"federation/uploadfederatecert","title":"Upload Federate Certificate","description":"In order for the federate servers to trust each other and their ATAK clients, they must share each others certificate authorities (CAs) in order to create a separate federate trust-store. One of the key components to how TAK Server satisfies all the restrictions is that we use one trust-store for local users, and one for Federates. The trust-store contains all the valid CAs that you will allow client certificates from. By having separate trust-stores, we can have the Federation channels allow connections with certificates from \u201coutside\u201d CAs, while not allowing ATAKs with certs from those \u201coutside\u201d CAs to make direct connections to our server.","sidebar":"tutorialSidebar"},"firewall/overview":{"id":"firewall/overview","title":"Overview","description":"One of the most common problems people have is the system default firewall blocking their traffic.","sidebar":"tutorialSidebar"},"firewall/rhelrockycentos":{"id":"firewall/rhelrockycentos","title":"RHEL, Rocky Linux, and CentOS","description":"To verify whether a firewall is running, use the command:","sidebar":"tutorialSidebar"},"firewall/ubunturaspberrypi":{"id":"firewall/ubunturaspberrypi","title":"Ubuntu and Raspberry Pi","description":"UFW (Uncomplicated Firewall) is a utility for managing firewalls. If is not installed on your server. Install with the following:","sidebar":"tutorialSidebar"},"groupfilteringformulticast":{"id":"groupfilteringformulticast","title":"Group Filtering for Multicast Networks","description":"The proxy attribute on the CoreConfig input element ( \\\\ ) was removed in TAK Server 4.1. The intent of the proxy attribute was to control bridging of multicast networks and federating multicast data. As TAK Server\u2019s group filtering capabilities have evolved, having a dedicated proxy attribute is no longer needed. Using filtergroup on the mcast input you can achieve greater control over multicast traffic.","sidebar":"tutorialSidebar"},"imagetest":{"id":"imagetest","title":"Image Test","description":"This is just a section demonstrating images."},"installation/oneserver/prerequisite":{"id":"installation/oneserver/prerequisite","title":"Prerequisites","description":"Start with a fresh install of a supported OS. For AWS / cloud installation, see recommended instance type on page 5. An OS install with a GUI is recommended, so that a web browser can be run locally to configure TAK Server.","sidebar":"tutorialSidebar"},"installation/oneserver/takserverconfiguration":{"id":"installation/oneserver/takserverconfiguration","title":"Configure TAK Server Installation","description":"On resource limited hosts, such as a Raspberry Pi, you may start/stop only essential api and messaging TAK Server services with:","sidebar":"tutorialSidebar"},"installation/oneserver/takserverinstallation":{"id":"installation/oneserver/takserverinstallation","title":"TAK Server Installation","description":"Rocky Linux 8","sidebar":"tutorialSidebar"},"installation/overview":{"id":"installation/overview","title":"Overview and Installer Files","description":"TAK Server supports multiple deployment configurations:","sidebar":"tutorialSidebar"},"installation/setup_wizard":{"id":"installation/setup_wizard","title":"Use Setup Wizard to Configure TAK Server","description":"The TAK Server configuration wizard will help you set up common configuration options once you have installed and started TAK Server. The wizard will guide you through the setup process for a secure configuration, using the default ports that ATAK and WinTAK will connect to.","sidebar":"tutorialSidebar"},"installation/twoserver/overview":{"id":"installation/twoserver/overview","title":"Overview","description":"Follow the procedures in the following two sections to install the database server, and the messaging server. For AWS / cloud installation, see recommended instance type on page 4. Use this instance type for both servers.","sidebar":"tutorialSidebar"},"installation/twoserver/serverone/dependencysetup":{"id":"installation/twoserver/serverone/dependencysetup","title":"Dependency Setup","description":"First, update firewall rules to allow communication with server two, for TCP port 5432.","sidebar":"tutorialSidebar"},"installation/twoserver/servertwo/configuretakserver":{"id":"installation/twoserver/servertwo/configuretakserver","title":"Configuration","description":"Configure database connection by updating /opt/tak/CoreConfig.xml:","sidebar":"tutorialSidebar"},"installation/twoserver/servertwo/installtakserver":{"id":"installation/twoserver/servertwo/installtakserver","title":"Install TAK Server","description":"Rocky Linux 8","sidebar":"tutorialSidebar"},"installation/twoserver/servertwo/prerequisites":{"id":"installation/twoserver/servertwo/prerequisites","title":"Prerequisites","description":"Start with a fresh install of a supported OS. An install with a GUI is recommended, so that a web browser can be run locally to configure TAK Server.","sidebar":"tutorialSidebar"},"logging":{"id":"logging","title":"Logging","description":"TAK Server provides several log files to provide information about relevant events that happen during execution. The log files are located in the /opt/tak/logs directory. This table shows the name of the log files, and their function.","sidebar":"tutorialSidebar"},"metrics":{"id":"metrics","title":"Metrics","description":"The TAK Server Metrics Dashboard is available in the Monitoring menu. The dashboard continuously renders the following information:","sidebar":"tutorialSidebar"},"oath2authentication":{"id":"oath2authentication","title":"OAuth2 Authentication","description":"TAK Server provides OAuth2 Authorization and Resource server capabilities using the OAuth2 Password authentication flow. OAuth2 integration works with existing authentication back ends, allowing TAK Server to issue tokens backed by the File or LDAP authenticators. TAK Server issues JSON Web Tokens (JWT) signed by the server certificate, allowing external systems to validate tokens against the server\u2019s trust chain. The OAuth2 token endpoint is available at https8446/oauth/token.","sidebar":"tutorialSidebar"},"softwareinstallationlocation":{"id":"softwareinstallationlocation","title":"Software Installation Location","description":"The RPM installer places the TAK server software and configuration in the directory /opt/tak. It creates a user named tak who is the owner of the files in that directory tree. Always use this tak user when editing CoreConfig.xml or generating certificates. You can become the tak user by entering:","sidebar":"tutorialSidebar"},"system-requirements/awsrequirements":{"id":"system-requirements/awsrequirements","title":"AWS / GovCloud Recommended Instance Type","description":"- c5.xlarge","sidebar":"tutorialSidebar"},"system-requirements/serverrequirements":{"id":"system-requirements/serverrequirements","title":"Server Requirements","description":"- 4 processor cores","sidebar":"tutorialSidebar"},"system-requirements/systemrequirements":{"id":"system-requirements/systemrequirements","title":"Supported Operating Systems","description":"- Rocky Linux 8 (Replacement for CentOS 8, which is EOL)","sidebar":"tutorialSidebar"},"upgrade/overview":{"id":"upgrade/overview","title":"Overview","description":"Follow this procedure to upgrade a system running TAK Server.","sidebar":"tutorialSidebar"},"upgrade/singleserver":{"id":"upgrade/singleserver","title":"Single-Server Upgrade","description":"Rocky Linux 8","sidebar":"tutorialSidebar"},"upgrade/twoserver":{"id":"upgrade/twoserver","title":"Two-Server Upgrade","description":"Rocky Linux 8","sidebar":"tutorialSidebar"},"usermanagementui":{"id":"usermanagementui","title":"User Management UI","description":"Overview","sidebar":"tutorialSidebar"},"webtak":{"id":"webtak","title":"WebTAK","description":"The WebTAK front-end application is bundled with TAK Server. The WebTAK back-end WebSockets networking channel and APIs are provided by TAK Server. WebTAK must be accessed using https.","sidebar":"tutorialSidebar"}}}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/93acc585.395b236f.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/93acc585.395b236f.js new file mode 100644 index 00000000..0dbb97d1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/93acc585.395b236f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[4546],{8393:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>o,contentTitle:()=>l,default:()=>d,frontMatter:()=>a,metadata:()=>i,toc:()=>u});var t=r(5893),s=r(1151);const a={},l="Ubuntu and Raspberry Pi",i={id:"firewall/ubunturaspberrypi",title:"Ubuntu and Raspberry Pi",description:"UFW (Uncomplicated Firewall) is a utility for managing firewalls. If is not installed on your server. Install with the following:",source:"@site/docs/firewall/ubunturaspberrypi.md",sourceDirName:"firewall",slug:"/firewall/ubunturaspberrypi",permalink:"/docs/firewall/ubunturaspberrypi",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/firewall/ubunturaspberrypi.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"RHEL, Rocky Linux, and CentOS",permalink:"/docs/firewall/rhelrockycentos"},next:{title:"Software Installation Location",permalink:"/docs/softwareinstallationlocation"}},o={},u=[];function c(e){const n={code:"code",h1:"h1",p:"p",pre:"pre",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"ubuntu-and-raspberry-pi",children:"Ubuntu and Raspberry Pi"}),"\n",(0,t.jsx)(n.p,{children:"UFW (Uncomplicated Firewall) is a utility for managing firewalls. If is not installed on your server. Install with the following:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo apt install ufw\n"})}),"\n",(0,t.jsx)(n.p,{children:"IMPORTANT: Raspberry Pi installs, please reboot your device after installing ufw."}),"\n",(0,t.jsx)(n.p,{children:"To check the status of the firewall service with current port rules:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo ufw status\n"})}),"\n",(0,t.jsx)(n.p,{children:"Perform the following commands to set initial rules for your firewall:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo ufw default deny incoming\nsudo ufw default allow outgoing\nsudo ufw allow ssh\n"})}),"\n",(0,t.jsx)(n.p,{children:"Turn on the firewall:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo ufw enable\n"})}),"\n",(0,t.jsx)(n.p,{children:"Add default configuration TAK Server ports:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo ufw allow 8089\nsudo ufw allow 8443\n"})})]})}function d(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(c,{...e})}):c(e)}},1151:(e,n,r)=>{r.d(n,{Z:()=>i,a:()=>l});var t=r(7294);const s={},a=t.createContext(s);function l(e){const n=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:l(e.components),t.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9677.38f534c1.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9677.38f534c1.js new file mode 100644 index 00000000..1b26849a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9677.38f534c1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9677],{1460:(e,t,s)=>{s.d(t,{Z:()=>f});var a=s(7294),n=s(512),r=s(6040),i=s(7524),l=s(3692),o=s(5999),c=s(6550),m=s(8596);function d(e){const{pathname:t}=(0,c.TH)();return(0,a.useMemo)((()=>e.filter((e=>function(e,t){return!(e.unlisted&&!(0,m.Mg)(e.permalink,t))}(e,t)))),[e,t])}const u={sidebar:"sidebar_re4s",sidebarItemTitle:"sidebarItemTitle_pO2u",sidebarItemList:"sidebarItemList_Yudw",sidebarItem:"sidebarItem__DBe",sidebarItemLink:"sidebarItemLink_mo7H",sidebarItemLinkActive:"sidebarItemLinkActive_I1ZP"};var h=s(5893);function g(e){let{sidebar:t}=e;const s=d(t.items);return(0,h.jsx)("aside",{className:"col col--3",children:(0,h.jsxs)("nav",{className:(0,n.Z)(u.sidebar,"thin-scrollbar"),"aria-label":(0,o.I)({id:"theme.blog.sidebar.navAriaLabel",message:"Blog recent posts navigation",description:"The ARIA label for recent posts in the blog sidebar"}),children:[(0,h.jsx)("div",{className:(0,n.Z)(u.sidebarItemTitle,"margin-bottom--md"),children:t.title}),(0,h.jsx)("ul",{className:(0,n.Z)(u.sidebarItemList,"clean-list"),children:s.map((e=>(0,h.jsx)("li",{className:u.sidebarItem,children:(0,h.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:u.sidebarItemLink,activeClassName:u.sidebarItemLinkActive,children:e.title})},e.permalink)))})]})})}var p=s(3102);function x(e){let{sidebar:t}=e;const s=d(t.items);return(0,h.jsx)("ul",{className:"menu__list",children:s.map((e=>(0,h.jsx)("li",{className:"menu__list-item",children:(0,h.jsx)(l.Z,{isNavLink:!0,to:e.permalink,className:"menu__link",activeClassName:"menu__link--active",children:e.title})},e.permalink)))})}function j(e){return(0,h.jsx)(p.Zo,{component:x,props:e})}function b(e){let{sidebar:t}=e;const s=(0,i.i)();return t?.items.length?"mobile"===s?(0,h.jsx)(j,{sidebar:t}):(0,h.jsx)(g,{sidebar:t}):null}function f(e){const{sidebar:t,toc:s,children:a,...i}=e,l=t&&t.items.length>0;return(0,h.jsx)(r.Z,{...i,children:(0,h.jsx)("div",{className:"container margin-vert--lg",children:(0,h.jsxs)("div",{className:"row",children:[(0,h.jsx)(b,{sidebar:t}),(0,h.jsx)("main",{className:(0,n.Z)("col",{"col--7":l,"col--9 col--offset-1":!l}),itemScope:!0,itemType:"https://schema.org/Blog",children:a}),s&&(0,h.jsx)("div",{className:"col col--2",children:s})]})})})}},390:(e,t,s)=>{s.d(t,{Z:()=>y});s(7294);var a=s(512),n=s(9460),r=s(4996),i=s(5893);function l(e){let{children:t,className:s}=e;const{frontMatter:a,assets:l,metadata:{description:o}}=(0,n.C)(),{withBaseUrl:c}=(0,r.C)(),m=l.image??a.image,d=a.keywords??[];return(0,i.jsxs)("article",{className:s,itemProp:"blogPost",itemScope:!0,itemType:"https://schema.org/BlogPosting",children:[o&&(0,i.jsx)("meta",{itemProp:"description",content:o}),m&&(0,i.jsx)("link",{itemProp:"image",href:c(m,{absolute:!0})}),d.length>0&&(0,i.jsx)("meta",{itemProp:"keywords",content:d.join(",")}),t]})}var o=s(3692);const c={title:"title_f1Hy"};function m(e){let{className:t}=e;const{metadata:s,isBlogPostPage:r}=(0,n.C)(),{permalink:l,title:m}=s,d=r?"h1":"h2";return(0,i.jsx)(d,{className:(0,a.Z)(c.title,t),itemProp:"headline",children:r?m:(0,i.jsx)(o.Z,{itemProp:"url",to:l,children:m})})}var d=s(5999),u=s(8824);const h={container:"container_mt6G"};function g(e){let{readingTime:t}=e;const s=function(){const{selectMessage:e}=(0,u.c)();return t=>{const s=Math.ceil(t);return e(s,(0,d.I)({id:"theme.blog.post.readingTime.plurals",description:'Pluralized label for "{readingTime} min read". Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)',message:"One min read|{readingTime} min read"},{readingTime:s}))}}();return(0,i.jsx)(i.Fragment,{children:s(t)})}function p(e){let{date:t,formattedDate:s}=e;return(0,i.jsx)("time",{dateTime:t,itemProp:"datePublished",children:s})}function x(){return(0,i.jsx)(i.Fragment,{children:" \xb7 "})}function j(e){let{className:t}=e;const{metadata:s}=(0,n.C)(),{date:r,formattedDate:l,readingTime:o}=s;return(0,i.jsxs)("div",{className:(0,a.Z)(h.container,"margin-vert--md",t),children:[(0,i.jsx)(p,{date:r,formattedDate:l}),void 0!==o&&(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(x,{}),(0,i.jsx)(g,{readingTime:o})]})]})}function b(e){return e.href?(0,i.jsx)(o.Z,{...e}):(0,i.jsx)(i.Fragment,{children:e.children})}function f(e){let{author:t,className:s}=e;const{name:n,title:r,url:l,imageURL:o,email:c}=t,m=l||c&&`mailto:${c}`||void 0;return(0,i.jsxs)("div",{className:(0,a.Z)("avatar margin-bottom--sm",s),children:[o&&(0,i.jsx)(b,{href:m,className:"avatar__photo-link",children:(0,i.jsx)("img",{className:"avatar__photo",src:o,alt:n,itemProp:"image"})}),n&&(0,i.jsxs)("div",{className:"avatar__intro",itemProp:"author",itemScope:!0,itemType:"https://schema.org/Person",children:[(0,i.jsx)("div",{className:"avatar__name",children:(0,i.jsx)(b,{href:m,itemProp:"url",children:(0,i.jsx)("span",{itemProp:"name",children:n})})}),r&&(0,i.jsx)("small",{className:"avatar__subtitle",itemProp:"description",children:r})]})]})}const v={authorCol:"authorCol_Hf19",imageOnlyAuthorRow:"imageOnlyAuthorRow_pa_O",imageOnlyAuthorCol:"imageOnlyAuthorCol_G86a"};function _(e){let{className:t}=e;const{metadata:{authors:s},assets:r}=(0,n.C)();if(0===s.length)return null;const l=s.every((e=>{let{name:t}=e;return!t}));return(0,i.jsx)("div",{className:(0,a.Z)("margin-top--md margin-bottom--sm",l?v.imageOnlyAuthorRow:"row",t),children:s.map(((e,t)=>(0,i.jsx)("div",{className:(0,a.Z)(!l&&"col col--6",l?v.imageOnlyAuthorCol:v.authorCol),children:(0,i.jsx)(f,{author:{...e,imageURL:r.authorsImageUrls[t]??e.imageURL}})},t)))})}function N(){return(0,i.jsxs)("header",{children:[(0,i.jsx)(m,{}),(0,i.jsx)(j,{}),(0,i.jsx)(_,{})]})}var P=s(8780),Z=s(7395);function k(e){let{children:t,className:s}=e;const{isBlogPostPage:r}=(0,n.C)();return(0,i.jsx)("div",{id:r?P.blogPostContainerID:void 0,className:(0,a.Z)("markdown",s),itemProp:"articleBody",children:(0,i.jsx)(Z.Z,{children:t})})}var C=s(4881),T=s(1526);function w(){return(0,i.jsx)("b",{children:(0,i.jsx)(d.Z,{id:"theme.blog.post.readMore",description:"The label used in blog post item excerpts to link to full blog posts",children:"Read More"})})}function I(e){const{blogPostTitle:t,...s}=e;return(0,i.jsx)(o.Z,{"aria-label":(0,d.I)({message:"Read more about {title}",id:"theme.blog.post.readMoreLabel",description:"The ARIA label for the link to full blog posts from excerpts"},{title:t}),...s,children:(0,i.jsx)(w,{})})}const F={blogPostFooterDetailsFull:"blogPostFooterDetailsFull_mRVl"};function L(){const{metadata:e,isBlogPostPage:t}=(0,n.C)(),{tags:s,title:r,editUrl:l,hasTruncateMarker:o}=e,c=!t&&o,m=s.length>0;return m||c||l?(0,i.jsxs)("footer",{className:(0,a.Z)("row docusaurus-mt-lg",t&&F.blogPostFooterDetailsFull),children:[m&&(0,i.jsx)("div",{className:(0,a.Z)("col",{"col--9":c}),children:(0,i.jsx)(T.Z,{tags:s})}),t&&l&&(0,i.jsx)("div",{className:"col margin-top--sm",children:(0,i.jsx)(C.Z,{editUrl:l})}),c&&(0,i.jsx)("div",{className:(0,a.Z)("col text--right",{"col--3":m}),children:(0,i.jsx)(I,{blogPostTitle:r,to:e.permalink})})]}):null}function y(e){let{children:t,className:s}=e;const r=function(){const{isBlogPostPage:e}=(0,n.C)();return e?void 0:"margin-bottom--xl"}();return(0,i.jsxs)(l,{className:(0,a.Z)(r,s),children:[(0,i.jsx)(N,{}),(0,i.jsx)(k,{children:t}),(0,i.jsx)(L,{})]})}},4881:(e,t,s)=>{s.d(t,{Z:()=>m});s(7294);var a=s(5999),n=s(5281),r=s(3692),i=s(512);const l={iconEdit:"iconEdit_Z9Sw"};var o=s(5893);function c(e){let{className:t,...s}=e;return(0,o.jsx)("svg",{fill:"currentColor",height:"20",width:"20",viewBox:"0 0 40 40",className:(0,i.Z)(l.iconEdit,t),"aria-hidden":"true",...s,children:(0,o.jsx)("g",{children:(0,o.jsx)("path",{d:"m34.5 11.7l-3 3.1-6.3-6.3 3.1-3q0.5-0.5 1.2-0.5t1.1 0.5l3.9 3.9q0.5 0.4 0.5 1.1t-0.5 1.2z m-29.5 17.1l18.4-18.5 6.3 6.3-18.4 18.4h-6.3v-6.2z"})})})}function m(e){let{editUrl:t}=e;return(0,o.jsxs)(r.Z,{to:t,className:n.k.common.editThisPage,children:[(0,o.jsx)(c,{}),(0,o.jsx)(a.Z,{id:"theme.common.editThisPage",description:"The link label to edit the current page",children:"Edit this page"})]})}},2244:(e,t,s)=>{s.d(t,{Z:()=>i});s(7294);var a=s(512),n=s(3692),r=s(5893);function i(e){const{permalink:t,title:s,subLabel:i,isNext:l}=e;return(0,r.jsxs)(n.Z,{className:(0,a.Z)("pagination-nav__link",l?"pagination-nav__link--next":"pagination-nav__link--prev"),to:t,children:[i&&(0,r.jsx)("div",{className:"pagination-nav__sublabel",children:i}),(0,r.jsx)("div",{className:"pagination-nav__label",children:s})]})}},3008:(e,t,s)=>{s.d(t,{Z:()=>l});s(7294);var a=s(512),n=s(3692);const r={tag:"tag_zVej",tagRegular:"tagRegular_sFm0",tagWithCount:"tagWithCount_h2kH"};var i=s(5893);function l(e){let{permalink:t,label:s,count:l}=e;return(0,i.jsxs)(n.Z,{href:t,className:(0,a.Z)(r.tag,l?r.tagWithCount:r.tagRegular),children:[s,l&&(0,i.jsx)("span",{children:l})]})}},1526:(e,t,s)=>{s.d(t,{Z:()=>o});s(7294);var a=s(512),n=s(5999),r=s(3008);const i={tags:"tags_jXut",tag:"tag_QGVx"};var l=s(5893);function o(e){let{tags:t}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)("b",{children:(0,l.jsx)(n.Z,{id:"theme.tags.tagsListLabel",description:"The label alongside a tag list",children:"Tags:"})}),(0,l.jsx)("ul",{className:(0,a.Z)(i.tags,"padding--none","margin-left--sm"),children:t.map((e=>{let{label:t,permalink:s}=e;return(0,l.jsx)("li",{className:i.tag,children:(0,l.jsx)(r.Z,{label:t,permalink:s})},s)}))})]})}},9460:(e,t,s)=>{s.d(t,{C:()=>o,n:()=>l});var a=s(7294),n=s(902),r=s(5893);const i=a.createContext(null);function l(e){let{children:t,content:s,isBlogPostPage:n=!1}=e;const l=function(e){let{content:t,isBlogPostPage:s}=e;return(0,a.useMemo)((()=>({metadata:t.metadata,frontMatter:t.frontMatter,assets:t.assets,toc:t.toc,isBlogPostPage:s})),[t,s])}({content:s,isBlogPostPage:n});return(0,r.jsx)(i.Provider,{value:l,children:t})}function o(){const e=(0,a.useContext)(i);if(null===e)throw new n.i6("BlogPostProvider");return e}},8824:(e,t,s)=>{s.d(t,{c:()=>c});var a=s(7294),n=s(2263);const r=["zero","one","two","few","many","other"];function i(e){return r.filter((t=>e.includes(t)))}const l={locale:"en",pluralForms:i(["one","other"]),select:e=>1===e?"one":"other"};function o(){const{i18n:{currentLocale:e}}=(0,n.Z)();return(0,a.useMemo)((()=>{try{return function(e){const t=new Intl.PluralRules(e);return{locale:e,pluralForms:i(t.resolvedOptions().pluralCategories),select:e=>t.select(e)}}(e)}catch(t){return console.error(`Failed to use Intl.PluralRules for locale "${e}".\nDocusaurus will fallback to the default (English) implementation.\nError: ${t.message}\n`),l}}),[e])}function c(){const e=o();return{selectMessage:(t,s)=>function(e,t,s){const a=e.split("|");if(1===a.length)return a[0];a.length>s.pluralForms.length&&console.error(`For locale=${s.locale}, a maximum of ${s.pluralForms.length} plural forms are expected (${s.pluralForms.join(",")}), but the message contains ${a.length}: ${e}`);const n=s.select(t),r=s.pluralForms.indexOf(n);return a[Math.min(r,a.length-1)]}(s,t,e)}}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/995e17fc.38645dd8.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/995e17fc.38645dd8.js new file mode 100644 index 00000000..5c9987b3 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/995e17fc.38645dd8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[5550],{5400:(e,t,s)=>{s.r(t),s.d(t,{assets:()=>u,contentTitle:()=>o,default:()=>l,frontMatter:()=>i,metadata:()=>a,toc:()=>c});var n=s(5893),r=s(1151);const i={},o="Supported Operating Systems",a={id:"system-requirements/systemrequirements",title:"Supported Operating Systems",description:"- Rocky Linux 8 (Replacement for CentOS 8, which is EOL)",source:"@site/docs/system-requirements/systemrequirements.md",sourceDirName:"system-requirements",slug:"/system-requirements/systemrequirements",permalink:"/docs/system-requirements/systemrequirements",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/system-requirements/systemrequirements.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Change Log",permalink:"/docs/changelog"},next:{title:"Server Requirements",permalink:"/docs/system-requirements/serverrequirements"}},u={},c=[];function m(e){const t={em:"em",h1:"h1",li:"li",p:"p",strong:"strong",ul:"ul",...(0,r.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"supported-operating-systems",children:"Supported Operating Systems"}),"\n",(0,n.jsxs)(t.ul,{children:["\n",(0,n.jsx)(t.li,{children:"Rocky Linux 8 (Replacement for CentOS 8, which is EOL)"}),"\n",(0,n.jsx)(t.li,{children:"Red Hat Enterprise Linux (RHEL) 8 or 7"}),"\n",(0,n.jsx)(t.li,{children:"Ubuntu 22"}),"\n",(0,n.jsx)(t.li,{children:"Raspberry Pi OS (64-bit)"}),"\n",(0,n.jsxs)(t.li,{children:["CentOS 7 (",(0,n.jsx)(t.em,{children:(0,n.jsx)(t.strong,{children:"not"})})," CentOS 8 Stream)"]}),"\n"]}),"\n",(0,n.jsx)(t.p,{children:"Java 17 is required. Java 17 is installed by default via package dependencies, but if your system has a different Java version installed in addition to Java 17, ensure that TAK Server is using Java 17."})]})}function l(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(m,{...e})}):m(e)}},1151:(e,t,s)=>{s.d(t,{Z:()=>a,a:()=>o});var n=s(7294);const r={},i=n.createContext(r);function o(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9beb87c2.f1fe94f7.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9beb87c2.f1fe94f7.js new file mode 100644 index 00000000..3de8f9c8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9beb87c2.f1fe94f7.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[80],{4123:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>i,contentTitle:()=>a,default:()=>d,frontMatter:()=>r,metadata:()=>c,toc:()=>u});var o=n(5893),s=n(1151);const r={},a="Change Log",c={id:"changelog",title:"Change Log",description:"See https://wiki.tak.gov/display/DEV/TAK+Server+Change+Log",source:"@site/docs/changelog.md",sourceDirName:".",slug:"/changelog",permalink:"/docs/changelog",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/changelog.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"About TAK Server",permalink:"/docs/about"},next:{title:"Supported Operating Systems",permalink:"/docs/system-requirements/systemrequirements"}},i={},u=[];function l(e){const t={a:"a",h1:"h1",p:"p",...(0,s.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"change-log",children:"Change Log"}),"\n",(0,o.jsxs)(t.p,{children:["See ",(0,o.jsx)(t.a,{href:"https://wiki.tak.gov/display/DEV/TAK+Server+Change+Log",children:"https://wiki.tak.gov/display/DEV/TAK+Server+Change+Log"})]})]})}function d(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>c,a:()=>a});var o=n(7294);const s={},r=o.createContext(s);function a(e){const t=o.useContext(r);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),o.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9e4087bc.79fe55e5.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9e4087bc.79fe55e5.js new file mode 100644 index 00000000..31f69f03 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/9e4087bc.79fe55e5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3608],{3169:(e,s,t)=>{t.r(s),t.d(s,{default:()=>o});t(7294);var a=t(3692),r=t(5999),i=t(1944),c=t(6040),n=t(2503),l=t(5893);function d(e){let{year:s,posts:t}=e;return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(n.Z,{as:"h3",id:s,children:s}),(0,l.jsx)("ul",{children:t.map((e=>(0,l.jsx)("li",{children:(0,l.jsxs)(a.Z,{to:e.metadata.permalink,children:[e.metadata.formattedDate," - ",e.metadata.title]})},e.metadata.date)))})]})}function h(e){let{years:s}=e;return(0,l.jsx)("section",{className:"margin-vert--lg",children:(0,l.jsx)("div",{className:"container",children:(0,l.jsx)("div",{className:"row",children:s.map(((e,s)=>(0,l.jsx)("div",{className:"col col--4 margin-vert--lg",children:(0,l.jsx)(d,{...e})},s)))})})})}function o(e){let{archive:s}=e;const t=(0,r.I)({id:"theme.blog.archive.title",message:"Archive",description:"The page & hero title of the blog archive page"}),a=(0,r.I)({id:"theme.blog.archive.description",message:"Archive",description:"The page & hero description of the blog archive page"}),d=function(e){const s=e.reduce(((e,s)=>{const t=s.metadata.date.split("-")[0],a=e.get(t)??[];return e.set(t,[s,...a])}),new Map);return Array.from(s,(e=>{let[s,t]=e;return{year:s,posts:t}}))}(s.blogPosts);return(0,l.jsxs)(l.Fragment,{children:[(0,l.jsx)(i.d,{title:t,description:a}),(0,l.jsxs)(c.Z,{children:[(0,l.jsx)("header",{className:"hero hero--primary",children:(0,l.jsxs)("div",{className:"container",children:[(0,l.jsx)(n.Z,{as:"h1",className:"hero__title",children:t}),(0,l.jsx)("p",{className:"hero__subtitle",children:a})]})}),(0,l.jsx)("main",{children:d.length>0&&(0,l.jsx)(h,{years:d})})]})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a5758fe9.cddfce92.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a5758fe9.cddfce92.js new file mode 100644 index 00000000..fec19296 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a5758fe9.cddfce92.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[894],{7011:s=>{s.exports=JSON.parse('{"label":"docusaurus-static","permalink":"/blog/tags/docusaurus-static","allTagsPath":"/blog/tags","count":1,"unlisted":false}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a6aa9e1f.1fe44632.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a6aa9e1f.1fe44632.js new file mode 100644 index 00000000..d5599cec --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a6aa9e1f.1fe44632.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3089],{46:(e,t,a)=>{a.r(t),a.d(t,{default:()=>m});a(7294);var s=a(512),n=a(2263),i=a(1944),r=a(5281),l=a(1460),o=a(9703),d=a(197),g=a(9985),c=a(5893);function p(e){const{metadata:t}=e,{siteConfig:{title:a}}=(0,n.Z)(),{blogDescription:s,blogTitle:r,permalink:l}=t,o="/"===l?a:r;return(0,c.jsxs)(c.Fragment,{children:[(0,c.jsx)(i.d,{title:o,description:s}),(0,c.jsx)(d.Z,{tag:"blog_posts_list"})]})}function u(e){const{metadata:t,items:a,sidebar:s}=e;return(0,c.jsxs)(l.Z,{sidebar:s,children:[(0,c.jsx)(g.Z,{items:a}),(0,c.jsx)(o.Z,{metadata:t})]})}function m(e){return(0,c.jsxs)(i.FG,{className:(0,s.Z)(r.k.wrapper.blogPages,r.k.page.blogListPage),children:[(0,c.jsx)(p,{...e}),(0,c.jsx)(u,{...e})]})}},9703:(e,t,a)=>{a.d(t,{Z:()=>r});a(7294);var s=a(5999),n=a(2244),i=a(5893);function r(e){const{metadata:t}=e,{previousPage:a,nextPage:r}=t;return(0,i.jsxs)("nav",{className:"pagination-nav","aria-label":(0,s.I)({id:"theme.blog.paginator.navAriaLabel",message:"Blog list page navigation",description:"The ARIA label for the blog pagination"}),children:[a&&(0,i.jsx)(n.Z,{permalink:a,title:(0,i.jsx)(s.Z,{id:"theme.blog.paginator.newerEntries",description:"The label used to navigate to the newer blog posts page (previous page)",children:"Newer Entries"})}),r&&(0,i.jsx)(n.Z,{permalink:r,title:(0,i.jsx)(s.Z,{id:"theme.blog.paginator.olderEntries",description:"The label used to navigate to the older blog posts page (next page)",children:"Older Entries"}),isNext:!0})]})}},9985:(e,t,a)=>{a.d(t,{Z:()=>r});a(7294);var s=a(9460),n=a(390),i=a(5893);function r(e){let{items:t,component:a=n.Z}=e;return(0,i.jsx)(i.Fragment,{children:t.map((e=>{let{content:t}=e;return(0,i.jsx)(s.n,{content:t,children:(0,i.jsx)(a,{children:(0,i.jsx)(t,{})})},t.metadata.permalink)}))})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a7023ddc.00580228.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a7023ddc.00580228.js new file mode 100644 index 00000000..6e119f0f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a7023ddc.00580228.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1713],{3457:a=>{a.exports=JSON.parse('[{"label":"hello","permalink":"/blog/tags/hello","count":3},{"label":"docusaurus-static","permalink":"/blog/tags/docusaurus-static","count":1},{"label":"facebook","permalink":"/blog/tags/facebook","count":1},{"label":"docusaurus","permalink":"/blog/tags/docusaurus","count":4},{"label":"hola","permalink":"/blog/tags/hola","count":1}]')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a7bd4aaa.07e993a8.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a7bd4aaa.07e993a8.js new file mode 100644 index 00000000..842aac8e --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a7bd4aaa.07e993a8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8518],{8564:(s,n,e)=>{e.r(n),e.d(n,{default:()=>l});e(7294);var r=e(1944),o=e(3320),t=e(4477),c=e(8790),u=e(197),i=e(5893);function a(s){const{version:n}=s;return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(u.Z,{version:n.version,tag:(0,o.os)(n.pluginId,n.version)}),(0,i.jsx)(r.d,{children:n.noIndex&&(0,i.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})]})}function d(s){const{version:n,route:e}=s;return(0,i.jsx)(r.FG,{className:n.className,children:(0,i.jsx)(t.q,{version:n,children:(0,c.H)(e.routes)})})}function l(s){return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(a,{...s}),(0,i.jsx)(d,{...s})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a80da1cf.154440f1.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a80da1cf.154440f1.js new file mode 100644 index 00000000..d16ab8a5 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a80da1cf.154440f1.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3205],{4863:s=>{s.exports=JSON.parse('{"label":"docusaurus","permalink":"/blog/tags/docusaurus","allTagsPath":"/blog/tags","count":4,"unlisted":false}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a94703ab.7bdd16ec.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a94703ab.7bdd16ec.js new file mode 100644 index 00000000..0cf99ecb --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/a94703ab.7bdd16ec.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[4368],{2674:(e,t,n)=>{n.r(t),n.d(t,{default:()=>be});var a=n(7294),o=n(512),i=n(1944),s=n(5281),l=n(2802),r=n(1116),c=n(5999),d=n(2466),u=n(5936);const m={backToTopButton:"backToTopButton_sjWU",backToTopButtonShow:"backToTopButtonShow_xfvO"};var b=n(5893);function h(){const{shown:e,scrollToTop:t}=function(e){let{threshold:t}=e;const[n,o]=(0,a.useState)(!1),i=(0,a.useRef)(!1),{startScroll:s,cancelScroll:l}=(0,d.Ct)();return(0,d.RF)(((e,n)=>{let{scrollY:a}=e;const s=n?.scrollY;s&&(i.current?i.current=!1:a>=s?(l(),o(!1)):a<t?o(!1):a+window.innerHeight<document.documentElement.scrollHeight&&o(!0))})),(0,u.S)((e=>{e.location.hash&&(i.current=!0,o(!1))})),{shown:n,scrollToTop:()=>s(0)}}({threshold:300});return(0,b.jsx)("button",{"aria-label":(0,c.I)({id:"theme.BackToTopButton.buttonAriaLabel",message:"Scroll back to top",description:"The ARIA label for the back to top button"}),className:(0,o.Z)("clean-btn",s.k.common.backToTopButton,m.backToTopButton,e&&m.backToTopButtonShow),type:"button",onClick:t})}var p=n(1442),x=n(6550),f=n(7524),j=n(6668),k=n(1327);function _(e){return(0,b.jsx)("svg",{width:"20",height:"20","aria-hidden":"true",...e,children:(0,b.jsxs)("g",{fill:"#7a7a7a",children:[(0,b.jsx)("path",{d:"M9.992 10.023c0 .2-.062.399-.172.547l-4.996 7.492a.982.982 0 01-.828.454H1c-.55 0-1-.453-1-1 0-.2.059-.403.168-.551l4.629-6.942L.168 3.078A.939.939 0 010 2.528c0-.548.45-.997 1-.997h2.996c.352 0 .649.18.828.45L9.82 9.472c.11.148.172.347.172.55zm0 0"}),(0,b.jsx)("path",{d:"M19.98 10.023c0 .2-.058.399-.168.547l-4.996 7.492a.987.987 0 01-.828.454h-3c-.547 0-.996-.453-.996-1 0-.2.059-.403.168-.551l4.625-6.942-4.625-6.945a.939.939 0 01-.168-.55 1 1 0 01.996-.997h3c.348 0 .649.18.828.45l4.996 7.492c.11.148.168.347.168.55zm0 0"})]})})}const v={collapseSidebarButton:"collapseSidebarButton_PEFL",collapseSidebarButtonIcon:"collapseSidebarButtonIcon_kv0_"};function g(e){let{onClick:t}=e;return(0,b.jsx)("button",{type:"button",title:(0,c.I)({id:"theme.docs.sidebar.collapseButtonTitle",message:"Collapse sidebar",description:"The title attribute for collapse button of doc sidebar"}),"aria-label":(0,c.I)({id:"theme.docs.sidebar.collapseButtonAriaLabel",message:"Collapse sidebar",description:"The title attribute for collapse button of doc sidebar"}),className:(0,o.Z)("button button--secondary button--outline",v.collapseSidebarButton),onClick:t,children:(0,b.jsx)(_,{className:v.collapseSidebarButtonIcon})})}var C=n(9689),S=n(902);const I=Symbol("EmptyContext"),N=a.createContext(I);function T(e){let{children:t}=e;const[n,o]=(0,a.useState)(null),i=(0,a.useMemo)((()=>({expandedItem:n,setExpandedItem:o})),[n]);return(0,b.jsx)(N.Provider,{value:i,children:t})}var B=n(6043),Z=n(8596),A=n(3692),L=n(2389);function y(e){let{collapsed:t,categoryLabel:n,onClick:a}=e;return(0,b.jsx)("button",{"aria-label":t?(0,c.I)({id:"theme.DocSidebarItem.expandCategoryAriaLabel",message:"Expand sidebar category '{label}'",description:"The ARIA label to expand the sidebar category"},{label:n}):(0,c.I)({id:"theme.DocSidebarItem.collapseCategoryAriaLabel",message:"Collapse sidebar category '{label}'",description:"The ARIA label to collapse the sidebar category"},{label:n}),type:"button",className:"clean-btn menu__caret",onClick:a})}function w(e){let{item:t,onItemClick:n,activePath:i,level:r,index:c,...d}=e;const{items:u,label:m,collapsible:h,className:p,href:x}=t,{docs:{sidebar:{autoCollapseCategories:f}}}=(0,j.L)(),k=function(e){const t=(0,L.Z)();return(0,a.useMemo)((()=>e.href&&!e.linkUnlisted?e.href:!t&&e.collapsible?(0,l.LM)(e):void 0),[e,t])}(t),_=(0,l._F)(t,i),v=(0,Z.Mg)(x,i),{collapsed:g,setCollapsed:C}=(0,B.u)({initialState:()=>!!h&&(!_&&t.collapsed)}),{expandedItem:T,setExpandedItem:w}=function(){const e=(0,a.useContext)(N);if(e===I)throw new S.i6("DocSidebarItemsExpandedStateProvider");return e}(),E=function(e){void 0===e&&(e=!g),w(e?null:c),C(e)};return function(e){let{isActive:t,collapsed:n,updateCollapsed:o}=e;const i=(0,S.D9)(t);(0,a.useEffect)((()=>{t&&!i&&n&&o(!1)}),[t,i,n,o])}({isActive:_,collapsed:g,updateCollapsed:E}),(0,a.useEffect)((()=>{h&&null!=T&&T!==c&&f&&C(!0)}),[h,T,c,C,f]),(0,b.jsxs)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemCategory,s.k.docs.docSidebarItemCategoryLevel(r),"menu__list-item",{"menu__list-item--collapsed":g},p),children:[(0,b.jsxs)("div",{className:(0,o.Z)("menu__list-item-collapsible",{"menu__list-item-collapsible--active":v}),children:[(0,b.jsx)(A.Z,{className:(0,o.Z)("menu__link",{"menu__link--sublist":h,"menu__link--sublist-caret":!x&&h,"menu__link--active":_}),onClick:h?e=>{n?.(t),x?E(!1):(e.preventDefault(),E())}:()=>{n?.(t)},"aria-current":v?"page":void 0,"aria-expanded":h?!g:void 0,href:h?k??"#":k,...d,children:m}),x&&h&&(0,b.jsx)(y,{collapsed:g,categoryLabel:m,onClick:e=>{e.preventDefault(),E()}})]}),(0,b.jsx)(B.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:g,children:(0,b.jsx)(V,{items:u,tabIndex:g?-1:0,onItemClick:n,activePath:i,level:r+1})})]})}var E=n(3919),H=n(9471);const M={menuExternalLink:"menuExternalLink_NmtK"};function R(e){let{item:t,onItemClick:n,activePath:a,level:i,index:r,...c}=e;const{href:d,label:u,className:m,autoAddBaseUrl:h}=t,p=(0,l._F)(t,a),x=(0,E.Z)(d);return(0,b.jsx)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemLink,s.k.docs.docSidebarItemLinkLevel(i),"menu__list-item",m),children:(0,b.jsxs)(A.Z,{className:(0,o.Z)("menu__link",!x&&M.menuExternalLink,{"menu__link--active":p}),autoAddBaseUrl:h,"aria-current":p?"page":void 0,to:d,...x&&{onClick:n?()=>n(t):void 0},...c,children:[u,!x&&(0,b.jsx)(H.Z,{})]})},u)}const W={menuHtmlItem:"menuHtmlItem_M9Kj"};function F(e){let{item:t,level:n,index:a}=e;const{value:i,defaultStyle:l,className:r}=t;return(0,b.jsx)("li",{className:(0,o.Z)(s.k.docs.docSidebarItemLink,s.k.docs.docSidebarItemLinkLevel(n),l&&[W.menuHtmlItem,"menu__list-item"],r),dangerouslySetInnerHTML:{__html:i}},a)}function P(e){let{item:t,...n}=e;switch(t.type){case"category":return(0,b.jsx)(w,{item:t,...n});case"html":return(0,b.jsx)(F,{item:t,...n});default:return(0,b.jsx)(R,{item:t,...n})}}function D(e){let{items:t,...n}=e;const a=(0,l.f)(t,n.activePath);return(0,b.jsx)(T,{children:a.map(((e,t)=>(0,b.jsx)(P,{item:e,index:t,...n},t)))})}const V=(0,a.memo)(D),U={menu:"menu_SIkG",menuWithAnnouncementBar:"menuWithAnnouncementBar_GW3s"};function K(e){let{path:t,sidebar:n,className:i}=e;const l=function(){const{isActive:e}=(0,C.nT)(),[t,n]=(0,a.useState)(e);return(0,d.RF)((t=>{let{scrollY:a}=t;e&&n(0===a)}),[e]),e&&t}();return(0,b.jsx)("nav",{"aria-label":(0,c.I)({id:"theme.docs.sidebar.navAriaLabel",message:"Docs sidebar",description:"The ARIA label for the sidebar navigation"}),className:(0,o.Z)("menu thin-scrollbar",U.menu,l&&U.menuWithAnnouncementBar,i),children:(0,b.jsx)("ul",{className:(0,o.Z)(s.k.docs.docSidebarMenu,"menu__list"),children:(0,b.jsx)(V,{items:n,activePath:t,level:1})})})}const Y="sidebar_njMd",z="sidebarWithHideableNavbar_wUlq",G="sidebarHidden_VK0M",O="sidebarLogo_isFc";function q(e){let{path:t,sidebar:n,onCollapse:a,isHidden:i}=e;const{navbar:{hideOnScroll:s},docs:{sidebar:{hideable:l}}}=(0,j.L)();return(0,b.jsxs)("div",{className:(0,o.Z)(Y,s&&z,i&&G),children:[s&&(0,b.jsx)(k.Z,{tabIndex:-1,className:O}),(0,b.jsx)(K,{path:t,sidebar:n}),l&&(0,b.jsx)(g,{onClick:a})]})}const J=a.memo(q);var Q=n(3102),X=n(2961);const $=e=>{let{sidebar:t,path:n}=e;const a=(0,X.e)();return(0,b.jsx)("ul",{className:(0,o.Z)(s.k.docs.docSidebarMenu,"menu__list"),children:(0,b.jsx)(V,{items:t,activePath:n,onItemClick:e=>{"category"===e.type&&e.href&&a.toggle(),"link"===e.type&&a.toggle()},level:1})})};function ee(e){return(0,b.jsx)(Q.Zo,{component:$,props:e})}const te=a.memo(ee);function ne(e){const t=(0,f.i)(),n="desktop"===t||"ssr"===t,a="mobile"===t;return(0,b.jsxs)(b.Fragment,{children:[n&&(0,b.jsx)(J,{...e}),a&&(0,b.jsx)(te,{...e})]})}const ae={expandButton:"expandButton_TmdG",expandButtonIcon:"expandButtonIcon_i1dp"};function oe(e){let{toggleSidebar:t}=e;return(0,b.jsx)("div",{className:ae.expandButton,title:(0,c.I)({id:"theme.docs.sidebar.expandButtonTitle",message:"Expand sidebar",description:"The ARIA label and title attribute for expand button of doc sidebar"}),"aria-label":(0,c.I)({id:"theme.docs.sidebar.expandButtonAriaLabel",message:"Expand sidebar",description:"The ARIA label and title attribute for expand button of doc sidebar"}),tabIndex:0,role:"button",onKeyDown:t,onClick:t,children:(0,b.jsx)(_,{className:ae.expandButtonIcon})})}const ie={docSidebarContainer:"docSidebarContainer_YfHR",docSidebarContainerHidden:"docSidebarContainerHidden_DPk8",sidebarViewport:"sidebarViewport_aRkj"};function se(e){let{children:t}=e;const n=(0,r.V)();return(0,b.jsx)(a.Fragment,{children:t},n?.name??"noSidebar")}function le(e){let{sidebar:t,hiddenSidebarContainer:n,setHiddenSidebarContainer:i}=e;const{pathname:l}=(0,x.TH)(),[r,c]=(0,a.useState)(!1),d=(0,a.useCallback)((()=>{r&&c(!1),!r&&(0,p.n)()&&c(!0),i((e=>!e))}),[i,r]);return(0,b.jsx)("aside",{className:(0,o.Z)(s.k.docs.docSidebarContainer,ie.docSidebarContainer,n&&ie.docSidebarContainerHidden),onTransitionEnd:e=>{e.currentTarget.classList.contains(ie.docSidebarContainer)&&n&&c(!0)},children:(0,b.jsx)(se,{children:(0,b.jsxs)("div",{className:(0,o.Z)(ie.sidebarViewport,r&&ie.sidebarViewportHidden),children:[(0,b.jsx)(ne,{sidebar:t,path:l,onCollapse:d,isHidden:r}),r&&(0,b.jsx)(oe,{toggleSidebar:d})]})})})}const re={docMainContainer:"docMainContainer_TBSr",docMainContainerEnhanced:"docMainContainerEnhanced_lQrH",docItemWrapperEnhanced:"docItemWrapperEnhanced_JWYK"};function ce(e){let{hiddenSidebarContainer:t,children:n}=e;const a=(0,r.V)();return(0,b.jsx)("main",{className:(0,o.Z)(re.docMainContainer,(t||!a)&&re.docMainContainerEnhanced),children:(0,b.jsx)("div",{className:(0,o.Z)("container padding-top--md padding-bottom--lg",re.docItemWrapper,t&&re.docItemWrapperEnhanced),children:n})})}const de={docRoot:"docRoot_UBD9",docsWrapper:"docsWrapper_hBAB"};function ue(e){let{children:t}=e;const n=(0,r.V)(),[o,i]=(0,a.useState)(!1);return(0,b.jsxs)("div",{className:de.docsWrapper,children:[(0,b.jsx)(h,{}),(0,b.jsxs)("div",{className:de.docRoot,children:[n&&(0,b.jsx)(le,{sidebar:n.items,hiddenSidebarContainer:o,setHiddenSidebarContainer:i}),(0,b.jsx)(ce,{hiddenSidebarContainer:o,children:t})]})]})}var me=n(5658);function be(e){const t=(0,l.SN)(e);if(!t)return(0,b.jsx)(me.Z,{});const{docElement:n,sidebarName:a,sidebarItems:c}=t;return(0,b.jsx)(i.FG,{className:(0,o.Z)(s.k.page.docsDocPage),children:(0,b.jsx)(r.b,{name:a,items:c,children:(0,b.jsx)(ue,{children:n})})})}},5658:(e,t,n)=>{n.d(t,{Z:()=>l});n(7294);var a=n(512),o=n(5999),i=n(2503),s=n(5893);function l(e){let{className:t}=e;return(0,s.jsx)("main",{className:(0,a.Z)("container margin-vert--xl",t),children:(0,s.jsx)("div",{className:"row",children:(0,s.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,s.jsx)(i.Z,{as:"h1",className:"hero__title",children:(0,s.jsx)(o.Z,{id:"theme.NotFound.title",description:"The title of the 404 page",children:"Page Not Found"})}),(0,s.jsx)("p",{children:(0,s.jsx)(o.Z,{id:"theme.NotFound.p1",description:"The first paragraph of the 404 page",children:"We could not find what you were looking for."})}),(0,s.jsx)("p",{children:(0,s.jsx)(o.Z,{id:"theme.NotFound.p2",description:"The 2nd paragraph of the 404 page",children:"Please contact the owner of the site that linked you to the original URL and let them know their link is broken."})})]})})})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b1e73d9b.341c4aee.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b1e73d9b.341c4aee.js new file mode 100644 index 00000000..f0c037b1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b1e73d9b.341c4aee.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1071],{3510:(e,n,i)=>{i.r(n),i.d(n,{assets:()=>d,contentTitle:()=>a,default:()=>h,frontMatter:()=>o,metadata:()=>r,toc:()=>c});var t=i(5893),s=i(1151);const o={},a="VBM Admin Configuration",r={id:"configuration/vbmadminconfig",title:"VBM Admin Configuration",description:"TAK Server can allow for additional filtering of cot messages recieved from inputs (server ports) and data feeds by using the VBM Configuration page in the Admin UI. To navigate there, go to Administative \\> VBM Configuration as shown below.",source:"@site/docs/configuration/vbmadminconfig.md",sourceDirName:"configuration",slug:"/configuration/vbmadminconfig",permalink:"/docs/configuration/vbmadminconfig",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/vbmadminconfig.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Optionally Disabling UI and WebTAK on HTTPS Ports",permalink:"/docs/configuration/optionallydisableui"},next:{title:"WebTAK",permalink:"/docs/webtak"}},d={},c=[];function l(e){const n={em:"em",h1:"h1",img:"img",p:"p",...(0,s.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"vbm-admin-configuration",children:"VBM Admin Configuration"}),"\n",(0,t.jsx)(n.p,{children:"TAK Server can allow for additional filtering of cot messages recieved from inputs (server ports) and data feeds by using the VBM Configuration page in the Admin UI. To navigate there, go to Administative > VBM Configuration as shown below."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"VBM Admin Configuration",src:i(3892).Z+"",width:"286",height:"614"})}),"\n",(0,t.jsx)(n.p,{children:"You will then see the following options."}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"VBM Admin Configuration",src:i(3641).Z+"",width:"244",height:"196"})}),"\n",(0,t.jsx)(n.p,{children:'To modify the VBM configurations select the checkbox next to the desired option and when you\'re finished click "Save changes".'}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.em,{children:'NOTE: "Disable SA Sharing" and "Disable Chat Sharing" will only be used if "Enable VBM" is selected.'})}),"\n",(0,t.jsx)(n.p,{children:"The VBM options have the following impact:"}),"\n",(0,t.jsx)(n.p,{children:'If "Enable VBM" is selected, messages recieved on a data feed are only brokered to clients which are subscribed to the mission if the message falls within the bounding box specified by the mission. For example, the message represented by the blue dot would be passed on while the message represented by the green dot would be filtered out for the mission with the displayed bounding box only if "Enable VBM" is turned on.'}),"\n",(0,t.jsx)(n.p,{children:(0,t.jsx)(n.img,{alt:"VBM Admin Configuration",src:i(9106).Z+"",width:"662",height:"448"})}),"\n",(0,t.jsx)(n.p,{children:'The second two options are only activated if "Enable VBM" is on and they refer to messages recieved from inputs (server ports). These options filter messages based on whether they are chat messages. Chat messages in this context are cot messages which have a type set to "b-t-f".'}),"\n",(0,t.jsx)(n.p,{children:'If "Disable SA Sharing" is selected, messages recieved from inputs are passed on if the message is a chat message as defined above.'}),"\n",(0,t.jsx)(n.p,{children:'If "Disable Chat Sharing" is selected, messages recieved from inputs are passed on if the messages is NOT a chat message as defined above.'}),"\n",(0,t.jsx)(n.p,{children:"These options are not mutually exclusive. Therefore, having both selected will filter out all messages recieved on inputs."})]})}function h(e={}){const{wrapper:n}={...(0,s.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(l,{...e})}):l(e)}},3892:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/vbm_admin_1-75153dbba7aa3d4932b48ec67c867c12.png"},3641:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/vbm_admin_2-82af839caa956a42c2b26eef01cd0a3e.png"},9106:(e,n,i)=>{i.d(n,{Z:()=>t});const t=i.p+"assets/images/vbm_admin_3-038e6792eb748581579c02a4f202dfa9.png"},1151:(e,n,i)=>{i.d(n,{Z:()=>r,a:()=>a});var t=i(7294);const s={},o=t.createContext(s);function a(e){const n=t.useContext(o);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function r(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),t.createElement(o.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b23f3d07.42d2421f.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b23f3d07.42d2421f.js new file mode 100644 index 00000000..a51aa62f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b23f3d07.42d2421f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9077],{4469:s=>{s.exports=JSON.parse('{"name":"docusaurus-plugin-content-blog","id":"default"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b2b675dd.d4c820a0.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b2b675dd.d4c820a0.js new file mode 100644 index 00000000..7195e011 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b2b675dd.d4c820a0.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[533],{8017:s=>{s.exports=JSON.parse('{"permalink":"/blog","page":1,"postsPerPage":10,"totalPages":1,"totalCount":5,"blogDescription":"Blog","blogTitle":"Blog"}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b2f554cd.6bbd0984.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b2f554cd.6bbd0984.js new file mode 100644 index 00000000..8c5c16cf --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b2f554cd.6bbd0984.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1477],{10:e=>{e.exports=JSON.parse('{"blogPosts":[{"id":"/2024/01/28/welcome-to-docusaurus-static","metadata":{"permalink":"/blog/2024/01/28/welcome-to-docusaurus-static","editUrl":"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2024-01-28-welcome-to-docusaurus-static.md","source":"@site/blog/2024-01-28-welcome-to-docusaurus-static.md","title":"Welcome to Docusaurus-Static!","description":"Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.","date":"2024-01-28T00:00:00.000Z","formattedDate":"January 28, 2024","tags":[{"label":"hello","permalink":"/blog/tags/hello"},{"label":"docusaurus-static","permalink":"/blog/tags/docusaurus-static"}],"readingTime":0.38,"hasTruncateMarker":false,"authors":[{"name":"OctoSpacc","title":"Chief Executive Officer @ Spacc Inc.","url":"https://hub.octt.eu.org","image_url":"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png","imageURL":"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}],"frontMatter":{"tags":["hello","docusaurus-static"],"authors":{"name":"OctoSpacc","title":"Chief Executive Officer @ Spacc Inc.","url":"https://hub.octt.eu.org","image_url":"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png","imageURL":"https://gitlab.com/uploads/-/system/user/avatar/6083316/avatar.png"}},"unlisted":false,"nextItem":{"title":"Welcome to Docusaurus!","permalink":"/blog/welcome"}},"content":"Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.\\n\\nNot only can the built site be navigated as a collection of static HTML pages from `file:///` locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you\'re not already on it, try opening <a href=\\"/docusaurus-static-single-file.html\\">docusaurus-static-single-file.html</a>..."},{"id":"welcome","metadata":{"permalink":"/blog/welcome","editUrl":"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2021-08-26-welcome/index.md","source":"@site/blog/2021-08-26-welcome/index.md","title":"Welcome to Docusaurus!","description":"Docusaurus blogging features are powered by the blog plugin.","date":"2021-08-26T00:00:00.000Z","formattedDate":"August 26, 2021","tags":[{"label":"facebook","permalink":"/blog/tags/facebook"},{"label":"hello","permalink":"/blog/tags/hello"},{"label":"docusaurus","permalink":"/blog/tags/docusaurus"}],"readingTime":0.405,"hasTruncateMarker":false,"authors":[{"name":"S\xe9bastien Lorber","title":"Docusaurus maintainer","url":"https://sebastienlorber.com","imageURL":"https://github.com/slorber.png","key":"slorber"},{"name":"Yangshun Tay","title":"Front End Engineer @ Facebook","url":"https://github.com/yangshun","imageURL":"https://github.com/yangshun.png","key":"yangshun"}],"frontMatter":{"slug":"welcome","title":"Welcome to Docusaurus!","authors":["slorber","yangshun"],"tags":["facebook","hello","docusaurus"]},"unlisted":false,"prevItem":{"title":"Welcome to Docusaurus-Static!","permalink":"/blog/2024/01/28/welcome-to-docusaurus-static"},"nextItem":{"title":"MDX Blog Post","permalink":"/blog/mdx-blog-post"}},"content":"[Docusaurus blogging features](https://docusaurus.io/docs/blog) are powered by the [blog plugin](https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog).\\n\\nSimply add Markdown files (or folders) to the `blog` directory.\\n\\nRegular blog authors can be added to `authors.yml`.\\n\\nThe blog post date can be extracted from filenames, such as:\\n\\n- `2019-05-30-welcome.md`\\n- `2019-05-30-welcome/index.md`\\n\\nA blog post folder can be convenient to co-locate blog post images:\\n\\n![Docusaurus Plushie](./docusaurus-plushie-banner.jpeg)\\n\\nThe blog supports tags as well!\\n\\n**And if you don\'t want a blog**: just delete this directory, and use `blog: false` in your Docusaurus config."},{"id":"mdx-blog-post","metadata":{"permalink":"/blog/mdx-blog-post","editUrl":"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2021-08-01-mdx-blog-post.mdx","source":"@site/blog/2021-08-01-mdx-blog-post.mdx","title":"MDX Blog Post","description":"Blog posts support Docusaurus Markdown features, such as MDX.","date":"2021-08-01T00:00:00.000Z","formattedDate":"August 1, 2021","tags":[{"label":"docusaurus","permalink":"/blog/tags/docusaurus"}],"readingTime":0.175,"hasTruncateMarker":false,"authors":[{"name":"S\xe9bastien Lorber","title":"Docusaurus maintainer","url":"https://sebastienlorber.com","imageURL":"https://github.com/slorber.png","key":"slorber"}],"frontMatter":{"slug":"mdx-blog-post","title":"MDX Blog Post","authors":["slorber"],"tags":["docusaurus"]},"unlisted":false,"prevItem":{"title":"Welcome to Docusaurus!","permalink":"/blog/welcome"},"nextItem":{"title":"Long Blog Post","permalink":"/blog/long-blog-post"}},"content":"Blog posts support [Docusaurus Markdown features](https://docusaurus.io/docs/markdown-features), such as [MDX](https://mdxjs.com/).\\n\\n:::tip\\n\\nUse the power of React to create interactive blog posts.\\n\\n```js\\n<button onClick={() => alert(\'button clicked!\')}>Click me!</button>\\n```\\n\\n<button onClick={() => alert(\'button clicked!\')}>Click me!</button>\\n\\n:::"},{"id":"long-blog-post","metadata":{"permalink":"/blog/long-blog-post","editUrl":"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2019-05-29-long-blog-post.md","source":"@site/blog/2019-05-29-long-blog-post.md","title":"Long Blog Post","description":"This is the summary of a very long blog post,","date":"2019-05-29T00:00:00.000Z","formattedDate":"May 29, 2019","tags":[{"label":"hello","permalink":"/blog/tags/hello"},{"label":"docusaurus","permalink":"/blog/tags/docusaurus"}],"readingTime":2.05,"hasTruncateMarker":true,"authors":[{"name":"Endilie Yacop Sucipto","title":"Maintainer of Docusaurus","url":"https://github.com/endiliey","imageURL":"https://github.com/endiliey.png","key":"endi"}],"frontMatter":{"slug":"long-blog-post","title":"Long Blog Post","authors":"endi","tags":["hello","docusaurus"]},"unlisted":false,"prevItem":{"title":"MDX Blog Post","permalink":"/blog/mdx-blog-post"},"nextItem":{"title":"First Blog Post","permalink":"/blog/first-blog-post"}},"content":"This is the summary of a very long blog post,\\n\\nUse a `\x3c!--` `truncate` `--\x3e` comment to limit blog post size in the list view.\\n\\n\x3c!--truncate--\x3e\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet\\n\\nLorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"},{"id":"first-blog-post","metadata":{"permalink":"/blog/first-blog-post","editUrl":"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2019-05-28-first-blog-post.md","source":"@site/blog/2019-05-28-first-blog-post.md","title":"First Blog Post","description":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet","date":"2019-05-28T00:00:00.000Z","formattedDate":"May 28, 2019","tags":[{"label":"hola","permalink":"/blog/tags/hola"},{"label":"docusaurus","permalink":"/blog/tags/docusaurus"}],"readingTime":0.12,"hasTruncateMarker":false,"authors":[{"name":"Gao Wei","title":"Docusaurus Core Team","url":"https://github.com/wgao19","image_url":"https://github.com/wgao19.png","imageURL":"https://github.com/wgao19.png"}],"frontMatter":{"slug":"first-blog-post","title":"First Blog Post","authors":{"name":"Gao Wei","title":"Docusaurus Core Team","url":"https://github.com/wgao19","image_url":"https://github.com/wgao19.png","imageURL":"https://github.com/wgao19.png"},"tags":["hola","docusaurus"]},"unlisted":false,"prevItem":{"title":"Long Blog Post","permalink":"/blog/long-blog-post"}},"content":"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"}]}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b31dded6.6a386031.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b31dded6.6a386031.js new file mode 100644 index 00000000..9308e32f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/b31dded6.6a386031.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9261],{260:(e,t,a)=>{a.r(t),a.d(t,{assets:()=>r,contentTitle:()=>s,default:()=>f,frontMatter:()=>o,metadata:()=>c,toc:()=>d});var n=a(5893),i=a(1151);const o={},s="Data Package and Mission File Blocker",c={id:"federation/datapackagemissionfileblocker",title:"Data Package and Mission File Blocker",description:"Data packages, mission packages, and missions can be federated between servers and their respective ATAK clients. These packages may contain configuration files such as ATAK .pref files that can result in the distribution of unwanted configuration changes to ATAK devices. A filter can be enabled to block files by file-type. To enable this feature, check the Data Package and Mission File Blocker box in the Federation Configuration page. The default file extension value is 'pref'. This can be changed by entering a new file type, clicking on the 'Add' button to add the entry, and clicking on the 'Save' buton at the bottom of the Federation Configuration page.",source:"@site/docs/federation/datapackagemissionfileblocker.md",sourceDirName:"federation",slug:"/federation/datapackagemissionfileblocker",permalink:"/docs/federation/datapackagemissionfileblocker",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/datapackagemissionfileblocker.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Mission Federation Disruption Tolerance",permalink:"/docs/federation/federationdisruptiontolerance"},next:{title:"Federation Example",permalink:"/docs/federation/federationexample"}},r={},d=[];function l(e){const t={h1:"h1",img:"img",p:"p",...(0,i.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"data-package-and-mission-file-blocker",children:"Data Package and Mission File Blocker"}),"\n",(0,n.jsx)(t.p,{children:"Data packages, mission packages, and missions can be federated between servers and their respective ATAK clients. These packages may contain configuration files such as ATAK .pref files that can result in the distribution of unwanted configuration changes to ATAK devices. A filter can be enabled to block files by file-type. To enable this feature, check the Data Package and Mission File Blocker box in the Federation Configuration page. The default file extension value is 'pref'. This can be changed by entering a new file type, clicking on the 'Add' button to add the entry, and clicking on the 'Save' buton at the bottom of the Federation Configuration page."}),"\n",(0,n.jsx)(t.p,{children:(0,n.jsx)(t.img,{alt:"Data Package and Mission File Blocker Box",src:a(4130).Z+"",width:"789",height:"234"})})]})}function f(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(l,{...e})}):l(e)}},4130:(e,t,a)=>{a.d(t,{Z:()=>n});const n=a.p+"assets/images/fed_9-e93e587a011894fdea4f18d0dad2c949.png"},1151:(e,t,a)=>{a.d(t,{Z:()=>c,a:()=>s});var n=a(7294);const i={},o=n.createContext(i);function s(e){const t=n.useContext(o);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function c(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:s(e.components),n.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/bb1b26fa.bc233fea.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/bb1b26fa.bc233fea.js new file mode 100644 index 00000000..3955c555 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/bb1b26fa.bc233fea.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2587],{6660:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>c,contentTitle:()=>a,default:()=>u,frontMatter:()=>s,metadata:()=>l,toc:()=>i});var t=r(5893),o=r(1151);const s={},a="RHEL, Rocky Linux, and CentOS",l={id:"firewall/rhelrockycentos",title:"RHEL, Rocky Linux, and CentOS",description:"To verify whether a firewall is running, use the command:",source:"@site/docs/firewall/rhelrockycentos.md",sourceDirName:"firewall",slug:"/firewall/rhelrockycentos",permalink:"/docs/firewall/rhelrockycentos",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/firewall/rhelrockycentos.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Overview",permalink:"/docs/firewall/overview"},next:{title:"Ubuntu and Raspberry Pi",permalink:"/docs/firewall/ubunturaspberrypi"}},c={},i=[];function d(e){const n={code:"code",h1:"h1",p:"p",pre:"pre",...(0,o.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(n.h1,{id:"rhel-rocky-linux-and-centos",children:"RHEL, Rocky Linux, and CentOS"}),"\n",(0,t.jsx)(n.p,{children:"To verify whether a firewall is running, use the command:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo systemctl status firewalld.service\n"})}),"\n",(0,t.jsx)(n.p,{children:"To see what zones are running"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo firewall-cmd --get-active-zones\n"})}),"\n",(0,t.jsx)(n.p,{children:"If you are working from a fresh OS install, the only active zone is 'public'."}),"\n",(0,t.jsx)(n.p,{children:"For each each zone, you'll want to enable TCP (and possibly UDP) ports for the inputs in your CoreConfig.xml file, plus the web server's port. For example,"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo firewall-cmd --zone=public --add-port 8089/tcp -permanent\nsudo firewall-cmd --zone=public --add-port 8443/tcp --permanent\n"})}),"\n",(0,t.jsx)(n.p,{children:"The ports you'll need to open for the default configuration are 8089 and 8443."}),"\n",(0,t.jsx)(n.p,{children:"Finally, enable your new firewall rules:"}),"\n",(0,t.jsx)(n.pre,{children:(0,t.jsx)(n.code,{className:"language-bash",children:"sudo firewall-cmd -reload\n"})})]})}function u(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,t.jsx)(n,{...e,children:(0,t.jsx)(d,{...e})}):d(e)}},1151:(e,n,r)=>{r.d(n,{Z:()=>l,a:()=>a});var t=r(7294);const o={},s=t.createContext(o);function a(e){const n=t.useContext(s);return t.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function l(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:a(e.components),t.createElement(s.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c451c5ed.574fb96c.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c451c5ed.574fb96c.js new file mode 100644 index 00000000..c37e853b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c451c5ed.574fb96c.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8278],{7895:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>d,contentTitle:()=>i,default:()=>l,frontMatter:()=>a,metadata:()=>s,toc:()=>c});var o=n(5893),r=n(1151);const a={},i="Enable Federation",s={id:"federation/enablefederation",title:"Enable Federation",description:"The first step is to enable federation on your TAK server. To do this, first go the Configuration > Manage Federates page. If federation is not yet enabled, click on the Edit Configuration button. This is also where you can pick the ports for each federation protocol.",source:"@site/docs/federation/enablefederation.md",sourceDirName:"federation",slug:"/federation/enablefederation",permalink:"/docs/federation/enablefederation",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/enablefederation.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Overview",permalink:"/docs/federation/overview"},next:{title:"Upload Federate Certificate",permalink:"/docs/federation/uploadfederatecert"}},d={},c=[];function f(e){const t={h1:"h1",img:"img",p:"p",...(0,r.a)(),...e.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(t.h1,{id:"enable-federation",children:"Enable Federation"}),"\n",(0,o.jsx)(t.p,{children:"The first step is to enable federation on your TAK server. To do this, first go the Configuration > Manage Federates page. If federation is not yet enabled, click on the Edit Configuration button. This is also where you can pick the ports for each federation protocol."}),"\n",(0,o.jsx)(t.p,{children:(0,o.jsx)(t.img,{alt:"Enable Federation",src:n(4794).Z+"",width:"1053",height:"551"})}),"\n",(0,o.jsx)(t.p,{children:"Do not forget to restart the server after changing the federation configuration in order for the changes to take effect!"})]})}function l(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,o.jsx)(t,{...e,children:(0,o.jsx)(f,{...e})}):f(e)}},4794:(e,t,n)=>{n.d(t,{Z:()=>o});const o=n.p+"assets/images/fed_1-dbe72a427d923b6018ff4a4c7dfd5313.png"},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>i});var o=n(7294);const r={},a=o.createContext(r);function i(e){const t=o.useContext(a);return o.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:i(e.components),o.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c4f5d8e4.278ca1e5.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c4f5d8e4.278ca1e5.js new file mode 100644 index 00000000..0349d3b8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c4f5d8e4.278ca1e5.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[4195],{9722:(e,a,t)=>{t.d(a,{Z:()=>c});var l,r=t(7294);function n(){return n=Object.assign?Object.assign.bind():function(e){for(var a=1;a<arguments.length;a++){var t=arguments[a];for(var l in t)Object.prototype.hasOwnProperty.call(t,l)&&(e[l]=t[l])}return e},n.apply(this,arguments)}const c=e=>{let{title:a,titleId:t,...c}=e;return r.createElement("svg",n({xmlns:"http://www.w3.org/2000/svg",width:1088,height:687.962,viewBox:"0 0 1088 687.962","aria-labelledby":t},c),void 0===a?r.createElement("title",{id:t},"Easy to Use"):a?r.createElement("title",{id:t},a):null,l||(l=r.createElement("g",{"data-name":"Group 12"},r.createElement("g",{"data-name":"Group 11"},r.createElement("path",{"data-name":"Path 83",d:"M961.81 454.442c-5.27 45.15-16.22 81.4-31.25 110.31-20 38.52-54.21 54.04-84.77 70.28a193.275 193.275 0 0 1-27.46 11.94c-55.61 19.3-117.85 14.18-166.74 3.99a657.282 657.282 0 0 0-104.09-13.16q-14.97-.675-29.97-.67c-15.42.02-293.07 5.29-360.67-131.57-16.69-33.76-28.13-75-32.24-125.27-11.63-142.12 52.29-235.46 134.74-296.47 155.97-115.41 369.76-110.57 523.43 7.88 102.36 78.9 198.2 198.31 179.02 362.74Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 84",d:"M930.56 564.752c-20 38.52-47.21 64.04-77.77 80.28a193.272 193.272 0 0 1-27.46 11.94c-55.61 19.3-117.85 14.18-166.74 3.99a657.3 657.3 0 0 0-104.09-13.16q-14.97-.675-29.97-.67-23.13.03-46.25 1.72c-100.17 7.36-253.82-6.43-321.42-143.29L326 177.962l62.95 161.619 20.09 51.59 55.37-75.98L493 275.962l130.2 149.27 36.8-81.27 254.78 207.919 14.21 11.59Z",fill:"#f2f2f2"}),r.createElement("path",{"data-name":"Path 85",d:"m302 282.962 26-57 36 83-31-60Z",opacity:.1}),r.createElement("path",{"data-name":"Path 86",d:"M554.5 647.802q-14.97-.675-29.97-.67l-115.49-255.96Z",opacity:.1}),r.createElement("path",{"data-name":"Path 87",d:"M464.411 315.191 493 292.962l130 150-132-128Z",opacity:.1}),r.createElement("path",{"data-name":"Path 88",d:"M852.79 645.032a193.265 193.265 0 0 1-27.46 11.94L623.2 425.232Z",opacity:.1}),r.createElement("circle",{"data-name":"Ellipse 11",cx:3,cy:3,r:3,transform:"translate(479 98.962)",fill:"#f2f2f2"}),r.createElement("circle",{"data-name":"Ellipse 12",cx:3,cy:3,r:3,transform:"translate(396 201.962)",fill:"#f2f2f2"}),r.createElement("circle",{"data-name":"Ellipse 13",cx:2,cy:2,r:2,transform:"translate(600 220.962)",fill:"#f2f2f2"}),r.createElement("circle",{"data-name":"Ellipse 14",cx:2,cy:2,r:2,transform:"translate(180 265.962)",fill:"#f2f2f2"}),r.createElement("circle",{"data-name":"Ellipse 15",cx:2,cy:2,r:2,transform:"translate(612 96.962)",fill:"#f2f2f2"}),r.createElement("circle",{"data-name":"Ellipse 16",cx:2,cy:2,r:2,transform:"translate(736 192.962)",fill:"#f2f2f2"}),r.createElement("circle",{"data-name":"Ellipse 17",cx:2,cy:2,r:2,transform:"translate(858 344.962)",fill:"#f2f2f2"}),r.createElement("path",{"data-name":"Path 89",d:"M306 121.222h-2.76v-2.76h-1.48v2.76H299v1.478h2.76v2.759h1.48V122.7H306Z",fill:"#f2f2f2"}),r.createElement("path",{"data-name":"Path 90",d:"M848 424.222h-2.76v-2.76h-1.48v2.76H841v1.478h2.76v2.759h1.48V425.7H848Z",fill:"#f2f2f2"}),r.createElement("path",{"data-name":"Path 91",d:"M1088 613.962c0 16.569-243.557 74-544 74s-544-57.431-544-74 243.557 14 544 14 544-30.568 544-14Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 92",d:"M1088 613.962c0 16.569-243.557 74-544 74s-544-57.431-544-74 243.557 14 544 14 544-30.568 544-14Z",opacity:.1}),r.createElement("ellipse",{"data-name":"Ellipse 18",cx:544,cy:30,rx:544,ry:30,transform:"translate(0 583.962)",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 93",d:"M568 571.962c0 33.137-14.775 24-33 24s-33 9.137-33-24 33-96 33-96 33 62.863 33 96Z",fill:"#ff6584"}),r.createElement("path",{"data-name":"Path 94",d:"M550 584.641c0 15.062-6.716 10.909-15 10.909s-15 4.153-15-10.909 15-43.636 15-43.636 15 28.576 15 43.636Z",opacity:.1}),r.createElement("rect",{"data-name":"Rectangle 97",width:92,height:18,rx:9,transform:"translate(489 604.962)",fill:"#2f2e41"}),r.createElement("rect",{"data-name":"Rectangle 98",width:92,height:18,rx:9,transform:"translate(489 586.962)",fill:"#2f2e41"}),r.createElement("path",{"data-name":"Path 95",d:"M137 490.528c0 55.343 34.719 100.126 77.626 100.126",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 96",d:"M214.626 590.654c0-55.965 38.745-101.251 86.626-101.251",fill:"#6c63ff"}),r.createElement("path",{"data-name":"Path 97",d:"M165.125 495.545c0 52.57 22.14 95.109 49.5 95.109",fill:"#6c63ff"}),r.createElement("path",{"data-name":"Path 98",d:"M214.626 590.654c0-71.511 44.783-129.377 100.126-129.377",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 99",d:"M198.3 591.36s11.009-.339 14.326-2.7 16.934-5.183 17.757-1.395 16.544 18.844 4.115 18.945-28.879-1.936-32.19-3.953-4.008-10.897-4.008-10.897Z",fill:"#a8a8a8"}),r.createElement("path",{"data-name":"Path 100",d:"M234.716 604.89c-12.429.1-28.879-1.936-32.19-3.953-2.522-1.536-3.527-7.048-3.863-9.591l-.368.014s.7 8.879 4.009 10.9 19.761 4.053 32.19 3.953c3.588-.029 4.827-1.305 4.759-3.2-.498 1.142-1.867 1.855-4.537 1.877Z",opacity:.2}),r.createElement("path",{"data-name":"Path 101",d:"M721.429 527.062c0 38.029 23.857 68.8 53.341 68.8",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 102",d:"M774.769 595.863c0-38.456 26.623-69.575 59.525-69.575",fill:"#6c63ff"}),r.createElement("path",{"data-name":"Path 103",d:"M740.755 530.509c0 36.124 15.213 65.354 34.014 65.354",fill:"#6c63ff"}),r.createElement("path",{"data-name":"Path 104",d:"M774.769 595.863c0-49.139 30.773-88.9 68.8-88.9",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 105",d:"M763.548 596.348s7.565-.233 9.844-1.856 11.636-3.562 12.2-.958 11.368 12.949 2.828 13.018-19.844-1.33-22.119-2.716-2.753-7.488-2.753-7.488Z",fill:"#a8a8a8"}),r.createElement("path",{"data-name":"Path 106",d:"M788.574 605.645c-8.54.069-19.844-1.33-22.119-2.716-1.733-1.056-2.423-4.843-2.654-6.59l-.253.01s.479 6.1 2.755 7.487 13.579 2.785 22.119 2.716c2.465-.02 3.317-.9 3.27-2.2-.343.788-1.283 1.278-3.118 1.293Z",opacity:.2}),r.createElement("path",{"data-name":"Path 107",d:"M893.813 618.699s11.36-1.729 14.5-4.591 16.89-7.488 18.217-3.667 19.494 17.447 6.633 19.107-30.153 1.609-33.835-.065-5.515-10.784-5.515-10.784Z",fill:"#a8a8a8"}),r.createElement("path",{"data-name":"Path 108",d:"M933.228 628.154c-12.86 1.659-30.153 1.609-33.835-.065-2.8-1.275-4.535-6.858-5.2-9.45l-.379.061s1.833 9.109 5.516 10.783 20.975 1.725 33.835.065c3.712-.479 4.836-1.956 4.529-3.906-.375 1.246-1.703 2.156-4.466 2.512Z",opacity:.2}),r.createElement("path",{"data-name":"Path 109",d:"M614.26 617.881s9.587-1.459 12.237-3.875 14.255-6.32 15.374-3.095 16.452 14.725 5.6 16.125-25.448 1.358-28.555-.055-4.656-9.1-4.656-9.1Z",fill:"#a8a8a8"}),r.createElement("path",{"data-name":"Path 110",d:"M647.524 625.856c-10.853 1.4-25.448 1.358-28.555-.055-2.367-1.076-3.827-5.788-4.39-7.976l-.32.051s1.547 7.687 4.655 9.1 17.7 1.456 28.555.055c3.133-.4 4.081-1.651 3.822-3.3-.314 1.057-1.435 1.825-3.767 2.125Z",opacity:.2}),r.createElement("path",{"data-name":"Path 111",d:"M122.389 613.09s7.463-1.136 9.527-3.016 11.1-4.92 11.969-2.409 12.808 11.463 4.358 12.553-19.811 1.057-22.23-.043-3.624-7.085-3.624-7.085Z",fill:"#a8a8a8"}),r.createElement("path",{"data-name":"Path 112",d:"M148.285 619.302c-8.449 1.09-19.811 1.057-22.23-.043-1.842-.838-2.979-4.506-3.417-6.209l-.249.04s1.2 5.984 3.624 7.085 13.781 1.133 22.23.043c2.439-.315 3.177-1.285 2.976-2.566-.246.818-1.119 1.416-2.934 1.65Z",opacity:.2}),r.createElement("path",{"data-name":"Path 113",d:"M383.7 601.318c0 30.22-42.124 20.873-93.7 20.873s-93.074 9.347-93.074-20.873 42.118-36.793 93.694-36.793 93.08 6.573 93.08 36.793Z",opacity:.1}),r.createElement("path",{"data-name":"Path 114",d:"M383.7 593.881c0 30.22-42.124 20.873-93.7 20.873s-93.074 9.347-93.074-20.873 42.114-36.8 93.69-36.8 93.084 6.576 93.084 36.8Z",fill:"#3f3d56"})),r.createElement("path",{"data-name":"Path 40",d:"M360.175 475.732h91.791v37.153h-91.791Z",fill:"#fff",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 41",d:"M277.126 597.026a21.828 21.828 0 0 1-18.908-10.927 21.829 21.829 0 0 0 18.908 32.782h21.855v-21.855Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 42",d:"m375.451 481.607 76.514-4.782v-10.928a21.854 21.854 0 0 0-21.855-21.855h-98.347l-2.732-4.735a3.154 3.154 0 0 0-5.464 0l-2.732 4.732-2.732-4.732a3.154 3.154 0 0 0-5.464 0l-2.732 4.732-2.731-4.732a3.154 3.154 0 0 0-5.464 0l-2.732 4.735h-.071l-4.526-4.525a3.153 3.153 0 0 0-5.276 1.414l-1.5 5.577-5.674-1.521a3.154 3.154 0 0 0-3.863 3.864l1.52 5.679-5.575 1.494a3.155 3.155 0 0 0-1.416 5.278l4.526 4.526v.07l-4.735 2.731a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.732a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.731a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.727a3.154 3.154 0 0 0 0 5.464l4.735 2.736-4.735 2.732a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.732a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.731a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.732a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.731a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.731a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.735a3.154 3.154 0 0 0 0 5.464l4.732 2.732-4.732 2.728a3.154 3.154 0 0 0 0 5.464l4.732 2.732a21.854 21.854 0 0 0 21.858 21.855h131.13a21.854 21.854 0 0 0 21.855-21.855v-87.42l-76.514-4.782a11.632 11.632 0 0 1 0-23.219",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 43",d:"M408.255 618.882h32.782v-43.71h-32.782Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 44",d:"M462.893 591.563a5.438 5.438 0 0 0-.7.07c-.042-.164-.081-.329-.127-.493a5.457 5.457 0 1 0-5.4-9.372q-.181-.185-.366-.367a5.454 5.454 0 1 0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467 5.467 0 1 0-10.788 0c-.162.042-.325.08-.486.126a5.457 5.457 0 1 0-9.384 5.4 21.843 21.843 0 1 0 36.421 21.02 5.452 5.452 0 1 0 .7-10.858",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 45",d:"M419.183 553.317h32.782v-21.855h-32.782Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 46",d:"M462.893 545.121a2.732 2.732 0 1 0 0-5.464 2.811 2.811 0 0 0-.349.035c-.022-.082-.04-.164-.063-.246a2.733 2.733 0 0 0-1.052-5.253 2.7 2.7 0 0 0-1.648.566q-.09-.093-.184-.184a2.7 2.7 0 0 0 .553-1.633 2.732 2.732 0 0 0-5.245-1.07 10.928 10.928 0 1 0 0 21.031 2.732 2.732 0 0 0 5.245-1.07 2.7 2.7 0 0 0-.553-1.633q.093-.09.184-.184a2.7 2.7 0 0 0 1.648.566 2.732 2.732 0 0 0 1.052-5.253c.023-.081.042-.164.063-.246a2.814 2.814 0 0 0 .349.035",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 47",d:"M320.836 479.556a2.732 2.732 0 0 1-2.732-2.732 8.2 8.2 0 0 0-16.391 0 2.732 2.732 0 0 1-5.464 0 13.66 13.66 0 0 1 27.319 0 2.732 2.732 0 0 1-2.732 2.732",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 48",d:"M364.546 618.881h65.565a21.854 21.854 0 0 0 21.855-21.855v-76.492h-65.565a21.854 21.854 0 0 0-21.855 21.855Z",fill:"#ffff50",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 49",d:"M435.596 554.41h-54.681a1.093 1.093 0 1 1 0-2.185h54.681a1.093 1.093 0 0 1 0 2.185m0 21.855h-54.681a1.093 1.093 0 1 1 0-2.186h54.681a1.093 1.093 0 0 1 0 2.186m0 21.855h-54.681a1.093 1.093 0 1 1 0-2.185h54.681a1.093 1.093 0 0 1 0 2.185m0-54.434h-54.681a1.093 1.093 0 1 1 0-2.185h54.681a1.093 1.093 0 0 1 0 2.185m0 21.652h-54.681a1.093 1.093 0 1 1 0-2.186h54.681a1.093 1.093 0 0 1 0 2.186m0 21.855h-54.681a1.093 1.093 0 1 1 0-2.186h54.681a1.093 1.093 0 0 1 0 2.186m16.369-100.959c-.013 0-.024-.007-.037-.005-3.377.115-4.974 3.492-6.384 6.472-1.471 3.114-2.608 5.139-4.473 5.078-2.064-.074-3.244-2.406-4.494-4.874-1.436-2.835-3.075-6.049-6.516-5.929-3.329.114-4.932 3.053-6.346 5.646-1.5 2.762-2.529 4.442-4.5 4.364-2.106-.076-3.225-1.972-4.52-4.167-1.444-2.443-3.112-5.191-6.487-5.1-3.272.113-4.879 2.606-6.3 4.808-1.5 2.328-2.552 3.746-4.551 3.662-2.156-.076-3.27-1.65-4.558-3.472-1.447-2.047-3.077-4.363-6.442-4.251-3.2.109-4.807 2.153-6.224 3.954-1.346 1.709-2.4 3.062-4.621 2.977a1.094 1.094 0 0 0-.079 2.186c3.3.11 4.967-1.967 6.417-3.81 1.286-1.635 2.4-3.045 4.582-3.12 2.1-.09 3.091 1.218 4.584 3.327 1.417 2 3.026 4.277 6.263 4.394 3.391.114 5.022-2.42 6.467-4.663 1.292-2 2.406-3.734 4.535-3.807 1.959-.073 3.026 1.475 4.529 4.022 1.417 2.4 3.023 5.121 6.324 5.241 3.415.118 5.064-2.863 6.5-5.5 1.245-2.282 2.419-4.437 4.5-4.509 1.959-.046 2.981 1.743 4.492 4.732 1.412 2.79 3.013 5.95 6.365 6.071h.185c3.348 0 4.937-3.36 6.343-6.331 1.245-2.634 2.423-5.114 4.444-5.216Z",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 50",d:"M342.691 618.882h43.71v-43.71h-43.71Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("g",{"data-name":"Group 8",transform:"rotate(-14.98 2188.845 -1120.376)"},r.createElement("rect",{"data-name":"Rectangle 3",width:92.361,height:36.462,rx:2,fill:"#d8d8d8"}),r.createElement("g",{"data-name":"Group 2",transform:"translate(1.531 23.03)",fill:"#4a4a4a"},r.createElement("rect",{"data-name":"Rectangle 4",width:5.336,height:5.336,rx:1,transform:"translate(16.797)"}),r.createElement("rect",{"data-name":"Rectangle 5",width:5.336,height:5.336,rx:1,transform:"translate(23.12)"}),r.createElement("rect",{"data-name":"Rectangle 6",width:5.336,height:5.336,rx:1,transform:"translate(29.444)"}),r.createElement("rect",{"data-name":"Rectangle 7",width:5.336,height:5.336,rx:1,transform:"translate(35.768)"}),r.createElement("rect",{"data-name":"Rectangle 8",width:5.336,height:5.336,rx:1,transform:"translate(42.091)"}),r.createElement("rect",{"data-name":"Rectangle 9",width:5.336,height:5.336,rx:1,transform:"translate(48.415)"}),r.createElement("rect",{"data-name":"Rectangle 10",width:5.336,height:5.336,rx:1,transform:"translate(54.739)"}),r.createElement("rect",{"data-name":"Rectangle 11",width:5.336,height:5.336,rx:1,transform:"translate(61.063)"}),r.createElement("rect",{"data-name":"Rectangle 12",width:5.336,height:5.336,rx:1,transform:"translate(67.386)"}),r.createElement("path",{"data-name":"Path 51",d:"M1.093 0h13.425a1.093 1.093 0 0 1 1.093 1.093v3.15a1.093 1.093 0 0 1-1.093 1.093H1.093A1.093 1.093 0 0 1 0 4.243v-3.15A1.093 1.093 0 0 1 1.093 0ZM75 0h13.426a1.093 1.093 0 0 1 1.093 1.093v3.15a1.093 1.093 0 0 1-1.093 1.093H75a1.093 1.093 0 0 1-1.093-1.093v-3.15A1.093 1.093 0 0 1 75 0Z",fillRule:"evenodd"})),r.createElement("g",{"data-name":"Group 3",transform:"translate(1.531 10.261)",fill:"#4a4a4a"},r.createElement("path",{"data-name":"Path 52",d:"M1.093 0h5.125A1.093 1.093 0 0 1 7.31 1.093v3.149a1.093 1.093 0 0 1-1.092 1.093H1.093A1.093 1.093 0 0 1 0 4.242V1.093A1.093 1.093 0 0 1 1.093 0Z",fillRule:"evenodd"}),r.createElement("rect",{"data-name":"Rectangle 13",width:5.336,height:5.336,rx:1,transform:"translate(8.299)"}),r.createElement("rect",{"data-name":"Rectangle 14",width:5.336,height:5.336,rx:1,transform:"translate(14.623)"}),r.createElement("rect",{"data-name":"Rectangle 15",width:5.336,height:5.336,rx:1,transform:"translate(20.947)"}),r.createElement("rect",{"data-name":"Rectangle 16",width:5.336,height:5.336,rx:1,transform:"translate(27.271)"}),r.createElement("rect",{"data-name":"Rectangle 17",width:5.336,height:5.336,rx:1,transform:"translate(33.594)"}),r.createElement("rect",{"data-name":"Rectangle 18",width:5.336,height:5.336,rx:1,transform:"translate(39.918)"}),r.createElement("rect",{"data-name":"Rectangle 19",width:5.336,height:5.336,rx:1,transform:"translate(46.242)"}),r.createElement("rect",{"data-name":"Rectangle 20",width:5.336,height:5.336,rx:1,transform:"translate(52.565)"}),r.createElement("rect",{"data-name":"Rectangle 21",width:5.336,height:5.336,rx:1,transform:"translate(58.888)"}),r.createElement("rect",{"data-name":"Rectangle 22",width:5.336,height:5.336,rx:1,transform:"translate(65.212)"}),r.createElement("rect",{"data-name":"Rectangle 23",width:5.336,height:5.336,rx:1,transform:"translate(71.536)"}),r.createElement("rect",{"data-name":"Rectangle 24",width:5.336,height:5.336,rx:1,transform:"translate(77.859)"}),r.createElement("rect",{"data-name":"Rectangle 25",width:5.336,height:5.336,rx:1,transform:"translate(84.183)"})),r.createElement("g",{"data-name":"Group 4",transform:"rotate(180 45.525 4.773)",fill:"#4a4a4a"},r.createElement("path",{"data-name":"Path 53",d:"M1.093 0h5.126a1.093 1.093 0 0 1 1.093 1.093v3.15a1.093 1.093 0 0 1-1.093 1.093H1.093A1.093 1.093 0 0 1 0 4.243v-3.15A1.093 1.093 0 0 1 1.093 0Z",fillRule:"evenodd"}),r.createElement("rect",{"data-name":"Rectangle 26",width:5.336,height:5.336,rx:1,transform:"translate(8.299)"}),r.createElement("rect",{"data-name":"Rectangle 27",width:5.336,height:5.336,rx:1,transform:"translate(14.623)"}),r.createElement("rect",{"data-name":"Rectangle 28",width:5.336,height:5.336,rx:1,transform:"translate(20.947)"}),r.createElement("rect",{"data-name":"Rectangle 29",width:5.336,height:5.336,rx:1,transform:"translate(27.271)"}),r.createElement("rect",{"data-name":"Rectangle 30",width:5.336,height:5.336,rx:1,transform:"translate(33.594)"}),r.createElement("rect",{"data-name":"Rectangle 31",width:5.336,height:5.336,rx:1,transform:"translate(39.918)"}),r.createElement("rect",{"data-name":"Rectangle 32",width:5.336,height:5.336,rx:1,transform:"translate(46.242)"}),r.createElement("rect",{"data-name":"Rectangle 33",width:5.336,height:5.336,rx:1,transform:"translate(52.565)"}),r.createElement("rect",{"data-name":"Rectangle 34",width:5.336,height:5.336,rx:1,transform:"translate(58.889)"}),r.createElement("rect",{"data-name":"Rectangle 35",width:5.336,height:5.336,rx:1,transform:"translate(65.213)"}),r.createElement("rect",{"data-name":"Rectangle 36",width:5.336,height:5.336,rx:1,transform:"translate(71.537)"}),r.createElement("rect",{"data-name":"Rectangle 37",width:5.336,height:5.336,rx:1,transform:"translate(77.86)"}),r.createElement("rect",{"data-name":"Rectangle 38",width:5.336,height:5.336,rx:1,transform:"translate(84.183)"}),r.createElement("rect",{"data-name":"Rectangle 39",width:5.336,height:5.336,rx:1,transform:"translate(8.299)"}),r.createElement("rect",{"data-name":"Rectangle 40",width:5.336,height:5.336,rx:1,transform:"translate(14.623)"}),r.createElement("rect",{"data-name":"Rectangle 41",width:5.336,height:5.336,rx:1,transform:"translate(20.947)"}),r.createElement("rect",{"data-name":"Rectangle 42",width:5.336,height:5.336,rx:1,transform:"translate(27.271)"}),r.createElement("rect",{"data-name":"Rectangle 43",width:5.336,height:5.336,rx:1,transform:"translate(33.594)"}),r.createElement("rect",{"data-name":"Rectangle 44",width:5.336,height:5.336,rx:1,transform:"translate(39.918)"}),r.createElement("rect",{"data-name":"Rectangle 45",width:5.336,height:5.336,rx:1,transform:"translate(46.242)"}),r.createElement("rect",{"data-name":"Rectangle 46",width:5.336,height:5.336,rx:1,transform:"translate(52.565)"}),r.createElement("rect",{"data-name":"Rectangle 47",width:5.336,height:5.336,rx:1,transform:"translate(58.889)"}),r.createElement("rect",{"data-name":"Rectangle 48",width:5.336,height:5.336,rx:1,transform:"translate(65.213)"}),r.createElement("rect",{"data-name":"Rectangle 49",width:5.336,height:5.336,rx:1,transform:"translate(71.537)"}),r.createElement("rect",{"data-name":"Rectangle 50",width:5.336,height:5.336,rx:1,transform:"translate(77.86)"}),r.createElement("rect",{"data-name":"Rectangle 51",width:5.336,height:5.336,rx:1,transform:"translate(84.183)"})),r.createElement("g",{"data-name":"Group 6",fill:"#4a4a4a"},r.createElement("path",{"data-name":"Path 54",d:"M2.624 16.584h7.3a1.093 1.093 0 0 1 1.092 1.093v3.15a1.093 1.093 0 0 1-1.093 1.093h-7.3a1.093 1.093 0 0 1-1.092-1.093v-3.149a1.093 1.093 0 0 1 1.093-1.094Z",fillRule:"evenodd"}),r.createElement("g",{"data-name":"Group 5",transform:"translate(12.202 16.584)"},r.createElement("rect",{"data-name":"Rectangle 52",width:5.336,height:5.336,rx:1}),r.createElement("rect",{"data-name":"Rectangle 53",width:5.336,height:5.336,rx:1,transform:"translate(6.324)"}),r.createElement("rect",{"data-name":"Rectangle 54",width:5.336,height:5.336,rx:1,transform:"translate(12.647)"}),r.createElement("rect",{"data-name":"Rectangle 55",width:5.336,height:5.336,rx:1,transform:"translate(18.971)"}),r.createElement("rect",{"data-name":"Rectangle 56",width:5.336,height:5.336,rx:1,transform:"translate(25.295)"}),r.createElement("rect",{"data-name":"Rectangle 57",width:5.336,height:5.336,rx:1,transform:"translate(31.619)"}),r.createElement("rect",{"data-name":"Rectangle 58",width:5.336,height:5.336,rx:1,transform:"translate(37.942)"}),r.createElement("rect",{"data-name":"Rectangle 59",width:5.336,height:5.336,rx:1,transform:"translate(44.265)"}),r.createElement("rect",{"data-name":"Rectangle 60",width:5.336,height:5.336,rx:1,transform:"translate(50.589)"}),r.createElement("rect",{"data-name":"Rectangle 61",width:5.336,height:5.336,rx:1,transform:"translate(56.912)"}),r.createElement("rect",{"data-name":"Rectangle 62",width:5.336,height:5.336,rx:1,transform:"translate(63.236)"})),r.createElement("path",{"data-name":"Path 55",d:"M83.053 16.584h6.906a1.093 1.093 0 0 1 1.091 1.093v3.15a1.093 1.093 0 0 1-1.091 1.093h-6.907a1.093 1.093 0 0 1-1.093-1.093v-3.149a1.093 1.093 0 0 1 1.093-1.094Z",fillRule:"evenodd"})),r.createElement("g",{"data-name":"Group 7",transform:"translate(1.531 29.627)",fill:"#4a4a4a"},r.createElement("rect",{"data-name":"Rectangle 63",width:5.336,height:5.336,rx:1}),r.createElement("rect",{"data-name":"Rectangle 64",width:5.336,height:5.336,rx:1,transform:"translate(6.324)"}),r.createElement("rect",{"data-name":"Rectangle 65",width:5.336,height:5.336,rx:1,transform:"translate(12.647)"}),r.createElement("rect",{"data-name":"Rectangle 66",width:5.336,height:5.336,rx:1,transform:"translate(18.971)"}),r.createElement("path",{"data-name":"Path 56",d:"M26.387 0h30.422a1.093 1.093 0 0 1 1.093 1.093v3.151a1.093 1.093 0 0 1-1.093 1.093H26.387a1.093 1.093 0 0 1-1.093-1.093V1.093A1.093 1.093 0 0 1 26.387 0Zm33.594 0h3.942a1.093 1.093 0 0 1 1.093 1.093v3.151a1.093 1.093 0 0 1-1.093 1.093h-3.942a1.093 1.093 0 0 1-1.093-1.093V1.093A1.093 1.093 0 0 1 59.981 0Z",fillRule:"evenodd"}),r.createElement("rect",{"data-name":"Rectangle 67",width:5.336,height:5.336,rx:1,transform:"translate(66.003)"}),r.createElement("rect",{"data-name":"Rectangle 68",width:5.336,height:5.336,rx:1,transform:"translate(72.327)"}),r.createElement("rect",{"data-name":"Rectangle 69",width:5.336,height:5.336,rx:1,transform:"translate(84.183)"}),r.createElement("path",{"data-name":"Path 57",d:"M78.254 2.273v-1.18A1.093 1.093 0 0 1 79.347 0h3.15a1.093 1.093 0 0 1 1.093 1.093v1.18Z"}),r.createElement("path",{"data-name":"Path 58",d:"M83.591 3.063v1.18a1.093 1.093 0 0 1-1.093 1.093h-3.15a1.093 1.093 0 0 1-1.093-1.093v-1.18Z"})),r.createElement("rect",{"data-name":"Rectangle 70",width:88.927,height:2.371,rx:1.085,transform:"translate(1.925 1.17)",fill:"#4a4a4a"}),r.createElement("rect",{"data-name":"Rectangle 71",width:4.986,height:1.581,rx:.723,transform:"translate(4.1 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 72",width:4.986,height:1.581,rx:.723,transform:"translate(10.923 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 73",width:4.986,height:1.581,rx:.723,transform:"translate(16.173 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 74",width:4.986,height:1.581,rx:.723,transform:"translate(21.421 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 75",width:4.986,height:1.581,rx:.723,transform:"translate(26.671 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 76",width:4.986,height:1.581,rx:.723,transform:"translate(33.232 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 77",width:4.986,height:1.581,rx:.723,transform:"translate(38.48 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 78",width:4.986,height:1.581,rx:.723,transform:"translate(43.73 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 79",width:4.986,height:1.581,rx:.723,transform:"translate(48.978 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 80",width:4.986,height:1.581,rx:.723,transform:"translate(55.54 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 81",width:4.986,height:1.581,rx:.723,transform:"translate(60.788 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 82",width:4.986,height:1.581,rx:.723,transform:"translate(66.038 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 83",width:4.986,height:1.581,rx:.723,transform:"translate(72.599 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 84",width:4.986,height:1.581,rx:.723,transform:"translate(77.847 1.566)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 85",width:4.986,height:1.581,rx:.723,transform:"translate(83.097 1.566)",fill:"#d8d8d8",opacity:.136})),r.createElement("path",{"data-name":"Path 59",d:"M408.256 591.563a5.439 5.439 0 0 0-.7.07c-.042-.164-.081-.329-.127-.493a5.457 5.457 0 1 0-5.4-9.372q-.181-.185-.366-.367a5.454 5.454 0 1 0-9.384-5.4c-.162-.046-.325-.084-.486-.126a5.467 5.467 0 1 0-10.788 0c-.162.042-.325.08-.486.126a5.457 5.457 0 1 0-9.384 5.4 21.843 21.843 0 1 0 36.421 21.02 5.452 5.452 0 1 0 .7-10.858",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 60",d:"M342.691 553.317h43.71v-21.855h-43.71Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 61",d:"M397.328 545.121a2.732 2.732 0 1 0 0-5.464 2.811 2.811 0 0 0-.349.035c-.022-.082-.04-.164-.063-.246a2.733 2.733 0 0 0-1.052-5.253 2.7 2.7 0 0 0-1.648.566q-.09-.093-.184-.184a2.7 2.7 0 0 0 .553-1.633 2.732 2.732 0 0 0-5.245-1.07 10.928 10.928 0 1 0 0 21.031 2.732 2.732 0 0 0 5.245-1.07 2.7 2.7 0 0 0-.553-1.633q.093-.09.184-.184a2.7 2.7 0 0 0 1.648.566 2.732 2.732 0 0 0 1.052-5.253c.023-.081.042-.164.063-.246a2.811 2.811 0 0 0 .349.035",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 62",d:"M408.256 464.531a2.967 2.967 0 0 1-.535-.055 2.754 2.754 0 0 1-.514-.153 2.838 2.838 0 0 1-.471-.251 4.139 4.139 0 0 1-.415-.339 3.2 3.2 0 0 1-.338-.415 2.7 2.7 0 0 1-.459-1.517 2.968 2.968 0 0 1 .055-.535 3.152 3.152 0 0 1 .152-.514 2.874 2.874 0 0 1 .252-.47 2.633 2.633 0 0 1 .753-.754 2.837 2.837 0 0 1 .471-.251 2.753 2.753 0 0 1 .514-.153 2.527 2.527 0 0 1 1.071 0 2.654 2.654 0 0 1 .983.4 4.139 4.139 0 0 1 .415.339 4.019 4.019 0 0 1 .339.415 2.786 2.786 0 0 1 .251.47 2.864 2.864 0 0 1 .208 1.049 2.77 2.77 0 0 1-.8 1.934 4.139 4.139 0 0 1-.415.339 2.722 2.722 0 0 1-1.519.459m21.855-1.366a2.789 2.789 0 0 1-1.935-.8 4.162 4.162 0 0 1-.338-.415 2.7 2.7 0 0 1-.459-1.519 2.789 2.789 0 0 1 .8-1.934 4.139 4.139 0 0 1 .415-.339 2.838 2.838 0 0 1 .471-.251 2.752 2.752 0 0 1 .514-.153 2.527 2.527 0 0 1 1.071 0 2.654 2.654 0 0 1 .983.4 4.139 4.139 0 0 1 .415.339 2.79 2.79 0 0 1 .8 1.934 3.069 3.069 0 0 1-.055.535 2.779 2.779 0 0 1-.153.514 3.885 3.885 0 0 1-.251.47 4.02 4.02 0 0 1-.339.415 4.138 4.138 0 0 1-.415.339 2.722 2.722 0 0 1-1.519.459",fillRule:"evenodd"}))))}},8066:(e,a,t)=>{t.d(a,{Z:()=>c});var l,r=t(7294);function n(){return n=Object.assign?Object.assign.bind():function(e){for(var a=1;a<arguments.length;a++){var t=arguments[a];for(var l in t)Object.prototype.hasOwnProperty.call(t,l)&&(e[l]=t[l])}return e},n.apply(this,arguments)}const c=e=>{let{title:a,titleId:t,...c}=e;return r.createElement("svg",n({xmlns:"http://www.w3.org/2000/svg",width:1041.277,height:554.141,viewBox:"0 0 1041.277 554.141","aria-labelledby":t},c),void 0===a?r.createElement("title",{id:t},"Powered by React"):a?r.createElement("title",{id:t},a):null,l||(l=r.createElement("g",{"data-name":"Group 24"},r.createElement("g",{"data-name":"Group 23",transform:"translate(-.011 -.035)"},r.createElement("path",{"data-name":"Path 299",d:"M961.48 438.21q-1.74 3.75-3.47 7.4-2.7 5.67-5.33 11.12c-.78 1.61-1.56 3.19-2.32 4.77-8.6 17.57-16.63 33.11-23.45 45.89a73.21 73.21 0 0 1-63.81 38.7l-151.65 1.65h-1.6l-13 .14-11.12.12-34.1.37h-1.38l-17.36.19h-.53l-107 1.16-95.51 1-11.11.12-69 .75h-.08l-44.75.48h-.48l-141.5 1.53-42.33.46a87.991 87.991 0 0 1-10.79-.54c-1.22-.14-2.44-.3-3.65-.49a87.38 87.38 0 0 1-51.29-27.54c-18.21-20.03-31.46-43.4-40.36-68.76q-1.93-5.49-3.6-11.12c-30.81-104.15 6.75-238.52 74.35-328.44q4.25-5.64 8.64-11l.07-.08c20.79-25.52 44.1-46.84 68.93-62 44-26.91 92.75-34.49 140.7-11.9 40.57 19.12 78.45 28.11 115.17 30.55 3.71.24 7.42.42 11.11.53 84.23 2.65 163.17-27.7 255.87-47.29 3.69-.78 7.39-1.55 11.12-2.28C763 .54 836.36-6.4 923.6 8.19a189.089 189.089 0 0 1 26.76 6.4q5.77 1.86 11.12 4c41.64 16.94 64.35 48.24 74 87.46q1.37 5.46 2.37 11.11c17.11 94.34-33 228.16-76.37 321.05Z",fill:"#f2f2f2"}),r.createElement("path",{"data-name":"Path 300",d:"M497.02 445.61a95.21 95.21 0 0 1-1.87 11.12h93.7v-11.12Zm-78.25 62.81 11.11-.09v-27.47c-3.81-.17-7.52-.34-11.11-.52Zm-232.92-62.81v11.12h198.5v-11.12Zm849.68-339.52h-74V18.6q-5.35-2.17-11.12-4v91.49H696.87V13.67c-3.73.73-7.43 1.5-11.12 2.28v90.14H429.88V63.24c-3.69-.11-7.4-.29-11.11-.53v43.38H162.9v-62c-24.83 15.16-48.14 36.48-68.93 62h-.07v.08q-4.4 5.4-8.64 11h8.64v328.44h-83q1.66 5.63 3.6 11.12h79.39v93.62a87 87 0 0 0 12.2 2.79c1.21.19 2.43.35 3.65.49a87.991 87.991 0 0 0 10.79.54l42.33-.46v-97h255.91v94.21l11.11-.12v-94.07h255.87v91.36l11.12-.12v-91.24h253.49v4.77c.76-1.58 1.54-3.16 2.32-4.77q2.63-5.45 5.33-11.12 1.73-3.64 3.47-7.4v-321h76.42q-1.01-5.69-2.37-11.12ZM162.9 445.61V117.17h255.87v328.44Zm267 0V117.17h255.85v328.44Zm520.48 0H696.87V117.17h253.49Z",opacity:.1}),r.createElement("path",{"data-name":"Path 301",d:"M863.09 533.65v13l-151.92 1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99-175.61 1.63h-.15l-44.65.42-.48.01-198.4 1.82v-15l46.65-28 93.6-.78 2-.01.66-.01 2-.03 44.94-.37 2.01-.01.64-.01 2-.01 14.41-.12.38-.01 35.55-.3h.29l277.4-2.34 6.79-.05h.68l5.18-.05 37.65-.31 2-.03 1.85-.02h.96l11.71-.09 2.32-.03 3.11-.02 9.75-.09 15.47-.13 2-.02 3.48-.02h.65l74.71-.64Z",fill:"#65617d"}),r.createElement("path",{"data-name":"Path 302",d:"M863.09 533.65v13l-151.92 1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99-175.61 1.63h-.15l-44.65.42-.48.01-198.4 1.82v-15l46.65-28 93.6-.78 2-.01.66-.01 2-.03 44.94-.37 2.01-.01.64-.01 2-.01 14.41-.12.38-.01 35.55-.3h.29l277.4-2.34 6.79-.05h.68l5.18-.05 37.65-.31 2-.03 1.85-.02h.96l11.71-.09 2.32-.03 3.11-.02 9.75-.09 15.47-.13 2-.02 3.48-.02h.65l74.71-.64Z",opacity:.2}),r.createElement("path",{"data-name":"Path 303",d:"M296.1 483.66v24.49a6.13 6.13 0 0 1-3.5 5.54 6 6 0 0 1-2.5.6l-34.9.74a6 6 0 0 1-2.7-.57 6.12 6.12 0 0 1-3.57-5.57v-25.23Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 304",d:"M296.1 483.66v24.49a6.13 6.13 0 0 1-3.5 5.54 6 6 0 0 1-2.5.6l-34.9.74a6 6 0 0 1-2.7-.57 6.12 6.12 0 0 1-3.57-5.57v-25.23Z",opacity:.1}),r.createElement("path",{"data-name":"Path 305",d:"M298.1 483.66v24.49a6.13 6.13 0 0 1-3.5 5.54 6 6 0 0 1-2.5.6l-34.9.74a6 6 0 0 1-2.7-.57 6.12 6.12 0 0 1-3.57-5.57v-25.23Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Rectangle 137",fill:"#3f3d56",d:"M680.92 483.65h47.17v31.5h-47.17z"}),r.createElement("path",{"data-name":"Rectangle 138",opacity:.1,d:"M680.92 483.65h47.17v31.5h-47.17z"}),r.createElement("path",{"data-name":"Rectangle 139",fill:"#3f3d56",d:"M678.92 483.65h47.17v31.5h-47.17z"}),r.createElement("path",{"data-name":"Path 306",d:"M298.09 483.65v4.97l-47.17 1.26v-6.23Z",opacity:.1}),r.createElement("path",{"data-name":"Path 307",d:"M381.35 312.36v168.2a4 4 0 0 1-3.85 3.95l-191.65 5.1h-.05a4 4 0 0 1-3.95-3.95v-173.3a4 4 0 0 1 3.95-3.95h191.6a4 4 0 0 1 3.95 3.95Z",fill:"#65617d"}),r.createElement("path",{"data-name":"Path 308",d:"M185.85 308.41v181.2h-.05a4 4 0 0 1-3.95-3.95v-173.3a4 4 0 0 1 3.95-3.95Z",opacity:.1}),r.createElement("path",{"data-name":"Path 309",d:"M194.59 319.15h177.5V467.4l-177.5 4Z",fill:"#39374d"}),r.createElement("path",{"data-name":"Path 310",d:"M726.09 483.65v6.41l-47.17-1.26v-5.15Z",opacity:.1}),r.createElement("path",{"data-name":"Path 311",d:"M788.35 312.36v173.3a4 4 0 0 1-4 3.95l-191.69-5.1a4 4 0 0 1-3.85-3.95v-168.2a4 4 0 0 1 3.95-3.95h191.6a4 4 0 0 1 3.99 3.95Z",fill:"#65617d"}),r.createElement("path",{"data-name":"Path 312",d:"M788.35 312.36v173.3a4 4 0 0 1-4 3.95v-181.2a4 4 0 0 1 4 3.95Z",opacity:.1}),r.createElement("path",{"data-name":"Path 313",d:"M775.59 319.15h-177.5V467.4l177.5 4Z",fill:"#39374d"}),r.createElement("path",{"data-name":"Path 314",d:"M583.85 312.36v168.2a4 4 0 0 1-3.85 3.95l-191.65 5.1a4 4 0 0 1-4-3.95v-173.3a4 4 0 0 1 3.95-3.95h191.6a4 4 0 0 1 3.95 3.95Z",fill:"#65617d"}),r.createElement("path",{"data-name":"Path 315",d:"M397.09 319.15h177.5V467.4l-177.5 4Z",fill:"#4267b2"}),r.createElement("path",{"data-name":"Path 316",d:"M863.09 533.65v13l-151.92 1.4-1.62.03-57.74.53-1.38.02-17.55.15h-.52l-106.98.99-175.61 1.63h-.15l-44.65.42-.48.01-198.4 1.82v-15l202.51-1.33h.48l40.99-.28h.19l283.08-1.87h.29l.17-.01h.47l4.79-.03h1.46l74.49-.5 4.4-.02.98-.01Z",opacity:.1}),r.createElement("circle",{"data-name":"Ellipse 111",cx:51.33,cy:51.33,r:51.33,transform:"translate(435.93 246.82)",fill:"#fbbebe"}),r.createElement("path",{"data-name":"Path 317",d:"M538.6 377.16s-99.5 12-90 0c3.44-4.34 4.39-17.2 4.2-31.85-.06-4.45-.22-9.06-.45-13.65-1.1-22-3.75-43.5-3.75-43.5s87-41 77-8.5c-4 13.13-2.69 31.57.35 48.88.89 5.05 1.92 10 3 14.7a344.66 344.66 0 0 0 9.65 33.92Z",fill:"#fbbebe"}),r.createElement("path",{"data-name":"Path 318",d:"M506.13 373.09c11.51-2.13 23.7-6 34.53-1.54 2.85 1.17 5.47 2.88 8.39 3.86s6.12 1.22 9.16 1.91c10.68 2.42 19.34 10.55 24.9 20s8.44 20.14 11.26 30.72l6.9 25.83c6 22.45 12 45.09 13.39 68.3a2437.506 2437.506 0 0 1-250.84 1.43c5.44-10.34 11-21.31 10.54-33s-7.19-23.22-4.76-34.74c1.55-7.34 6.57-13.39 9.64-20.22 8.75-19.52 1.94-45.79 17.32-60.65 6.92-6.68 17-9.21 26.63-8.89 12.28.41 24.85 4.24 37 6.11 15.56 2.36 30.26 3.76 45.94.88Z",fill:"#ff6584"}),r.createElement("path",{"data-name":"Path 319",d:"m637.03 484.26-.1 1.43v.1l-.17 2.3-1.33 18.51-1.61 22.3-.46 6.28-1 13.44v.17l-107 1-175.59 1.9v.84h-.14v-1.12l.45-14.36.86-28.06.74-23.79.07-2.37a10.53 10.53 0 0 1 11.42-10.17c4.72.4 10.85.89 18.18 1.41l3 .22c42.33 2.94 120.56 6.74 199.5 2 1.66-.09 3.33-.19 5-.31 12.24-.77 24.47-1.76 36.58-3a10.53 10.53 0 0 1 11.6 11.23Z",opacity:.1}),r.createElement("path",{"data-name":"Path 320",d:"M349.74 552.53v-.84l175.62-1.91 107-1h.3v-.17l1-13.44.43-6 1.64-22.61 1.29-17.9v-.44a10.617 10.617 0 0 0-.11-2.47.3.3 0 0 0 0-.1 10.391 10.391 0 0 0-2-4.64 10.54 10.54 0 0 0-9.42-4 937.419 937.419 0 0 1-36.58 3c-1.67.12-3.34.22-5 .31-78.94 4.69-157.17.89-199.5-2l-3-.22c-7.33-.52-13.46-1-18.18-1.41a10.54 10.54 0 0 0-11.24 8.53 11 11 0 0 0-.18 1.64l-.68 22.16-.93 28.07-.44 14.36v1.12Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 321",d:"m637.33 491.27-1.23 15.33-1.83 22.85-.46 5.72-1 12.81-.06.64v.17l-.15 1.48.11-1.48h-.29l-107 1-175.65 1.9v-.28l.49-14.36 1-28.06.64-18.65a6.36 6.36 0 0 1 3.06-5.25 6.25 6.25 0 0 1 3.78-.9c2.1.17 4.68.37 7.69.59 4.89.36 10.92.78 17.94 1.22 13 .82 29.31 1.7 48 2.42 52 2 122.2 2.67 188.88-3.17 3-.26 6.1-.55 9.13-.84a6.26 6.26 0 0 1 3.48.66 5.159 5.159 0 0 1 .86.54 6.14 6.14 0 0 1 2 2.46 3.564 3.564 0 0 1 .25.61 6.279 6.279 0 0 1 .36 2.59Z",opacity:.1}),r.createElement("path",{"data-name":"Path 322",d:"M298.1 504.96v3.19a6.13 6.13 0 0 1-3.5 5.54l-40.1.77a6.12 6.12 0 0 1-3.57-5.57v-3Z",opacity:.1}),r.createElement("path",{"data-name":"Path 323",d:"m298.59 515.57-52.25 1v-8.67l52.25-1Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 324",d:"m298.59 515.57-52.25 1v-8.67l52.25-1Z",opacity:.1}),r.createElement("path",{"data-name":"Path 325",d:"m300.59 515.57-52.25 1v-8.67l52.25-1Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 326",d:"M679.22 506.96v3.19a6.13 6.13 0 0 0 3.5 5.54l40.1.77a6.12 6.12 0 0 0 3.57-5.57v-3Z",opacity:.1}),r.createElement("path",{"data-name":"Path 327",d:"m678.72 517.57 52.25 1v-8.67l-52.25-1Z",opacity:.1}),r.createElement("path",{"data-name":"Path 328",d:"m676.72 517.57 52.25 1v-8.67l-52.25-1Z",fill:"#3f3d56"}),r.createElement("path",{"data-name":"Path 329",d:"M454.79 313.88c.08 7-3.16 13.6-5.91 20.07a163.491 163.491 0 0 0-12.66 74.71c.73 11 2.58 22 .73 32.9s-8.43 21.77-19 24.9c17.53 10.45 41.26 9.35 57.76-2.66 8.79-6.4 15.34-15.33 21.75-24.11a97.86 97.86 0 0 1-13.31 44.75 103.43 103.43 0 0 0 73.51-40.82c4.31-5.81 8.06-12.19 9.72-19.23 3.09-13-1.22-26.51-4.51-39.5a266.055 266.055 0 0 1-6.17-33c-.43-3.56-.78-7.22.1-10.7 1-4.07 3.67-7.51 5.64-11.22 5.6-10.54 5.73-23.3 2.86-34.88s-8.49-22.26-14.06-32.81c-4.46-8.46-9.3-17.31-17.46-22.28-5.1-3.1-11-4.39-16.88-5.64l-25.37-5.43c-5.55-1.19-11.26-2.38-16.87-1.51-9.47 1.48-16.14 8.32-22 15.34-4.59 5.46-15.81 15.71-16.6 22.86-.72 6.59 5.1 17.63 6.09 24.58 1.3 9 2.22 6 7.3 11.52 3.21 3.42 5.28 7.37 5.34 12.16Z",fill:"#3f3d56"})),r.createElement("path",{"data-name":"Path 40",d:"M280.139 370.832h43.635v17.662h-43.635Z",fill:"#fff",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 41",d:"M240.66 428.493a10.377 10.377 0 0 1-8.989-5.195 10.377 10.377 0 0 0 8.988 15.584h10.391v-10.389Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 42",d:"m287.402 373.625 36.373-2.273v-5.195a10.389 10.389 0 0 0-10.39-10.389h-46.75l-1.3-2.249a1.5 1.5 0 0 0-2.6 0l-1.3 2.249-1.3-2.249a1.5 1.5 0 0 0-2.6 0l-1.3 2.249-1.3-2.249a1.5 1.5 0 0 0-2.6 0l-1.3 2.249h-.034l-2.152-2.151a1.5 1.5 0 0 0-2.508.672l-.696 2.653-2.7-.723a1.5 1.5 0 0 0-1.836 1.837l.722 2.7-2.65.71a1.5 1.5 0 0 0-.673 2.509l2.152 2.152v.033l-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.25 1.282-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3-2.249 1.3a1.5 1.5 0 0 0 0 2.6l2.249 1.3a10.389 10.389 0 0 0 10.389 10.34h62.335a10.389 10.389 0 0 0 10.39-10.39v-41.557l-36.373-2.273a5.53 5.53 0 0 1 0-11.038",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 43",d:"M302.996 438.882h15.584v-20.779h-15.584Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 44",d:"M328.97 425.895a2.582 2.582 0 0 0-.332.033c-.02-.078-.038-.156-.06-.234a2.594 2.594 0 1 0-2.567-4.455q-.086-.088-.174-.175a2.593 2.593 0 1 0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6 2.6 0 1 0-5.128 0c-.077.02-.154.038-.231.06a2.594 2.594 0 1 0-4.461 2.569 10.384 10.384 0 1 0 17.314 9.992 2.592 2.592 0 1 0 .332-5.161",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 45",d:"M308.191 407.713h15.584v-10.389h-15.584Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 46",d:"M328.969 403.818a1.3 1.3 0 1 0 0-2.6 1.336 1.336 0 0 0-.166.017l-.03-.117a1.3 1.3 0 0 0-.5-2.5 1.285 1.285 0 0 0-.783.269l-.087-.087a1.285 1.285 0 0 0 .263-.776 1.3 1.3 0 0 0-2.493-.509 5.195 5.195 0 1 0 0 10 1.3 1.3 0 0 0 2.493-.509 1.285 1.285 0 0 0-.263-.776l.087-.087a1.285 1.285 0 0 0 .783.269 1.3 1.3 0 0 0 .5-2.5c.011-.038.02-.078.03-.117a1.337 1.337 0 0 0 .166.017",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 47",d:"M261.439 372.65a1.3 1.3 0 0 1-1.3-1.3 3.9 3.9 0 0 0-7.792 0 1.3 1.3 0 1 1-2.6 0 6.494 6.494 0 0 1 12.987 0 1.3 1.3 0 0 1-1.3 1.3",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 48",d:"M282.217 438.882h31.168a10.389 10.389 0 0 0 10.389-10.389V392.13h-31.168a10.389 10.389 0 0 0-10.389 10.389Z",fill:"#ffff50",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 49",d:"M315.993 408.233h-25.994a.52.52 0 1 1 0-1.039h25.994a.52.52 0 0 1 0 1.039m0 10.389h-25.994a.52.52 0 1 1 0-1.039h25.994a.52.52 0 0 1 0 1.039m0 10.389h-25.994a.52.52 0 1 1 0-1.039h25.994a.52.52 0 0 1 0 1.039m0-25.877h-25.994a.52.52 0 1 1 0-1.039h25.994a.52.52 0 0 1 0 1.039m0 10.293h-25.994a.52.52 0 1 1 0-1.039h25.994a.52.52 0 0 1 0 1.039m0 10.389h-25.994a.52.52 0 1 1 0-1.039h25.994a.52.52 0 0 1 0 1.039m7.782-47.993h-.018c-1.605.055-2.365 1.66-3.035 3.077-.7 1.48-1.24 2.443-2.126 2.414-.981-.035-1.542-1.144-2.137-2.317-.683-1.347-1.462-2.876-3.1-2.819-1.582.054-2.344 1.451-3.017 2.684-.715 1.313-1.2 2.112-2.141 2.075-1-.036-1.533-.938-2.149-1.981-.686-1.162-1.479-2.467-3.084-2.423-1.555.053-2.319 1.239-2.994 2.286-.713 1.106-1.213 1.781-2.164 1.741-1.025-.036-1.554-.784-2.167-1.65-.688-.973-1.463-2.074-3.062-2.021a3.815 3.815 0 0 0-2.959 1.879c-.64.812-1.14 1.456-2.2 1.415a.52.52 0 0 0-.037 1.039 3.588 3.588 0 0 0 3.05-1.811c.611-.777 1.139-1.448 2.178-1.483 1-.043 1.47.579 2.179 1.582.674.953 1.438 2.033 2.977 2.089 1.612.054 2.387-1.151 3.074-2.217.614-.953 1.144-1.775 2.156-1.81.931-.035 1.438.7 2.153 1.912.674 1.141 1.437 2.434 3.006 2.491 1.623.056 2.407-1.361 3.09-2.616.592-1.085 1.15-2.109 2.14-2.143.931-.022 1.417.829 2.135 2.249.671 1.326 1.432 2.828 3.026 2.886h.088c1.592 0 2.347-1.6 3.015-3.01.592-1.252 1.152-2.431 2.113-2.479Z",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 50",d:"M271.828 438.882h20.779v-20.779h-20.779Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("g",{"data-name":"Group 8",transform:"rotate(-14.98 1643.944 -873.93)"},r.createElement("rect",{"data-name":"Rectangle 3",width:43.906,height:17.333,rx:2,fill:"#d8d8d8"}),r.createElement("g",{"data-name":"Group 2",transform:"translate(.728 10.948)",fill:"#4a4a4a"},r.createElement("rect",{"data-name":"Rectangle 4",width:2.537,height:2.537,rx:1,transform:"translate(7.985)"}),r.createElement("rect",{"data-name":"Rectangle 5",width:2.537,height:2.537,rx:1,transform:"translate(10.991)"}),r.createElement("rect",{"data-name":"Rectangle 6",width:2.537,height:2.537,rx:1,transform:"translate(13.997)"}),r.createElement("rect",{"data-name":"Rectangle 7",width:2.537,height:2.537,rx:1,transform:"translate(17.003)"}),r.createElement("rect",{"data-name":"Rectangle 8",width:2.537,height:2.537,rx:1,transform:"translate(20.009)"}),r.createElement("rect",{"data-name":"Rectangle 9",width:2.537,height:2.537,rx:1,transform:"translate(23.015)"}),r.createElement("rect",{"data-name":"Rectangle 10",width:2.537,height:2.537,rx:1,transform:"translate(26.021)"}),r.createElement("rect",{"data-name":"Rectangle 11",width:2.537,height:2.537,rx:1,transform:"translate(29.028)"}),r.createElement("rect",{"data-name":"Rectangle 12",width:2.537,height:2.537,rx:1,transform:"translate(32.034)"}),r.createElement("path",{"data-name":"Path 51",d:"M.519 0H6.9a.519.519 0 0 1 .521.52v1.5a.519.519 0 0 1-.519.519H.519A.519.519 0 0 1 0 2.017V.519A.519.519 0 0 1 .519 0Zm35.134 0h6.383a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519h-6.384a.519.519 0 0 1-.519-.519v-1.5A.519.519 0 0 1 35.652 0Z",fillRule:"evenodd"})),r.createElement("g",{"data-name":"Group 3",transform:"translate(.728 4.878)",fill:"#4a4a4a"},r.createElement("path",{"data-name":"Path 52",d:"M.519 0h2.437a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519H.519A.519.519 0 0 1 0 2.017V.519A.519.519 0 0 1 .519 0Z",fillRule:"evenodd"}),r.createElement("rect",{"data-name":"Rectangle 13",width:2.537,height:2.537,rx:1,transform:"translate(3.945)"}),r.createElement("rect",{"data-name":"Rectangle 14",width:2.537,height:2.537,rx:1,transform:"translate(6.951)"}),r.createElement("rect",{"data-name":"Rectangle 15",width:2.537,height:2.537,rx:1,transform:"translate(9.958)"}),r.createElement("rect",{"data-name":"Rectangle 16",width:2.537,height:2.537,rx:1,transform:"translate(12.964)"}),r.createElement("rect",{"data-name":"Rectangle 17",width:2.537,height:2.537,rx:1,transform:"translate(15.97)"}),r.createElement("rect",{"data-name":"Rectangle 18",width:2.537,height:2.537,rx:1,transform:"translate(18.976)"}),r.createElement("rect",{"data-name":"Rectangle 19",width:2.537,height:2.537,rx:1,transform:"translate(21.982)"}),r.createElement("rect",{"data-name":"Rectangle 20",width:2.537,height:2.537,rx:1,transform:"translate(24.988)"}),r.createElement("rect",{"data-name":"Rectangle 21",width:2.537,height:2.537,rx:1,transform:"translate(27.994)"}),r.createElement("rect",{"data-name":"Rectangle 22",width:2.537,height:2.537,rx:1,transform:"translate(31)"}),r.createElement("rect",{"data-name":"Rectangle 23",width:2.537,height:2.537,rx:1,transform:"translate(34.006)"}),r.createElement("rect",{"data-name":"Rectangle 24",width:2.537,height:2.537,rx:1,transform:"translate(37.012)"}),r.createElement("rect",{"data-name":"Rectangle 25",width:2.537,height:2.537,rx:1,transform:"translate(40.018)"})),r.createElement("g",{"data-name":"Group 4",transform:"rotate(180 21.642 2.269)",fill:"#4a4a4a"},r.createElement("path",{"data-name":"Path 53",d:"M.519 0h2.437a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519H.519A.519.519 0 0 1 0 2.017V.519A.519.519 0 0 1 .519 0Z",fillRule:"evenodd"}),r.createElement("rect",{"data-name":"Rectangle 26",width:2.537,height:2.537,rx:1,transform:"translate(3.945)"}),r.createElement("rect",{"data-name":"Rectangle 27",width:2.537,height:2.537,rx:1,transform:"translate(6.951)"}),r.createElement("rect",{"data-name":"Rectangle 28",width:2.537,height:2.537,rx:1,transform:"translate(9.958)"}),r.createElement("rect",{"data-name":"Rectangle 29",width:2.537,height:2.537,rx:1,transform:"translate(12.964)"}),r.createElement("rect",{"data-name":"Rectangle 30",width:2.537,height:2.537,rx:1,transform:"translate(15.97)"}),r.createElement("rect",{"data-name":"Rectangle 31",width:2.537,height:2.537,rx:1,transform:"translate(18.976)"}),r.createElement("rect",{"data-name":"Rectangle 32",width:2.537,height:2.537,rx:1,transform:"translate(21.982)"}),r.createElement("rect",{"data-name":"Rectangle 33",width:2.537,height:2.537,rx:1,transform:"translate(24.988)"}),r.createElement("rect",{"data-name":"Rectangle 34",width:2.537,height:2.537,rx:1,transform:"translate(27.994)"}),r.createElement("rect",{"data-name":"Rectangle 35",width:2.537,height:2.537,rx:1,transform:"translate(31.001)"}),r.createElement("rect",{"data-name":"Rectangle 36",width:2.537,height:2.537,rx:1,transform:"translate(34.007)"}),r.createElement("rect",{"data-name":"Rectangle 37",width:2.537,height:2.537,rx:1,transform:"translate(37.013)"}),r.createElement("rect",{"data-name":"Rectangle 38",width:2.537,height:2.537,rx:1,transform:"translate(40.018)"}),r.createElement("rect",{"data-name":"Rectangle 39",width:2.537,height:2.537,rx:1,transform:"translate(3.945)"}),r.createElement("rect",{"data-name":"Rectangle 40",width:2.537,height:2.537,rx:1,transform:"translate(6.951)"}),r.createElement("rect",{"data-name":"Rectangle 41",width:2.537,height:2.537,rx:1,transform:"translate(9.958)"}),r.createElement("rect",{"data-name":"Rectangle 42",width:2.537,height:2.537,rx:1,transform:"translate(12.964)"}),r.createElement("rect",{"data-name":"Rectangle 43",width:2.537,height:2.537,rx:1,transform:"translate(15.97)"}),r.createElement("rect",{"data-name":"Rectangle 44",width:2.537,height:2.537,rx:1,transform:"translate(18.976)"}),r.createElement("rect",{"data-name":"Rectangle 45",width:2.537,height:2.537,rx:1,transform:"translate(21.982)"}),r.createElement("rect",{"data-name":"Rectangle 46",width:2.537,height:2.537,rx:1,transform:"translate(24.988)"}),r.createElement("rect",{"data-name":"Rectangle 47",width:2.537,height:2.537,rx:1,transform:"translate(27.994)"}),r.createElement("rect",{"data-name":"Rectangle 48",width:2.537,height:2.537,rx:1,transform:"translate(31.001)"}),r.createElement("rect",{"data-name":"Rectangle 49",width:2.537,height:2.537,rx:1,transform:"translate(34.007)"}),r.createElement("rect",{"data-name":"Rectangle 50",width:2.537,height:2.537,rx:1,transform:"translate(37.013)"}),r.createElement("rect",{"data-name":"Rectangle 51",width:2.537,height:2.537,rx:1,transform:"translate(40.018)"})),r.createElement("g",{"data-name":"Group 6",fill:"#4a4a4a"},r.createElement("path",{"data-name":"Path 54",d:"M1.247 7.883h3.47a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519h-3.47A.519.519 0 0 1 .728 9.9V8.403a.519.519 0 0 1 .519-.52Z",fillRule:"evenodd"}),r.createElement("g",{"data-name":"Group 5",transform:"translate(5.801 7.883)"},r.createElement("rect",{"data-name":"Rectangle 52",width:2.537,height:2.537,rx:1}),r.createElement("rect",{"data-name":"Rectangle 53",width:2.537,height:2.537,rx:1,transform:"translate(3.006)"}),r.createElement("rect",{"data-name":"Rectangle 54",width:2.537,height:2.537,rx:1,transform:"translate(6.012)"}),r.createElement("rect",{"data-name":"Rectangle 55",width:2.537,height:2.537,rx:1,transform:"translate(9.018)"}),r.createElement("rect",{"data-name":"Rectangle 56",width:2.537,height:2.537,rx:1,transform:"translate(12.025)"}),r.createElement("rect",{"data-name":"Rectangle 57",width:2.537,height:2.537,rx:1,transform:"translate(15.031)"}),r.createElement("rect",{"data-name":"Rectangle 58",width:2.537,height:2.537,rx:1,transform:"translate(18.037)"}),r.createElement("rect",{"data-name":"Rectangle 59",width:2.537,height:2.537,rx:1,transform:"translate(21.042)"}),r.createElement("rect",{"data-name":"Rectangle 60",width:2.537,height:2.537,rx:1,transform:"translate(24.049)"}),r.createElement("rect",{"data-name":"Rectangle 61",width:2.537,height:2.537,rx:1,transform:"translate(27.055)"}),r.createElement("rect",{"data-name":"Rectangle 62",width:2.537,height:2.537,rx:1,transform:"translate(30.061)"})),r.createElement("path",{"data-name":"Path 55",d:"M39.482 7.883h3.28a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519h-3.281a.519.519 0 0 1-.519-.521V8.403a.519.519 0 0 1 .519-.52Z",fillRule:"evenodd"})),r.createElement("g",{"data-name":"Group 7",transform:"translate(.728 14.084)",fill:"#4a4a4a"},r.createElement("rect",{"data-name":"Rectangle 63",width:2.537,height:2.537,rx:1}),r.createElement("rect",{"data-name":"Rectangle 64",width:2.537,height:2.537,rx:1,transform:"translate(3.006)"}),r.createElement("rect",{"data-name":"Rectangle 65",width:2.537,height:2.537,rx:1,transform:"translate(6.012)"}),r.createElement("rect",{"data-name":"Rectangle 66",width:2.537,height:2.537,rx:1,transform:"translate(9.018)"}),r.createElement("path",{"data-name":"Path 56",d:"M12.543 0h14.462a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519H12.543a.519.519 0 0 1-.519-.52V.519A.519.519 0 0 1 12.543 0Zm15.97 0h1.874a.519.519 0 0 1 .519.519v1.5a.519.519 0 0 1-.519.519h-1.874a.519.519 0 0 1-.519-.519v-1.5A.519.519 0 0 1 28.513 0Z",fillRule:"evenodd"}),r.createElement("rect",{"data-name":"Rectangle 67",width:2.537,height:2.537,rx:1,transform:"translate(31.376)"}),r.createElement("rect",{"data-name":"Rectangle 68",width:2.537,height:2.537,rx:1,transform:"translate(34.382)"}),r.createElement("rect",{"data-name":"Rectangle 69",width:2.537,height:2.537,rx:1,transform:"translate(40.018)"}),r.createElement("path",{"data-name":"Path 57",d:"M37.199 1.08V.519A.519.519 0 0 1 37.718 0h1.499a.519.519 0 0 1 .519.519v.561Z"}),r.createElement("path",{"data-name":"Path 58",d:"M39.737 1.456v.561a.519.519 0 0 1-.519.519h-1.499a.519.519 0 0 1-.519-.519v-.561Z"})),r.createElement("rect",{"data-name":"Rectangle 70",width:42.273,height:1.127,rx:.564,transform:"translate(.915 .556)",fill:"#4a4a4a"}),r.createElement("rect",{"data-name":"Rectangle 71",width:2.37,height:.752,rx:.376,transform:"translate(1.949 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 72",width:2.37,height:.752,rx:.376,transform:"translate(5.193 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 73",width:2.37,height:.752,rx:.376,transform:"translate(7.688 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 74",width:2.37,height:.752,rx:.376,transform:"translate(10.183 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 75",width:2.37,height:.752,rx:.376,transform:"translate(12.679 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 76",width:2.37,height:.752,rx:.376,transform:"translate(15.797 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 77",width:2.37,height:.752,rx:.376,transform:"translate(18.292 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 78",width:2.37,height:.752,rx:.376,transform:"translate(20.788 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 79",width:2.37,height:.752,rx:.376,transform:"translate(23.283 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 80",width:2.37,height:.752,rx:.376,transform:"translate(26.402 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 81",width:2.37,height:.752,rx:.376,transform:"translate(28.897 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 82",width:2.37,height:.752,rx:.376,transform:"translate(31.393 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 83",width:2.37,height:.752,rx:.376,transform:"translate(34.512 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 84",width:2.37,height:.752,rx:.376,transform:"translate(37.007 .744)",fill:"#d8d8d8",opacity:.136}),r.createElement("rect",{"data-name":"Rectangle 85",width:2.37,height:.752,rx:.376,transform:"translate(39.502 .744)",fill:"#d8d8d8",opacity:.136})),r.createElement("path",{"data-name":"Path 59",d:"M302.996 425.895a2.583 2.583 0 0 0-.332.033c-.02-.078-.038-.156-.06-.234a2.594 2.594 0 1 0-2.567-4.455q-.086-.088-.174-.175a2.593 2.593 0 1 0-4.461-2.569c-.077-.022-.154-.04-.231-.06a2.6 2.6 0 1 0-5.128 0c-.077.02-.154.038-.231.06a2.594 2.594 0 1 0-4.461 2.569 10.384 10.384 0 1 0 17.314 9.992 2.592 2.592 0 1 0 .332-5.161",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 60",d:"M271.828 407.713h20.779v-10.389h-20.779Z",fill:"#3ecc5f",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 61",d:"M297.801 403.818a1.3 1.3 0 1 0 0-2.6 1.338 1.338 0 0 0-.166.017l-.03-.117a1.3 1.3 0 0 0-.5-2.5 1.285 1.285 0 0 0-.783.269l-.087-.087a1.285 1.285 0 0 0 .263-.776 1.3 1.3 0 0 0-2.493-.509 5.195 5.195 0 1 0 0 10 1.3 1.3 0 0 0 2.493-.509 1.285 1.285 0 0 0-.263-.776l.087-.087a1.285 1.285 0 0 0 .783.269 1.3 1.3 0 0 0 .5-2.5c.011-.038.02-.078.03-.117a1.335 1.335 0 0 0 .166.017",fill:"#44d860",fillRule:"evenodd"}),r.createElement("path",{"data-name":"Path 62",d:"M302.997 365.507a1.41 1.41 0 0 1-.255-.026 1.309 1.309 0 0 1-.244-.073 1.349 1.349 0 0 1-.224-.119 1.967 1.967 0 0 1-.2-.161 1.52 1.52 0 0 1-.161-.2 1.282 1.282 0 0 1-.218-.722 1.41 1.41 0 0 1 .026-.255 1.5 1.5 0 0 1 .072-.244 1.364 1.364 0 0 1 .12-.223 1.252 1.252 0 0 1 .358-.358 1.349 1.349 0 0 1 .224-.119 1.309 1.309 0 0 1 .244-.073 1.2 1.2 0 0 1 .509 0 1.262 1.262 0 0 1 .468.192 1.968 1.968 0 0 1 .2.161 1.908 1.908 0 0 1 .161.2 1.322 1.322 0 0 1 .12.223 1.361 1.361 0 0 1 .1.5 1.317 1.317 0 0 1-.379.919 1.968 1.968 0 0 1-.2.161 1.346 1.346 0 0 1-.223.119 1.332 1.332 0 0 1-.5.1m10.389-.649a1.326 1.326 0 0 1-.92-.379 1.979 1.979 0 0 1-.161-.2 1.282 1.282 0 0 1-.218-.722 1.326 1.326 0 0 1 .379-.919 1.967 1.967 0 0 1 .2-.161 1.351 1.351 0 0 1 .224-.119 1.308 1.308 0 0 1 .244-.073 1.2 1.2 0 0 1 .509 0 1.262 1.262 0 0 1 .468.192 1.967 1.967 0 0 1 .2.161 1.326 1.326 0 0 1 .379.919 1.461 1.461 0 0 1-.026.255 1.323 1.323 0 0 1-.073.244 1.847 1.847 0 0 1-.119.223 1.911 1.911 0 0 1-.161.2 1.967 1.967 0 0 1-.2.161 1.294 1.294 0 0 1-.722.218",fillRule:"evenodd"}),r.createElement("g",{transform:"translate(466.3 278.56)",fill:"#61dafb"},r.createElement("path",{"data-name":"Path 330",d:"M263.668 117.179c0-5.827-7.3-11.35-18.487-14.775 2.582-11.4 1.434-20.477-3.622-23.382a7.861 7.861 0 0 0-4.016-1v4a4.152 4.152 0 0 1 2.044.466c2.439 1.4 3.5 6.724 2.672 13.574-.2 1.685-.52 3.461-.914 5.272a86.9 86.9 0 0 0-11.386-1.954 87.469 87.469 0 0 0-7.459-8.965c5.845-5.433 11.332-8.41 15.062-8.41V78c-4.931 0-11.386 3.514-17.913 9.611-6.527-6.061-12.982-9.539-17.913-9.539v4c3.712 0 9.216 2.959 15.062 8.356a84.687 84.687 0 0 0-7.405 8.947 83.732 83.732 0 0 0-11.4 1.972 54.136 54.136 0 0 1-.932-5.2c-.843-6.85.2-12.175 2.618-13.592a3.991 3.991 0 0 1 2.062-.466v-4a8 8 0 0 0-4.052 1c-5.039 2.9-6.168 11.96-3.568 23.328-11.153 3.443-18.415 8.947-18.415 14.757 0 5.828 7.3 11.35 18.487 14.775-2.582 11.4-1.434 20.477 3.622 23.382a7.882 7.882 0 0 0 4.034 1c4.931 0 11.386-3.514 17.913-9.611 6.527 6.061 12.982 9.539 17.913 9.539a8 8 0 0 0 4.052-1c5.039-2.9 6.168-11.96 3.568-23.328 11.111-3.42 18.373-8.943 18.373-14.752Zm-23.346-11.96a80.235 80.235 0 0 1-2.421 7.083 83.185 83.185 0 0 0-2.349-4.3 96.877 96.877 0 0 0-2.582-4.2c2.547.377 5.004.843 7.353 1.417Zm-8.212 19.1c-1.4 2.421-2.833 4.716-4.321 6.85a93.313 93.313 0 0 1-8.1.359c-2.708 0-5.415-.126-8.069-.341q-2.232-3.2-4.339-6.814-2.044-3.523-3.73-7.136a94.058 94.058 0 0 1 3.712-7.154c1.4-2.421 2.833-4.716 4.321-6.85a93.313 93.313 0 0 1 8.1-.359c2.708 0 5.415.126 8.069.341q2.232 3.2 4.339 6.814 2.044 3.523 3.73 7.136a101.198 101.198 0 0 1-3.712 7.15Zm5.792-2.331a76.525 76.525 0 0 1 2.474 7.136 80.22 80.22 0 0 1-7.387 1.434c.879-1.381 1.757-2.8 2.582-4.25a96.22 96.22 0 0 0 2.329-4.324Zm-18.182 19.128a73.921 73.921 0 0 1-4.985-5.738c1.614.072 3.263.126 4.931.126 1.685 0 3.353-.036 4.985-.126a69.993 69.993 0 0 1-4.931 5.738Zm-13.34-10.561c-2.546-.377-5-.843-7.352-1.417a80.235 80.235 0 0 1 2.421-7.083c.735 1.434 1.506 2.869 2.349 4.3s1.702 2.837 2.582 4.2Zm13.25-37.314a73.924 73.924 0 0 1 4.985 5.738 110.567 110.567 0 0 0-4.931-.126c-1.686 0-3.353.036-4.985.126a69.993 69.993 0 0 1 4.931-5.738ZM206.362 103.8a100.567 100.567 0 0 0-4.913 8.55 76.525 76.525 0 0 1-2.474-7.136 90.158 90.158 0 0 1 7.387-1.414Zm-16.227 22.449c-6.348-2.708-10.454-6.258-10.454-9.073s4.106-6.383 10.454-9.073c1.542-.663 3.228-1.255 4.967-1.811a86.122 86.122 0 0 0 4.034 10.92 84.9 84.9 0 0 0-3.981 10.866 53.804 53.804 0 0 1-5.021-1.826Zm9.647 25.623c-2.439-1.4-3.5-6.724-2.672-13.574.2-1.686.52-3.461.914-5.272a86.9 86.9 0 0 0 11.386 1.954 87.465 87.465 0 0 0 7.459 8.965c-5.845 5.433-11.332 8.41-15.062 8.41a4.279 4.279 0 0 1-2.026-.48Zm42.532-13.663c.843 6.85-.2 12.175-2.618 13.592a3.99 3.99 0 0 1-2.062.466c-3.712 0-9.216-2.959-15.062-8.356a84.689 84.689 0 0 0 7.405-8.947 83.731 83.731 0 0 0 11.4-1.972 50.194 50.194 0 0 1 .936 5.22Zm6.9-11.96c-1.542.663-3.228 1.255-4.967 1.811a86.12 86.12 0 0 0-4.034-10.92 84.9 84.9 0 0 0 3.981-10.866 56.777 56.777 0 0 1 5.039 1.829c6.348 2.708 10.454 6.258 10.454 9.073-.017 2.818-4.123 6.386-10.471 9.076Z"}),r.createElement("path",{"data-name":"Path 331",d:"M201.718 78.072Z"}),r.createElement("circle",{"data-name":"Ellipse 112",cx:8.194,cy:8.194,r:8.194,transform:"translate(211.472 108.984)"}),r.createElement("path",{"data-name":"Path 332",d:"M237.525 78.018Z"})))))}},4002:(e,a,t)=>{t.d(a,{Z:()=>D});var l,r,n,c,h,d,m,i,f,s,o,g,E,p,R,v,x,w,Z,M,u,P,y,b,q,V,A,H,j,G,O,_,N,B,C,F,k=t(7294);function z(){return z=Object.assign?Object.assign.bind():function(e){for(var a=1;a<arguments.length;a++){var t=arguments[a];for(var l in t)Object.prototype.hasOwnProperty.call(t,l)&&(e[l]=t[l])}return e},z.apply(this,arguments)}const D=e=>{let{title:a,titleId:t,...D}=e;return k.createElement("svg",z({xmlns:"http://www.w3.org/2000/svg",width:1129,height:663,viewBox:"0 0 1129 663","aria-labelledby":t},D),void 0===a?k.createElement("title",{id:t},"Focus on What Matters"):a?k.createElement("title",{id:t},a):null,l||(l=k.createElement("circle",{cx:321,cy:321,r:321,fill:"#f2f2f2"})),r||(r=k.createElement("ellipse",{cx:559,cy:635.5,rx:514,ry:27.5,fill:"#3f3d56"})),n||(n=k.createElement("ellipse",{cx:558,cy:627,rx:460,ry:22,opacity:.2})),c||(c=k.createElement("path",{fill:"#3f3d56",d:"M131 152.5h840v50H131z"})),h||(h=k.createElement("path",{d:"M131 608.83a21.67 21.67 0 0 0 21.67 21.67h796.66A21.67 21.67 0 0 0 971 608.83V177.5H131ZM949.33 117.5H152.67A21.67 21.67 0 0 0 131 139.17v38.33h840v-38.33a21.67 21.67 0 0 0-21.67-21.67Z",fill:"#3f3d56"})),d||(d=k.createElement("path",{d:"M949.33 117.5H152.67A21.67 21.67 0 0 0 131 139.17v38.33h840v-38.33a21.67 21.67 0 0 0-21.67-21.67Z",opacity:.2})),m||(m=k.createElement("circle",{cx:181,cy:147.5,r:13,fill:"#3f3d56"})),i||(i=k.createElement("circle",{cx:217,cy:147.5,r:13,fill:"#3f3d56"})),f||(f=k.createElement("circle",{cx:253,cy:147.5,r:13,fill:"#3f3d56"})),s||(s=k.createElement("rect",{x:168,y:213.5,width:337,height:386,rx:5.335,fill:"#606060"})),o||(o=k.createElement("rect",{x:603,y:272.5,width:284,height:22,rx:5.476,fill:"#2e8555"})),g||(g=k.createElement("rect",{x:537,y:352.5,width:416,height:15,rx:5.476,fill:"#2e8555"})),E||(E=k.createElement("rect",{x:537,y:396.5,width:416,height:15,rx:5.476,fill:"#2e8555"})),p||(p=k.createElement("rect",{x:537,y:440.5,width:416,height:15,rx:5.476,fill:"#2e8555"})),R||(R=k.createElement("rect",{x:537,y:484.5,width:416,height:15,rx:5.476,fill:"#2e8555"})),v||(v=k.createElement("rect",{x:865,y:552.5,width:88,height:26,rx:7.028,fill:"#3ecc5f"})),x||(x=k.createElement("path",{d:"M1053.103 506.116a30.114 30.114 0 0 0 3.983-15.266c0-13.797-8.544-24.98-19.083-24.98s-19.082 11.183-19.082 24.98a30.114 30.114 0 0 0 3.983 15.266 31.248 31.248 0 0 0 0 30.532 31.248 31.248 0 0 0 0 30.532 31.248 31.248 0 0 0 0 30.532 30.114 30.114 0 0 0-3.983 15.266c0 13.797 8.543 24.981 19.082 24.981s19.083-11.184 19.083-24.98a30.114 30.114 0 0 0-3.983-15.267 31.248 31.248 0 0 0 0-30.532 31.248 31.248 0 0 0 0-30.532 31.248 31.248 0 0 0 0-30.532Z",fill:"#3f3d56"})),w||(w=k.createElement("ellipse",{cx:1038.003,cy:460.318,rx:19.083,ry:24.981,fill:"#3f3d56"})),Z||(Z=k.createElement("ellipse",{cx:1038.003,cy:429.786,rx:19.083,ry:24.981,fill:"#3f3d56"})),M||(M=k.createElement("path",{d:"M1109.439 220.845a91.61 91.61 0 0 0 7.106-10.461l-50.14-8.235 54.228.403a91.566 91.566 0 0 0 1.746-72.426l-72.755 37.742 67.097-49.321A91.413 91.413 0 1 0 965.75 220.845a91.458 91.458 0 0 0-10.425 16.67l65.087 33.814-69.4-23.292a91.46 91.46 0 0 0 14.738 85.837 91.406 91.406 0 1 0 143.689 0 91.418 91.418 0 0 0 0-113.03Z",fill:"#3ecc5f",fillRule:"evenodd"})),u||(u=k.createElement("path",{d:"M946.188 277.36a91.013 91.013 0 0 0 19.562 56.514 91.406 91.406 0 1 0 143.689 0c12.25-15.553-163.25-66.774-163.25-56.515Z",opacity:.1})),P||(P=k.createElement("path",{d:"M330.12 342.936h111.474v45.12H330.12Z",fill:"#fff",fillRule:"evenodd"})),y||(y=k.createElement("path",{d:"M229.263 490.241a26.51 26.51 0 0 1-22.963-13.27 26.51 26.51 0 0 0 22.963 39.812h26.541V490.24Z",fill:"#3ecc5f",fillRule:"evenodd"})),b||(b=k.createElement("path",{d:"m348.672 350.07 92.922-5.807v-13.27a26.54 26.54 0 0 0-26.541-26.542H295.616l-3.318-5.746a3.83 3.83 0 0 0-6.635 0l-3.318 5.746-3.317-5.746a3.83 3.83 0 0 0-6.636 0l-3.317 5.746-3.318-5.746a3.83 3.83 0 0 0-6.635 0l-3.318 5.746c-.03 0-.056.004-.086.004l-5.497-5.495a3.83 3.83 0 0 0-6.407 1.717l-1.817 6.773-6.89-1.847a3.83 3.83 0 0 0-4.691 4.693l1.844 6.891-6.77 1.814a3.832 3.832 0 0 0-1.72 6.41l5.497 5.497c0 .028-.004.055-.004.085l-5.747 3.317a3.83 3.83 0 0 0 0 6.636l5.747 3.317-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318-5.747 3.317a3.83 3.83 0 0 0 0 6.636l5.747 3.317-5.747 3.318a3.83 3.83 0 0 0 0 6.636l5.747 3.317-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318-5.747 3.317a3.83 3.83 0 0 0 0 6.636l5.747 3.317-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318-5.747 3.317a3.83 3.83 0 0 0 0 6.636l5.747 3.317-5.747 3.318a3.83 3.83 0 0 0 0 6.635l5.747 3.318a26.54 26.54 0 0 0 26.541 26.542h159.249a26.54 26.54 0 0 0 26.541-26.542V384.075l-92.922-5.807a14.126 14.126 0 0 1 0-28.197",fill:"#3ecc5f",fillRule:"evenodd"})),q||(q=k.createElement("path",{d:"M388.511 516.783h39.812V463.7h-39.812Z",fill:"#3ecc5f",fillRule:"evenodd"})),V||(V=k.createElement("path",{d:"M454.865 483.606a6.602 6.602 0 0 0-.848.085c-.05-.2-.099-.4-.154-.599a6.627 6.627 0 1 0-6.557-11.382q-.22-.225-.445-.446a6.624 6.624 0 1 0-11.397-6.564c-.196-.055-.394-.102-.59-.152a6.64 6.64 0 1 0-13.101 0c-.197.05-.394.097-.59.152a6.628 6.628 0 1 0-11.398 6.564 26.528 26.528 0 1 0 44.232 25.528 6.621 6.621 0 1 0 .848-13.186",fill:"#44d860",fillRule:"evenodd"})),A||(A=k.createElement("path",{d:"M401.782 437.158h39.812v-26.541h-39.812Z",fill:"#3ecc5f",fillRule:"evenodd"})),H||(H=k.createElement("path",{d:"M454.865 427.205a3.318 3.318 0 0 0 0-6.635 3.411 3.411 0 0 0-.424.042c-.026-.1-.049-.199-.077-.298a3.319 3.319 0 0 0-1.278-6.38 3.282 3.282 0 0 0-2 .688q-.11-.113-.224-.223a3.282 3.282 0 0 0 .672-1.983 3.318 3.318 0 0 0-6.37-1.299 13.27 13.27 0 1 0 0 25.541 3.318 3.318 0 0 0 6.37-1.3 3.282 3.282 0 0 0-.672-1.982q.114-.11.223-.223a3.282 3.282 0 0 0 2.001.688 3.318 3.318 0 0 0 1.278-6.38c.028-.098.05-.199.077-.298a3.413 3.413 0 0 0 .424.042",fill:"#44d860",fillRule:"evenodd"})),j||(j=k.createElement("path",{d:"M282.345 347.581a3.318 3.318 0 0 1-3.317-3.318 9.953 9.953 0 1 0-19.906 0 3.318 3.318 0 1 1-6.636 0 16.588 16.588 0 1 1 33.177 0 3.318 3.318 0 0 1-3.318 3.318",fillRule:"evenodd"})),G||(G=k.createElement("path",{d:"M335.428 516.783h79.625a26.54 26.54 0 0 0 26.541-26.542v-92.895H361.97a26.54 26.54 0 0 0-26.542 26.542Z",fill:"#ffff50",fillRule:"evenodd"})),O||(O=k.createElement("path",{d:"M421.714 438.485h-66.406a1.327 1.327 0 0 1 0-2.654h66.406a1.327 1.327 0 0 1 0 2.654m0 26.542h-66.406a1.327 1.327 0 1 1 0-2.654h66.406a1.327 1.327 0 0 1 0 2.654m0 26.541h-66.406a1.327 1.327 0 1 1 0-2.654h66.406a1.327 1.327 0 0 1 0 2.654m0-66.106h-66.406a1.327 1.327 0 0 1 0-2.655h66.406a1.327 1.327 0 0 1 0 2.655m0 26.294h-66.406a1.327 1.327 0 0 1 0-2.654h66.406a1.327 1.327 0 0 1 0 2.654m0 26.542h-66.406a1.327 1.327 0 0 1 0-2.655h66.406a1.327 1.327 0 0 1 0 2.655m19.88-122.607c-.016 0-.03-.008-.045-.007-4.1.14-6.04 4.241-7.753 7.86-1.786 3.783-3.168 6.242-5.432 6.167-2.506-.09-3.94-2.922-5.458-5.918-1.744-3.443-3.734-7.347-7.913-7.201-4.042.138-5.99 3.708-7.706 6.857-1.828 3.355-3.071 5.394-5.47 5.3-2.557-.093-3.916-2.395-5.488-5.06-1.753-2.967-3.78-6.304-7.878-6.19-3.973.137-5.925 3.166-7.648 5.84-1.822 2.826-3.098 4.549-5.527 4.447-2.618-.093-3.97-2.004-5.535-4.216-1.757-2.486-3.737-5.3-7.823-5.163-3.886.133-5.838 2.615-7.56 4.802-1.634 2.075-2.91 3.718-5.611 3.615a1.328 1.328 0 1 0-.096 2.654c4.004.134 6.032-2.389 7.793-4.628 1.562-1.985 2.91-3.698 5.564-3.789 2.556-.108 3.754 1.48 5.567 4.041 1.721 2.434 3.675 5.195 7.606 5.337 4.118.138 6.099-2.94 7.853-5.663 1.569-2.434 2.923-4.535 5.508-4.624 2.38-.088 3.674 1.792 5.5 4.885 1.722 2.916 3.671 6.22 7.68 6.365 4.147.143 6.15-3.477 7.895-6.682 1.511-2.77 2.938-5.388 5.466-5.475 2.38-.056 3.62 2.116 5.456 5.746 1.714 3.388 3.658 7.226 7.73 7.373l.224.004c4.066 0 5.996-4.08 7.704-7.689 1.511-3.198 2.942-6.21 5.397-6.334Z",fillRule:"evenodd"})),_||(_=k.createElement("path",{d:"M308.887 516.783h53.083V463.7h-53.083Z",fill:"#3ecc5f",fillRule:"evenodd"})),N||(N=k.createElement("path",{d:"M388.511 483.606a6.602 6.602 0 0 0-.848.085c-.05-.2-.098-.4-.154-.599a6.627 6.627 0 1 0-6.557-11.382q-.22-.225-.444-.446a6.624 6.624 0 1 0-11.397-6.564c-.197-.055-.394-.102-.59-.152a6.64 6.64 0 1 0-13.102 0c-.196.05-.394.097-.59.152a6.628 6.628 0 1 0-11.397 6.564 26.528 26.528 0 1 0 44.231 25.528 6.621 6.621 0 1 0 .848-13.186",fill:"#44d860",fillRule:"evenodd"})),B||(B=k.createElement("path",{d:"M308.887 437.158h53.083v-26.541h-53.083Z",fill:"#3ecc5f",fillRule:"evenodd"})),C||(C=k.createElement("path",{d:"M375.24 427.205a3.318 3.318 0 1 0 0-6.635 3.411 3.411 0 0 0-.423.042c-.026-.1-.05-.199-.077-.298a3.319 3.319 0 0 0-1.278-6.38 3.282 3.282 0 0 0-2.001.688q-.11-.113-.223-.223a3.282 3.282 0 0 0 .671-1.983 3.318 3.318 0 0 0-6.37-1.299 13.27 13.27 0 1 0 0 25.541 3.318 3.318 0 0 0 6.37-1.3 3.282 3.282 0 0 0-.671-1.982q.113-.11.223-.223a3.282 3.282 0 0 0 2.001.688 3.318 3.318 0 0 0 1.278-6.38c.028-.098.05-.199.077-.298a3.413 3.413 0 0 0 .423.042",fill:"#44d860",fillRule:"evenodd"})),F||(F=k.createElement("path",{d:"M388.511 329.334a3.603 3.603 0 0 1-.65-.067 3.344 3.344 0 0 1-.624-.185 3.447 3.447 0 0 1-.572-.306 5.027 5.027 0 0 1-.504-.411 3.887 3.887 0 0 1-.41-.504 3.275 3.275 0 0 1-.558-1.845 3.602 3.602 0 0 1 .067-.65 3.826 3.826 0 0 1 .184-.624 3.489 3.489 0 0 1 .307-.57 3.197 3.197 0 0 1 .914-.916 3.447 3.447 0 0 1 .572-.305 3.344 3.344 0 0 1 .624-.186 3.07 3.07 0 0 1 1.3 0 3.223 3.223 0 0 1 1.195.49 5.028 5.028 0 0 1 .504.412 4.88 4.88 0 0 1 .411.504 3.382 3.382 0 0 1 .306.571 3.478 3.478 0 0 1 .252 1.274 3.364 3.364 0 0 1-.969 2.349 5.027 5.027 0 0 1-.504.411 3.306 3.306 0 0 1-1.845.558m26.542-1.66a3.388 3.388 0 0 1-2.35-.968 5.042 5.042 0 0 1-.41-.504 3.275 3.275 0 0 1-.558-1.845 3.387 3.387 0 0 1 .967-2.349 5.026 5.026 0 0 1 .505-.411 3.447 3.447 0 0 1 .572-.305 3.343 3.343 0 0 1 .623-.186 3.07 3.07 0 0 1 1.3 0 3.224 3.224 0 0 1 1.195.49 5.026 5.026 0 0 1 .504.412 3.388 3.388 0 0 1 .97 2.35 3.726 3.726 0 0 1-.067.65 3.374 3.374 0 0 1-.186.623 4.715 4.715 0 0 1-.305.57 4.88 4.88 0 0 1-.412.505 5.026 5.026 0 0 1-.504.412 3.305 3.305 0 0 1-1.844.557",fillRule:"evenodd"})))}},3261:(e,a,t)=>{t.r(a),t.d(a,{default:()=>o});var l=t(512),r=t(3692),n=t(2263),c=t(6040),h=t(2503);const d={features:"features_t9lD",featureSvg:"featureSvg_GfXr"};var m=t(5893);t(9722).Z,m.Fragment,t(4002).Z,m.Fragment,t(8066).Z,m.Fragment;function i(){return(0,m.jsx)("section",{className:d.features,children:(0,m.jsx)("div",{className:"container"})})}const f={heroBanner:"heroBanner_qdFl",buttons:"buttons_AeoN"};function s(){const{siteConfig:e}=(0,n.Z)();return(0,m.jsx)("header",{className:(0,l.Z)("hero hero--primary",f.heroBanner),children:(0,m.jsxs)("div",{className:"container",children:[(0,m.jsx)(h.Z,{as:"h1",className:"hero__title",children:e.title}),(0,m.jsx)("p",{className:"hero__subtitle",children:e.tagline}),(0,m.jsx)("div",{className:f.buttons,children:(0,m.jsx)(r.Z,{className:"button button--secondary button--lg",to:"/docs/about",children:"Go to Docs"})})]})})}function o(){const{siteConfig:e}=(0,n.Z)();return(0,m.jsxs)(c.Z,{title:`Hello from ${e.title}`,description:"Description will go into a meta tag in <head />",children:[(0,m.jsx)(s,{}),(0,m.jsx)("main",{children:(0,m.jsx)(i,{})})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c7687382.1346df97.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c7687382.1346df97.js new file mode 100644 index 00000000..01df61cc --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c7687382.1346df97.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2547],{2952:(e,t,r)=>{r.r(t),r.d(t,{assets:()=>c,contentTitle:()=>a,default:()=>h,frontMatter:()=>i,metadata:()=>o,toc:()=>l});var n=r(5893),s=r(1151);const i={},a="Appendix B: Certificate Generation",o={id:"appendixb",title:"Appendix B: Certificate Generation",description:"TAK Server includes scripts for generating a private security enclave, which will create a Certificate Authority (CA) as well as server and client certificates.",source:"@site/docs/appendixb.md",sourceDirName:".",slug:"/appendixb",permalink:"/docs/appendixb",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/appendixb.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Appendix A: Acronyms and Abbreviations",permalink:"/docs/appendixa"},next:{title:"Appendix C: Certificate Signing",permalink:"/docs/appendixc"}},c={},l=[{value:"Configure TAK Server Certificate",id:"configure-tak-server-certificate",level:2},{value:"Installing Client Certificates",id:"installing-client-certificates",level:2}];function d(e){const t={code:"code",h1:"h1",h2:"h2",li:"li",ol:"ol",p:"p",pre:"pre",...(0,s.a)(),...e.components};return(0,n.jsxs)(n.Fragment,{children:[(0,n.jsx)(t.h1,{id:"appendix-b-certificate-generation",children:"Appendix B: Certificate Generation"}),"\n",(0,n.jsx)(t.p,{children:"TAK Server includes scripts for generating a private security enclave, which will create a Certificate Authority (CA) as well as server and client certificates."}),"\n",(0,n.jsx)(t.p,{children:"First, figure out how many client certificates you are going to need. Ideally you should have a different client cert for each ATAK device on your network."}),"\n",(0,n.jsx)(t.p,{children:"Become the tak user:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo su tak\n"})}),"\n",(0,n.jsx)(t.p,{children:"Edit the certificate-generation configuration file, at this location:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"/opt/tak/certs/cert-metadata.sh\n"})}),"\n",(0,n.jsx)(t.p,{children:"Set options for country, state, city, organization, and organizational_unit."}),"\n",(0,n.jsx)(t.p,{children:"Change directory:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"cd /opt/tak/certs\n"})}),"\n",(0,n.jsx)(t.p,{children:"Create a certificate authority (CA):"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"./makeRootCa.sh\n"})}),"\n",(0,n.jsx)(t.p,{children:"Follow the prompt to name the CA."}),"\n",(0,n.jsx)(t.p,{children:"Create a server certificate:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"./makeCert.sh server takserver\n"})}),"\n",(0,n.jsx)(t.p,{children:"This command will result in a server certificate named '/opt/tak/certs/files/takserver.jks'"}),"\n",(0,n.jsx)(t.p,{children:"Create one or more client certificates. You should use a different client cert for each ATAK device on your network. This username will be provisioned in the certificate as the CN (Common Name). When using certs on devices that are connected to an input that is configured for group filtering without authentication messages, this username will be used by TAK Server to look up group membership information in an authentication repository, such as Active Directory (AD). This command will create a cert for the username user:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"./makeCert.sh client user\n"})}),"\n",(0,n.jsx)(t.p,{children:"Generate another cert, named admin to access the admin UI:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"./makeCert.sh client admin\n"})}),"\n",(0,n.jsx)(t.p,{children:"The generated CA truststores and certs will be located here:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"/opt/tak/certs/files\n"})}),"\n",(0,n.jsx)(t.p,{children:'Follow the instruction on "Configure TAK Server Certificate" to set up the server to use the generated certs and to authenticate users on a TLS port. If using the default configuration, TLS will be correctly set up on 8443.'}),"\n",(0,n.jsx)(t.p,{children:"Become a normal user:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"exit\n"})}),"\n",(0,n.jsx)(t.p,{children:"Restart the TAK Server:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo systemctl restart takserver\n"})}),"\n",(0,n.jsx)(t.p,{children:"Authorize the admin cert to perform administrative functions using the UI:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:"sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem\n"})}),"\n",(0,n.jsx)(t.h2,{id:"configure-tak-server-certificate",children:"Configure TAK Server Certificate"}),"\n",(0,n.jsx)(t.p,{children:"In /opt/tak, check the following settings in CoreConfig.xml:"}),"\n",(0,n.jsxs)(t.ol,{children:["\n",(0,n.jsx)(t.li,{children:"In the <tls> element, the keystoreFile attribute should be set to the server keystore that was generated with makeCerts.sh, above. If you followed the instructions verbatim, the server keystore is '/opt/tak/certs/files/takserver.jks'."}),"\n",(0,n.jsx)(t.li,{children:"Also in the <tls> element, the truststoreFile attribute should be set to the the trust store that was generated with makeCerts.sh, above. If you used the default arguments, your trust store file is '/opt/tak/certs/files/truststore-root.jks'."}),"\n",(0,n.jsx)(t.li,{children:"In the <network> element, add a TLS input, specifying group-based filtering without requiring an authentication message:"}),"\n"]}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:'<input _name="stdssl" protocol="tls" port="8089" auth="x509"/>\n'})}),"\n",(0,n.jsx)(t.p,{children:"You can change the port number if you want.\nExample:"}),"\n",(0,n.jsx)(t.pre,{children:(0,n.jsx)(t.code,{className:"language-bash",children:'<network multicastTTL="5">\n \x3c!-- <input _name="stdtcp" protocol="tcp" port="8087"/> --\x3e\n \x3c!-- <input _name="stdudp" protocol="udp" port="8087"/> --\x3e\n <input _name="stdssl" protocol="tls" port="8089" auth="x509"/>\n \x3c!-- <input _name="streamtcp" protocol="stcp" port="8088"/> --\x3e\n \x3c!-- <input _name="SAproxy" protocol="mcast" group="239.2.3.1" port="6969" proxy="true"/> --\x3e\n \x3c!-- <input _name="GeoChatproxy" protocol="mcast" group="224.10.10.1" port="17012" proxy="true"/> --\x3e\n \x3c!--<announce enable="true" uid="Marti1" group="239.2.3.1" port="6969" interval="1" ip="192.168.1.137" />--\x3e\n</network>\n...\n<security>\n <tls context="TLSv1" keymanager="SunX509" keystore="JKS" keystoreFile="certs/files/takserver.jks" keystorePass="atakatak" truststore="JKS" truststoreFile="certs/files/truststore-root.jks" truststorePass="atakatak">\n (Uncomment the following if you are using a CRL)\n \x3c!-- <crl _name="Marti CA" crlFile="certs/ca.crl"/> --\x3e\n </tls>\n</security>\n'})}),"\n",(0,n.jsx)(t.p,{children:"Then (re)start the TAK Server as normal."}),"\n",(0,n.jsx)(t.h2,{id:"installing-client-certificates",children:"Installing Client Certificates"}),"\n",(0,n.jsx)(t.p,{children:"Take the truststore-root.p12 and user.p12 files and copy them to your Android device. In ATAK, open 'Settings -> General Settings -> Network Settings' and set the SSL/TLS Truststore and Client Certificate preferences to point to those .p12 files."}),"\n",(0,n.jsx)(t.p,{children:"Repeat the procedure described above for creating a new server connection, but be sure to select SSL as the protocol."}),"\n",(0,n.jsx)(t.p,{children:"These same .p12 files can be installed in a browser, and used to access the Web UI (for admin use) and WebTAK (for normal users or admins). The process to install these files varies by browser and operating system, but can generally be configured by going to the browser preferences, and the security or certificates section."})]})}function h(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,n.jsx)(t,{...e,children:(0,n.jsx)(d,{...e})}):d(e)}},1151:(e,t,r)=>{r.d(t,{Z:()=>o,a:()=>a});var n=r(7294);const s={},i=n.createContext(s);function a(e){const t=n.useContext(i);return n.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function o(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:a(e.components),n.createElement(i.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c92107ec.2b0a69d8.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c92107ec.2b0a69d8.js new file mode 100644 index 00000000..753510e5 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/c92107ec.2b0a69d8.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6129],{4848:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>i,contentTitle:()=>o,default:()=>u,frontMatter:()=>t,metadata:()=>l,toc:()=>d});var r=n(5893),a=n(1151);const t={},o="Install TAK Server",l={id:"installation/twoserver/servertwo/installtakserver",title:"Install TAK Server",description:"Rocky Linux 8",source:"@site/docs/installation/twoserver/servertwo/installtakserver.md",sourceDirName:"installation/twoserver/servertwo",slug:"/installation/twoserver/servertwo/installtakserver",permalink:"/docs/installation/twoserver/servertwo/installtakserver",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/twoserver/servertwo/installtakserver.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Prerequisites",permalink:"/docs/installation/twoserver/servertwo/prerequisites"},next:{title:"Configuration",permalink:"/docs/installation/twoserver/servertwo/configuretakserver"}},i={},d=[{value:"Rocky Linux 8",id:"rocky-linux-8",level:2},{value:"RHEL 8",id:"rhel-8",level:2},{value:"RHEL 7",id:"rhel-7",level:2},{value:"Ubuntu & Raspberry Pi OS",id:"ubuntu--raspberry-pi-os",level:2},{value:"CentOS 7",id:"centos-7",level:2},{value:"yum install From tak.gov",id:"yum-install-from-takgov",level:2}];function c(e){const s={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",p:"p",pre:"pre",strong:"strong",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.h1,{id:"install-tak-server",children:"Install TAK Server"}),"\n",(0,r.jsx)(s.h2,{id:"rocky-linux-8",children:"Rocky Linux 8"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install java-17-openjdk-devel -y\nsudo dnf install takserver-core-5.2-RELEASEx.noarch.rpm -y\nsudo dnf install checkpolicy\ncd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Check Java version"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"rhel-8",children:"RHEL 8"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y\nsudo dnf install takserver-core-5.2-RELEASEx.noarch.rpm -y\nsudo dnf install checkpolicy\ncd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Check Java version"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"rhel-7",children:"RHEL 7"}),"\n",(0,r.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server."}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsxs)(s.strong,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,r.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linuxx64_bin.rpm\nsudo rpm -ivh takserver-core-5.2-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Check Java version"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"ubuntu--raspberry-pi-os",children:"Ubuntu & Raspberry Pi OS"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo apt install takserver-core-5.2-RELEASEx_all.deb\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Check Java version"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"centos-7",children:"CentOS 7"}),"\n",(0,r.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server."}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsxs)(s.strong,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,r.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install epel-release -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linuxx64_bin.rpm\nsudo rpm -ivh takserver-core-5.2-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Check Java version"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:'This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:'}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"yum-install-from-takgov",children:"yum install From tak.gov"}),"\n",(0,r.jsx)(s.p,{children:"Check to see if a .repo file exists for tak.gov"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"ls /etc/yum.repos.d/Takserver.repo\n"})}),"\n",(0,r.jsx)(s.p,{children:"If it exists, skip down to the yum install of takserver database. If it doesn't exist, create a new .repo file to point yum to the yum repo on tak.gov."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"cd /etc/yum.repos.d\nsudo vi Takserver.repo\n"})}),"\n",(0,r.jsx)(s.p,{children:"You can also edit using another editor besides vi. Update the file Takserver.repo to contain:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"[takrepo]\nname=TakserverRepository\nbaseurl=https://<ARTIFACTORY_USER>:<ARTIFACTORY_TOKEN>@artifacts.tak.gov/artifactory/takserver-yum\nenabled=1\ngpgcheck=0\n"})}),"\n",(0,r.jsx)(s.p,{children:'Where <ARTIFACTORY_USER> is a valid login to Artifactory that has access to the takserver-yum repo and <ARTIFACTORY_TOKEN> is a special token unique to the given Artifactory user that can be retrieved by that user using the "Set Me Up" menu option to retrieve it.'}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Note: Do NOT use your password as it the Token is more secure and cannot be used for logging in. Only for retrieving or publishing."})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Also note: When you change the password of the given user, you will also need to retrieve the new token which is based on it and update the baseurl in the Takserver.repo file."})}),"\n",(0,r.jsx)(s.p,{children:"Save the Takserver.repo file and then do the install of the takserver."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install takserver-core-5.2-RELEASEx\n"})})]})}function u(e={}){const{wrapper:s}={...(0,a.a)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},1151:(e,s,n)=>{n.d(s,{Z:()=>l,a:()=>o});var r=n(7294);const a={},t=r.createContext(a);function o(e){const s=r.useContext(t);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:o(e.components),r.createElement(t.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ccc49370.c8f8ad68.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ccc49370.c8f8ad68.js new file mode 100644 index 00000000..0e29977f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ccc49370.c8f8ad68.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[6103],{5203:(e,n,t)=>{t.r(n),t.d(n,{default:()=>p});t(7294);var a=t(512),s=t(1944),i=t(5281),o=t(9460),l=t(1460),r=t(390),c=t(5999),d=t(2244),u=t(5893);function m(e){const{nextItem:n,prevItem:t}=e;return(0,u.jsxs)("nav",{className:"pagination-nav docusaurus-mt-lg","aria-label":(0,c.I)({id:"theme.blog.post.paginator.navAriaLabel",message:"Blog post page navigation",description:"The ARIA label for the blog posts pagination"}),children:[t&&(0,u.jsx)(d.Z,{...t,subLabel:(0,u.jsx)(c.Z,{id:"theme.blog.post.paginator.newerPost",description:"The blog post button label to navigate to the newer/previous post",children:"Newer Post"})}),n&&(0,u.jsx)(d.Z,{...n,subLabel:(0,u.jsx)(c.Z,{id:"theme.blog.post.paginator.olderPost",description:"The blog post button label to navigate to the older/next post",children:"Older Post"}),isNext:!0})]})}function g(){const{assets:e,metadata:n}=(0,o.C)(),{title:t,description:a,date:i,tags:l,authors:r,frontMatter:c}=n,{keywords:d}=c,m=e.image??c.image;return(0,u.jsxs)(s.d,{title:t,description:a,keywords:d,image:m,children:[(0,u.jsx)("meta",{property:"og:type",content:"article"}),(0,u.jsx)("meta",{property:"article:published_time",content:i}),r.some((e=>e.url))&&(0,u.jsx)("meta",{property:"article:author",content:r.map((e=>e.url)).filter(Boolean).join(",")}),l.length>0&&(0,u.jsx)("meta",{property:"article:tag",content:l.map((e=>e.label)).join(",")})]})}var h=t(9407),f=t(2212);function v(e){let{sidebar:n,children:t}=e;const{metadata:a,toc:s}=(0,o.C)(),{nextItem:i,prevItem:c,frontMatter:d,unlisted:g}=a,{hide_table_of_contents:v,toc_min_heading_level:p,toc_max_heading_level:x}=d;return(0,u.jsxs)(l.Z,{sidebar:n,toc:!v&&s.length>0?(0,u.jsx)(h.Z,{toc:s,minHeadingLevel:p,maxHeadingLevel:x}):void 0,children:[g&&(0,u.jsx)(f.Z,{}),(0,u.jsx)(r.Z,{children:t}),(i||c)&&(0,u.jsx)(m,{nextItem:i,prevItem:c})]})}function p(e){const n=e.content;return(0,u.jsx)(o.n,{content:e.content,isBlogPostPage:!0,children:(0,u.jsxs)(s.FG,{className:(0,a.Z)(i.k.wrapper.blogPages,i.k.page.blogPostPage),children:[(0,u.jsx)(g,{}),(0,u.jsx)(v,{sidebar:e.sidebar,children:(0,u.jsx)(n,{})})]})})}},9407:(e,n,t)=>{t.d(n,{Z:()=>c});t(7294);var a=t(512),s=t(3743);const i={tableOfContents:"tableOfContents_bqdL",docItemContainer:"docItemContainer_F8PC"};var o=t(5893);const l="table-of-contents__link toc-highlight",r="table-of-contents__link--active";function c(e){let{className:n,...t}=e;return(0,o.jsx)("div",{className:(0,a.Z)(i.tableOfContents,"thin-scrollbar",n),children:(0,o.jsx)(s.Z,{...t,linkClassName:l,linkActiveClassName:r})})}},3743:(e,n,t)=>{t.d(n,{Z:()=>f});var a=t(7294),s=t(6668);function i(e){const n=e.map((e=>({...e,parentIndex:-1,children:[]}))),t=Array(7).fill(-1);n.forEach(((e,n)=>{const a=t.slice(2,e.level);e.parentIndex=Math.max(...a),t[e.level]=n}));const a=[];return n.forEach((e=>{const{parentIndex:t,...s}=e;t>=0?n[t].children.push(s):a.push(s)})),a}function o(e){let{toc:n,minHeadingLevel:t,maxHeadingLevel:a}=e;return n.flatMap((e=>{const n=o({toc:e.children,minHeadingLevel:t,maxHeadingLevel:a});return function(e){return e.level>=t&&e.level<=a}(e)?[{...e,children:n}]:n}))}function l(e){const n=e.getBoundingClientRect();return n.top===n.bottom?l(e.parentNode):n}function r(e,n){let{anchorTopOffset:t}=n;const a=e.find((e=>l(e).top>=t));if(a){return function(e){return e.top>0&&e.bottom<window.innerHeight/2}(l(a))?a:e[e.indexOf(a)-1]??null}return e[e.length-1]??null}function c(){const e=(0,a.useRef)(0),{navbar:{hideOnScroll:n}}=(0,s.L)();return(0,a.useEffect)((()=>{e.current=n?0:document.querySelector(".navbar").clientHeight}),[n]),e}function d(e){const n=(0,a.useRef)(void 0),t=c();(0,a.useEffect)((()=>{if(!e)return()=>{};const{linkClassName:a,linkActiveClassName:s,minHeadingLevel:i,maxHeadingLevel:o}=e;function l(){const e=function(e){return Array.from(document.getElementsByClassName(e))}(a),l=function(e){let{minHeadingLevel:n,maxHeadingLevel:t}=e;const a=[];for(let s=n;s<=t;s+=1)a.push(`h${s}.anchor`);return Array.from(document.querySelectorAll(a.join()))}({minHeadingLevel:i,maxHeadingLevel:o}),c=r(l,{anchorTopOffset:t.current}),d=e.find((e=>c&&c.id===function(e){return decodeURIComponent(e.href.substring(e.href.indexOf("#")+1))}(e)));e.forEach((e=>{!function(e,t){t?(n.current&&n.current!==e&&n.current.classList.remove(s),e.classList.add(s),n.current=e):e.classList.remove(s)}(e,e===d)}))}return document.addEventListener("scroll",l),document.addEventListener("resize",l),l(),()=>{document.removeEventListener("scroll",l),document.removeEventListener("resize",l)}}),[e,t])}var u=t(3692),m=t(5893);function g(e){let{toc:n,className:t,linkClassName:a,isChild:s}=e;return n.length?(0,m.jsx)("ul",{className:s?void 0:t,children:n.map((e=>(0,m.jsxs)("li",{children:[(0,m.jsx)(u.Z,{to:`#${e.id}`,className:a??void 0,dangerouslySetInnerHTML:{__html:e.value}}),(0,m.jsx)(g,{isChild:!0,toc:e.children,className:t,linkClassName:a})]},e.id)))}):null}const h=a.memo(g);function f(e){let{toc:n,className:t="table-of-contents table-of-contents__left-border",linkClassName:l="table-of-contents__link",linkActiveClassName:r,minHeadingLevel:c,maxHeadingLevel:u,...g}=e;const f=(0,s.L)(),v=c??f.tableOfContents.minHeadingLevel,p=u??f.tableOfContents.maxHeadingLevel,x=function(e){let{toc:n,minHeadingLevel:t,maxHeadingLevel:s}=e;return(0,a.useMemo)((()=>o({toc:i(n),minHeadingLevel:t,maxHeadingLevel:s})),[n,t,s])}({toc:n,minHeadingLevel:v,maxHeadingLevel:p});return d((0,a.useMemo)((()=>{if(l&&r)return{linkClassName:l,linkActiveClassName:r,minHeadingLevel:v,maxHeadingLevel:p}}),[l,r,v,p])),(0,m.jsx)(h,{toc:x,className:t,linkClassName:l,...g})}},2212:(e,n,t)=>{t.d(n,{Z:()=>g});t(7294);var a=t(512),s=t(5999),i=t(5742),o=t(5893);function l(){return(0,o.jsx)(s.Z,{id:"theme.unlistedContent.title",description:"The unlisted content banner title",children:"Unlisted page"})}function r(){return(0,o.jsx)(s.Z,{id:"theme.unlistedContent.message",description:"The unlisted content banner message",children:"This page is unlisted. Search engines will not index it, and only users having a direct link can access it."})}function c(){return(0,o.jsx)(i.Z,{children:(0,o.jsx)("meta",{name:"robots",content:"noindex, nofollow"})})}var d=t(5281),u=t(9047);function m(e){let{className:n}=e;return(0,o.jsx)(u.Z,{type:"caution",title:(0,o.jsx)(l,{}),className:(0,a.Z)(n,d.k.common.unlistedBanner),children:(0,o.jsx)(r,{})})}function g(e){return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(c,{}),(0,o.jsx)(m,{...e})]})}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ce40d3b6.3d1f0f07.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ce40d3b6.3d1f0f07.js new file mode 100644 index 00000000..5e810c27 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ce40d3b6.3d1f0f07.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3511],{7781:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>g,frontMatter:()=>a,metadata:()=>r,toc:()=>u});var i=n(5893),s=n(1151);const a={},o="Group Assignment Using Authentication Messages",r={id:"configuration/groupassignmentusingauth",title:"Group Assignment Using Authentication Messages",description:"If ATAK or another TAK client is configured to send an authentication message after establishing a connection to TAK Server, the username and password credentials contained in that message will be used, in conjunction with an authentication backend, to determine the group membership of a user. TAK Server will then filter messages according to common group membership, in a similar fashion to filtering configured by \\ for a given \\.",source:"@site/docs/configuration/groupassignmentusingauth.md",sourceDirName:"configuration",slug:"/configuration/groupassignmentusingauth",permalink:"/docs/configuration/groupassignmentusingauth",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/groupassignmentusingauth.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Group Assignment by Input",permalink:"/docs/configuration/groupassignmentbyinput"},next:{title:"Group Assignment using Client Certificates",permalink:"/docs/configuration/groupassignmentusingclientcerts"}},c={},u=[];function l(e){const t={code:"code",h1:"h1",p:"p",pre:"pre",...(0,s.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"group-assignment-using-authentication-messages",children:"Group Assignment Using Authentication Messages"}),"\n",(0,i.jsx)(t.p,{children:"If ATAK or another TAK client is configured to send an authentication message after establishing a connection to TAK Server, the username and password credentials contained in that message will be used, in conjunction with an authentication backend, to determine the group membership of a user. TAK Server will then filter messages according to common group membership, in a similar fashion to filtering configured by <filtergroups> for a given <input>."}),"\n",(0,i.jsx)(t.p,{children:"TAK Server can be configured at the input level to expect authentication messages with each new client connection. If the authentication message is not sent, or is invalid, the client will be disconnected. The \u201cauth\u201d attribute on the input indicates which authentication strategy will be used when processing authentication messages. A value of \u201cfile\u201d tells TAK Server to validate authentication credentials using the flat-file backend. A value of \u201cldap\u201d indicates that an Active Directory or LDAP server should be used to validate the credentials."}),"\n",(0,i.jsx)(t.p,{children:"For example, this input definition specifies streaming TCP, encrypted with TLS, authenticating the user with a client certificate and also requiring an authentication message, and using the LDAP authentication backend:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:' <input _name="ldapssl" protocol="tls" port="8091" auth="ldap"/>\n'})})]})}function g(e={}){const{wrapper:t}={...(0,s.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(l,{...e})}):l(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>r,a:()=>o});var i=n(7294);const s={},a=i.createContext(s);function o(e){const t=i.useContext(a);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function r(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(s):e.components||s:o(e.components),i.createElement(a.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/cfff6e91.a587f6fb.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/cfff6e91.a587f6fb.js new file mode 100644 index 00000000..c02913e7 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/cfff6e91.a587f6fb.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9740],{734:(e,n,r)=>{r.r(n),r.d(n,{assets:()=>d,contentTitle:()=>l,default:()=>h,frontMatter:()=>a,metadata:()=>t,toc:()=>o});var s=r(5893),i=r(1151);const a={},l="Overview and Installer Files",t={id:"installation/overview",title:"Overview and Installer Files",description:"TAK Server supports multiple deployment configurations:",source:"@site/docs/installation/overview.md",sourceDirName:"installation",slug:"/installation/overview",permalink:"/docs/installation/overview",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/overview.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"AWS / GovCloud Recommended Instance Type",permalink:"/docs/system-requirements/awsrequirements"},next:{title:"Prerequisites",permalink:"/docs/installation/oneserver/prerequisite"}},d={},o=[{value:"Installer for single-server install",id:"installer-for-single-server-install",level:2},{value:"Database installer for two-server install",id:"database-installer-for-two-server-install",level:2},{value:"Core installer for two-server install",id:"core-installer-for-two-server-install",level:2},{value:"Containerized docker install bundle",id:"containerized-docker-install-bundle",level:2},{value:"Containerized hardeneded docker install bundle",id:"containerized-hardeneded-docker-install-bundle",level:2},{value:"Installer for federation hub (beta)",id:"installer-for-federation-hub-beta",level:2},{value:"Verifying GPG signatures",id:"verifying-gpg-signatures",level:2},{value:"Verifying GPG Signatures on RPM Packages",id:"verifying-gpg-signatures-on-rpm-packages",level:3},{value:"Verifying GPG signatures on DEB packages",id:"verifying-gpg-signatures-on-deb-packages",level:3}];function c(e){const n={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",h3:"h3",li:"li",p:"p",pre:"pre",strong:"strong",ul:"ul",...(0,i.a)(),...e.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsx)(n.h1,{id:"overview-and-installer-files",children:"Overview and Installer Files"}),"\n",(0,s.jsx)(n.p,{children:"TAK Server supports multiple deployment configurations:"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Single server install:"})," One server running TAK Server core (messaging, API, plugins and database): recommended for fewer than 500 users."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Two server install:"})," One server running TAK Server core (messaging, API, plugins and database) and a second server running PostgreSQL database: recommended for more than 500 users."]}),"\n",(0,s.jsxs)(n.li,{children:[(0,s.jsx)(n.strong,{children:"Containerized docker install:"})," One container running TAK Server core (messaging, API, plugins and database) and another container running PostgreSQL database (designed for operating systems other than CentOS 7 / RHEL 7). Hardened containers are published to both tak.gov and IronBank (see ",(0,s.jsx)(n.a,{href:"https://ironbank.dso.mil/repomap?searchText=tak%20server",children:"https://ironbank.dso.mil/repomap?searchText=tak%20server"}),")."]}),"\n"]}),"\n",(0,s.jsx)(n.p,{children:"The following installation files are provided:"}),"\n",(0,s.jsx)(n.h2,{id:"installer-for-single-server-install",children:"Installer for single-server install"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["RHEL/Rocky/CentOS: ",(0,s.jsx)(n.code,{children:"takserver-5.2-RELEASE-x.noarch.rpm"})]}),"\n",(0,s.jsxs)(n.li,{children:["Ubuntu/RaspPi: ",(0,s.jsx)(n.code,{children:"takserver_5.2-RELEASE-x_all.deb"})]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"database-installer-for-two-server-install",children:"Database installer for two-server install"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["RHEL/Rocky/CentOS: ",(0,s.jsx)(n.code,{children:"takserver-database-5.2-RELEASE-x.noarch.rpm"})]}),"\n",(0,s.jsxs)(n.li,{children:["Ubuntu/RaspPi: ",(0,s.jsx)(n.code,{children:"takserver-database_5.2-RELEASE-x_all.deb"})]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"core-installer-for-two-server-install",children:"Core installer for two-server install"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["RHEL/Rocky/CentOS: ",(0,s.jsx)(n.code,{children:"takserver-core-5.2-RELEASE-x.noarch.rpm"})]}),"\n",(0,s.jsxs)(n.li,{children:["Ubuntu/RaspPi: ",(0,s.jsx)(n.code,{children:"takserver-core_5.2-RELEASE-x_all.deb"})]}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"containerized-docker-install-bundle",children:"Containerized docker install bundle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"takserver-docker-5.2-RELEASE-x.zip"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"containerized-hardeneded-docker-install-bundle",children:"Containerized hardeneded docker install bundle"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsx)(n.li,{children:(0,s.jsx)(n.code,{children:"takserver-docker-hardened-5.2-RELEASE-x.zip"})}),"\n"]}),"\n",(0,s.jsx)(n.h2,{id:"installer-for-federation-hub-beta",children:"Installer for federation hub (beta)"}),"\n",(0,s.jsxs)(n.ul,{children:["\n",(0,s.jsxs)(n.li,{children:["RHEL/Rocky/CentOS: ",(0,s.jsx)(n.code,{children:"takserver-fed-hub-5.2-RELEASE-x.noarch.rpm"})]}),"\n",(0,s.jsxs)(n.li,{children:["Ubuntu/RaspPi: ",(0,s.jsx)(n.code,{children:"takserver-fed-hub_5.2-RELEASE-x_all.deb"})]}),"\n"]}),"\n",(0,s.jsxs)(n.p,{children:["Federation hub documentation available here:\n",(0,s.jsx)(n.a,{href:"https://wiki.tak.gov/display/TPC/Federation+Hub",children:"https://wiki.tak.gov/display/TPC/Federation+Hub"})]}),"\n",(0,s.jsx)(n.h2,{id:"verifying-gpg-signatures",children:"Verifying GPG signatures"}),"\n",(0,s.jsx)(n.h3,{id:"verifying-gpg-signatures-on-rpm-packages",children:"Verifying GPG Signatures on RPM Packages"}),"\n",(0,s.jsxs)(n.p,{children:["The GPG public key for TAK Server can be found under\n",(0,s.jsx)(n.a,{href:"https://artifacts.tak.gov/ui/repos/tree/General/TAKServer/release/",children:"https://artifacts.tak.gov/ui/repos/tree/General/TAKServer/release/"})]}),"\n",(0,s.jsxs)(n.p,{children:["Select the TAK Server release version and download the file ",(0,s.jsx)(n.em,{children:"takserver-public-gpg.key"})]}),"\n",(0,s.jsx)(n.p,{children:"Import the key to the RPM key management:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo rpm --import takserver-public-gpg.key\n"})}),"\n",(0,s.jsx)(n.p,{children:"Verifying signature for the rpm installer package:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"rpm --checksig takserver-5.2-RELEASE<version>.noarch.rpm\n"})}),"\n",(0,s.jsx)(n.p,{children:"Example of a successful output:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"takserver-5.2-RELEASE28.noarch.rpm: rsa sha1 (md5) pgp md5 OK\n"})}),"\n",(0,s.jsx)(n.p,{children:"Example of a failed output:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"takserver-5.2-RELEASE28.noarch.rpm: RSA sha1 ((MD5) PGP) md5 NOT OK\n(MISSING KEYS: (MD5) PGP#6851f5b5)\n"})}),"\n",(0,s.jsx)(n.p,{children:"If the RPM packages were not signed with a GPG key, the output might look like:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"takserver-5.2-RELEASE25.noarch.rpm: sha1 md5 OK\n"})}),"\n",(0,s.jsx)(n.h3,{id:"verifying-gpg-signatures-on-deb-packages",children:"Verifying GPG signatures on DEB packages"}),"\n",(0,s.jsxs)(n.p,{children:["Select the appropriate TAK Server release version and download the file ",(0,s.jsx)(n.em,{children:"takserver-public-gpg.key"})," and ",(0,s.jsx)(n.em,{children:"deb_policy.pol"})]}),"\n",(0,s.jsx)(n.p,{children:"Install the debsig-verify utility:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo apt install debsig-verify\n"})}),"\n",(0,s.jsx)(n.p,{children:"Using the ID within the deb_policy.pol file, ex. 039FCDA2D8907527, run the following command to verify signed TAK Server deb resources:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"sudo mkdir /usr/share/debsig/keyrings/039FCDA2D8907527\nsudo mkdir /etc/debsig/policies/039FCDA2D8907527\nsudo touch /usr/share/debsig/keyrings/039FCDA2D8907527/debsig.gpg\nsudo gpg --no-default-keyring --keyring /usr/share/debsig/keyrings/039FCDA2D8907527/debsig.gpg --import takserver-public-gpg.key\nsudo cp deb_policy.pol /etc/debsig/policies/039FCDA2D8907527/debsig.pol\ndebsig-verify -v takserver-5.2-RELEASE_all.deb\n"})}),"\n",(0,s.jsx)(n.p,{children:"Confirm signature verification by identifying statement:"}),"\n",(0,s.jsx)(n.pre,{children:(0,s.jsx)(n.code,{className:"language-bash",children:"debsig: Verified package from 'TAK Product Center' (TAK Server Release)\n"})})]})}function h(e={}){const{wrapper:n}={...(0,i.a)(),...e.components};return n?(0,s.jsx)(n,{...e,children:(0,s.jsx)(c,{...e})}):c(e)}},1151:(e,n,r)=>{r.d(n,{Z:()=>t,a:()=>l});var s=r(7294);const i={},a=s.createContext(i);function l(e){const n=s.useContext(a);return s.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function t(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:l(e.components),s.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/d43d2919.bc9a61f9.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/d43d2919.bc9a61f9.js new file mode 100644 index 00000000..96c0be2c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/d43d2919.bc9a61f9.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[3354],{9475:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>i,contentTitle:()=>o,default:()=>c,frontMatter:()=>a,metadata:()=>l,toc:()=>d});var t=n(5893),r=n(1151);const a={},o="TAK Server Installation",l={id:"installation/oneserver/takserverinstallation",title:"TAK Server Installation",description:"Rocky Linux 8",source:"@site/docs/installation/oneserver/takserverinstallation.md",sourceDirName:"installation/oneserver",slug:"/installation/oneserver/takserverinstallation",permalink:"/docs/installation/oneserver/takserverinstallation",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/installation/oneserver/takserverinstallation.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Prerequisites",permalink:"/docs/installation/oneserver/prerequisite"},next:{title:"Configure TAK Server Installation",permalink:"/docs/installation/oneserver/takserverconfiguration"}},i={},d=[{value:"Rocky Linux 8",id:"rocky-linux-8",level:2},{value:"RHEL 8",id:"rhel-8",level:2},{value:"RHEL 7",id:"rhel-7",level:2},{value:"Ubuntu and Raspberry Pi OS",id:"ubuntu-and-raspberry-pi-os",level:2},{value:"CentOS 7",id:"centos-7",level:2},{value:"yum Install From tak.gov",id:"yum-install-from-takgov",level:2}];function p(e){const s={a:"a",code:"code",em:"em",h1:"h1",h2:"h2",li:"li",p:"p",pre:"pre",ul:"ul",...(0,r.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsx)(s.h1,{id:"tak-server-installation",children:"TAK Server Installation"}),"\n",(0,t.jsx)(s.h2,{id:"rocky-linux-8",children:"Rocky Linux 8"}),"\n",(0,t.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable PowerTools (needed for dependencies of postgis.) Install TAK server. Apply SELinux takserver-policy."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.em,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf install epel-release -y\nsudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm\nsudo dnf -qy module disable postgresql && sudo dnf update -y\nsudo dnf install java-17-openjdk-devel -y\nsudo dnf config-manager --set-enabled powertools\nsudo dnf install takserver-5.2-RELEASEx.noarch.rpm -y\nsudo dnf install checkpolicy\ncd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"rhel-8",children:"RHEL 8"}),"\n",(0,t.jsx)(s.p,{children:"RHEL8 FIPS mode Support. TAK Server has experimental support for RHEL8 FIPS mode. This is intended for evaluation only, for hardened environments. These steps enable TAK Server to operate with RHEL FIPS mode enabled, but does not provide full FIPS 140 compliance. See below for a new option for certs script when using FIPS mode. Client certificates generated with FIPS mode may not work with ATAK."}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo rpm --import http://download.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-8\nsudo rpm --import https://download.postgresql.org/pub/repos/yum/RPM-GPG-KEY-PGDG\nsudo vi /etc/fapolicyd/rules.d/99-whitelist.rules\n"})}),"\n",(0,t.jsx)(s.p,{children:"Add line:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"deny perm=any all : all\n"})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo vi /etc/fapolicyd/rules.d/39-tak.rules\n"})}),"\n",(0,t.jsx)(s.p,{children:"Add lines:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"allow perm=open all : dir=/opt/tak/ ftype=application/x-sharedlib trust=0\nallow perm=open exe=/usr/pgsql-15/bin/postgres : all\n"})}),"\n",(0,t.jsxs)(s.ul,{children:["\n",(0,t.jsx)(s.li,{children:"Custom certificates with stronger algorithms will need to be generated for use on systems with FIPS enabled. To do this: follow the existing certificate instructions, but append --fips to the end of each ./makeRootCa.sh and ./makeCert.sh command. Note: ATAK may not support certificates generated with these stronger algorithms*"}),"\n"]}),"\n",(0,t.jsx)(s.p,{children:"--- End FIPS Mode Commands ---"}),"\n",(0,t.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable Repository Management and repository CodeReady Builder. Install TAK server. Apply SELinux takserver-policy."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.em,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y\nsudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm\nsudo dnf update -y && sudo dnf install java-17-openjdk-devel -y\nsudo dnf module disable postgresql\nsudo subscription-manager config --rhsm.manage_repos=1\nsudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms\n"})}),"\n",(0,t.jsx)(s.p,{children:"Note: If you get the error \u2018This system has no repositories available through subscriptions\u2019, you need to subscribe your system with:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach\n"})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo dnf install takserver-5.2-RELEASEx.noarch.rpm -y\nsudo dnf install checkpolicy\ncd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"rhel-7",children:"RHEL 7"}),"\n",(0,t.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install TAK server"}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.em,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\nsudo rpm -ivh takserver-5.2-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,t.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,t.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"ubuntu-and-raspberry-pi-os",children:"Ubuntu and Raspberry Pi OS"}),"\n",(0,t.jsx)(s.p,{children:"Install the postgres repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install TAK server"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo mkdir -p /etc/apt/keyrings\nsudo curl https://www.postgresql.org/media/keys/ACCC4CF8.asc --output /etc/apt/keyrings/postgresql.asc\nsudo sh -c 'echo \"deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main\" > /etc/apt/sources.list.d/postgresql.list'\nsudo apt update\nsudo apt install ./takserver-5.2-RELEASEx_all.deb\n"})}),"\n",(0,t.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"centos-7",children:"CentOS 7"}),"\n",(0,t.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server."}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsxs)(s.em,{children:["Note that when installing postgres, you may run into issues related to the GPG key \u2013 if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: ",(0,t.jsx)(s.a,{href:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/",children:"https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/"})]})}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo yum install epel-release -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\nsudo rpm -ivh takserver-5.2-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,t.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on CentOS 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,t.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,t.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,t.jsx)(s.h2,{id:"yum-install-from-takgov",children:"yum Install From tak.gov"}),"\n",(0,t.jsx)(s.p,{children:"Check to see if a .repo file exists for tak.gov"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"ls /etc/yum.repos.d/Takserver.repo\n"})}),"\n",(0,t.jsx)(s.p,{children:"If it exists, skip down to the yum install of takserver database. If it doesn't exist, create a new .repo file to point yum to the yum repo on tak.gov."}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"cd /etc/yum.repos.d\nsudo vi Takserver.repo\n"})}),"\n",(0,t.jsx)(s.p,{children:"You can also edit using another editor besides vi. Update the file Takserver.repo to contain:"}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"[takrepo]\nname=TakserverRepository\nbaseurl=https://<ARTIFACTORY_USER>:<ARTIFACTORY_TOKEN>@artifacts.tak.gov/artifactory/takserver-yum\nenabled=1\ngpgcheck=0\n"})}),"\n",(0,t.jsx)(s.p,{children:'Where <ARTIFACTORY_USER> is a valid login to Artifactory that has access to the takserver-yum repo and <ARTIFACTORY_TOKEN> is a special token unique to the given Artifactory user that can be retrieved by that user using the "Set Me Up" menu option to retrieve it.'}),"\n",(0,t.jsx)(s.p,{children:(0,t.jsx)(s.em,{children:"Note: Do NOT use your password as it the Token is more secure and cannot be used for logging in. Only for retrieving or publishing. Also note: When you change the password of the given user, you will also need to retrieve the new token which is based on it and update the baseurl in the Takserver.repo file."})}),"\n",(0,t.jsx)(s.p,{children:"Save the Takserver.repo file and then do the install of the takserver."}),"\n",(0,t.jsx)(s.pre,{children:(0,t.jsx)(s.code,{className:"language-bash",children:"sudo yum install takserver-core-5.2-RELEASEx\n"})})]})}function c(e={}){const{wrapper:s}={...(0,r.a)(),...e.components};return s?(0,t.jsx)(s,{...e,children:(0,t.jsx)(p,{...e})}):p(e)}},1151:(e,s,n)=>{n.d(s,{Z:()=>l,a:()=>o});var t=n(7294);const r={},a=t.createContext(r);function o(e){const s=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:o(e.components),t.createElement(a.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/d9f32620.34b19fce.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/d9f32620.34b19fce.js new file mode 100644 index 00000000..c37daca5 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/d9f32620.34b19fce.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[1914],{8123:(e,o,s)=>{s.r(o),s.d(o,{assets:()=>c,contentTitle:()=>l,default:()=>d,frontMatter:()=>a,metadata:()=>r,toc:()=>u});var t=s(5893),n=s(1151);const a={slug:"welcome",title:"Welcome to Docusaurus!",authors:["slorber","yangshun"],tags:["facebook","hello","docusaurus"]},l=void 0,r={permalink:"/blog/welcome",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2021-08-26-welcome/index.md",source:"@site/blog/2021-08-26-welcome/index.md",title:"Welcome to Docusaurus!",description:"Docusaurus blogging features are powered by the blog plugin.",date:"2021-08-26T00:00:00.000Z",formattedDate:"August 26, 2021",tags:[{label:"facebook",permalink:"/blog/tags/facebook"},{label:"hello",permalink:"/blog/tags/hello"},{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:.405,hasTruncateMarker:!1,authors:[{name:"S\xe9bastien Lorber",title:"Docusaurus maintainer",url:"https://sebastienlorber.com",imageURL:"https://github.com/slorber.png",key:"slorber"},{name:"Yangshun Tay",title:"Front End Engineer @ Facebook",url:"https://github.com/yangshun",imageURL:"https://github.com/yangshun.png",key:"yangshun"}],frontMatter:{slug:"welcome",title:"Welcome to Docusaurus!",authors:["slorber","yangshun"],tags:["facebook","hello","docusaurus"]},unlisted:!1,prevItem:{title:"Welcome to Docusaurus-Static!",permalink:"/blog/2024/01/28/welcome-to-docusaurus-static"},nextItem:{title:"MDX Blog Post",permalink:"/blog/mdx-blog-post"}},c={authorsImageUrls:[void 0,void 0]},u=[];function i(e){const o={a:"a",code:"code",img:"img",li:"li",p:"p",strong:"strong",ul:"ul",...(0,n.a)(),...e.components};return(0,t.jsxs)(t.Fragment,{children:[(0,t.jsxs)(o.p,{children:[(0,t.jsx)(o.a,{href:"https://docusaurus.io/docs/blog",children:"Docusaurus blogging features"})," are powered by the ",(0,t.jsx)(o.a,{href:"https://docusaurus.io/docs/api/plugins/@docusaurus/plugin-content-blog",children:"blog plugin"}),"."]}),"\n",(0,t.jsxs)(o.p,{children:["Simply add Markdown files (or folders) to the ",(0,t.jsx)(o.code,{children:"blog"})," directory."]}),"\n",(0,t.jsxs)(o.p,{children:["Regular blog authors can be added to ",(0,t.jsx)(o.code,{children:"authors.yml"}),"."]}),"\n",(0,t.jsx)(o.p,{children:"The blog post date can be extracted from filenames, such as:"}),"\n",(0,t.jsxs)(o.ul,{children:["\n",(0,t.jsx)(o.li,{children:(0,t.jsx)(o.code,{children:"2019-05-30-welcome.md"})}),"\n",(0,t.jsx)(o.li,{children:(0,t.jsx)(o.code,{children:"2019-05-30-welcome/index.md"})}),"\n"]}),"\n",(0,t.jsx)(o.p,{children:"A blog post folder can be convenient to co-locate blog post images:"}),"\n",(0,t.jsx)(o.p,{children:(0,t.jsx)(o.img,{alt:"Docusaurus Plushie",src:s(6853).Z+"",width:"1500",height:"500"})}),"\n",(0,t.jsx)(o.p,{children:"The blog supports tags as well!"}),"\n",(0,t.jsxs)(o.p,{children:[(0,t.jsx)(o.strong,{children:"And if you don't want a blog"}),": just delete this directory, and use ",(0,t.jsx)(o.code,{children:"blog: false"})," in your Docusaurus config."]})]})}function d(e={}){const{wrapper:o}={...(0,n.a)(),...e.components};return o?(0,t.jsx)(o,{...e,children:(0,t.jsx)(i,{...e})}):i(e)}},6853:(e,o,s)=>{s.d(o,{Z:()=>t});const t=s.p+"assets/images/docusaurus-plushie-banner-a60f7593abca1e3eef26a9afa244e4fb.jpeg"},1151:(e,o,s)=>{s.d(o,{Z:()=>r,a:()=>l});var t=s(7294);const n={},a=t.createContext(n);function l(e){const o=t.useContext(a);return t.useMemo((function(){return"function"==typeof e?e(o):{...o,...e}}),[o,e])}function r(e){let o;return o=e.disableParentContext?"function"==typeof e.components?e.components(n):e.components||n:l(e.components),t.createElement(a.Provider,{value:o},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/da5cb8ba.59bc4eaf.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/da5cb8ba.59bc4eaf.js new file mode 100644 index 00000000..c70a3844 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/da5cb8ba.59bc4eaf.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2200],{9850:(t,e,n)=>{n.r(e),n.d(e,{assets:()=>c,contentTitle:()=>s,default:()=>d,frontMatter:()=>r,metadata:()=>i,toc:()=>l});var o=n(5893),a=n(1151);const r={},s="Software Installation Location",i={id:"softwareinstallationlocation",title:"Software Installation Location",description:"The RPM installer places the TAK server software and configuration in the directory /opt/tak. It creates a user named tak who is the owner of the files in that directory tree. Always use this tak user when editing CoreConfig.xml or generating certificates. You can become the tak user by entering:",source:"@site/docs/softwareinstallationlocation.md",sourceDirName:".",slug:"/softwareinstallationlocation",permalink:"/docs/softwareinstallationlocation",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/softwareinstallationlocation.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Ubuntu and Raspberry Pi",permalink:"/docs/firewall/ubunturaspberrypi"},next:{title:"Overview",permalink:"/docs/configuration/overview"}},c={},l=[];function u(t){const e={code:"code",h1:"h1",p:"p",pre:"pre",strong:"strong",...(0,a.a)(),...t.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(e.h1,{id:"software-installation-location",children:"Software Installation Location"}),"\n",(0,o.jsxs)(e.p,{children:["The RPM installer places the TAK server software and configuration in the directory /opt/tak. It creates a user named tak who is the owner of the files in that directory tree. ",(0,o.jsx)(e.strong,{children:"Always use this tak user when editing CoreConfig.xml or generating certificates. You can become the tak user by entering:"})]}),"\n",(0,o.jsx)(e.pre,{children:(0,o.jsx)(e.code,{className:"language-bash",children:"sudo su tak\n"})})]})}function d(t={}){const{wrapper:e}={...(0,a.a)(),...t.components};return e?(0,o.jsx)(e,{...t,children:(0,o.jsx)(u,{...t})}):u(t)}},1151:(t,e,n)=>{n.d(e,{Z:()=>i,a:()=>s});var o=n(7294);const a={},r=o.createContext(a);function s(t){const e=o.useContext(r);return o.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function i(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(a):t.components||a:s(t.components),o.createElement(r.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e16015ca.bc4e4817.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e16015ca.bc4e4817.js new file mode 100644 index 00000000..b6f55743 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e16015ca.bc4e4817.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[9700],{5688:s=>{s.exports=JSON.parse('{"label":"hola","permalink":"/blog/tags/hola","allTagsPath":"/blog/tags","count":1,"unlisted":false}')}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e273c56f.464b1d37.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e273c56f.464b1d37.js new file mode 100644 index 00000000..86252a59 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e273c56f.464b1d37.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2362],{9954:(t,o,e)=>{e.r(o),e.d(o,{assets:()=>n,contentTitle:()=>u,default:()=>m,frontMatter:()=>i,metadata:()=>r,toc:()=>l});var s=e(5893),a=e(1151);const i={slug:"first-blog-post",title:"First Blog Post",authors:{name:"Gao Wei",title:"Docusaurus Core Team",url:"https://github.com/wgao19",image_url:"https://github.com/wgao19.png"},tags:["hola","docusaurus"]},u=void 0,r={permalink:"/blog/first-blog-post",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2019-05-28-first-blog-post.md",source:"@site/blog/2019-05-28-first-blog-post.md",title:"First Blog Post",description:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet",date:"2019-05-28T00:00:00.000Z",formattedDate:"May 28, 2019",tags:[{label:"hola",permalink:"/blog/tags/hola"},{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:.12,hasTruncateMarker:!1,authors:[{name:"Gao Wei",title:"Docusaurus Core Team",url:"https://github.com/wgao19",image_url:"https://github.com/wgao19.png",imageURL:"https://github.com/wgao19.png"}],frontMatter:{slug:"first-blog-post",title:"First Blog Post",authors:{name:"Gao Wei",title:"Docusaurus Core Team",url:"https://github.com/wgao19",image_url:"https://github.com/wgao19.png",imageURL:"https://github.com/wgao19.png"},tags:["hola","docusaurus"]},unlisted:!1,prevItem:{title:"Long Blog Post",permalink:"/blog/long-blog-post"}},n={authorsImageUrls:[void 0]},l=[];function c(t){const o={p:"p",...(0,a.a)(),...t.components};return(0,s.jsx)(o.p,{children:"Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet"})}function m(t={}){const{wrapper:o}={...(0,a.a)(),...t.components};return o?(0,s.jsx)(o,{...t,children:(0,s.jsx)(c,{...t})}):c(t)}},1151:(t,o,e)=>{e.d(o,{Z:()=>r,a:()=>u});var s=e(7294);const a={},i=s.createContext(a);function u(t){const o=s.useContext(i);return s.useMemo((function(){return"function"==typeof t?t(o):{...o,...t}}),[o,t])}function r(t){let o;return o=t.disableParentContext?"function"==typeof t.components?t.components(a):t.components||a:u(t.components),s.createElement(i.Provider,{value:o},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e3eb95dd.384da0ba.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e3eb95dd.384da0ba.js new file mode 100644 index 00000000..f8cc4010 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e3eb95dd.384da0ba.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2372],{3124:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>s,default:()=>l,frontMatter:()=>o,metadata:()=>a,toc:()=>u});var i=n(5893),r=n(1151);const o={},s="Group Assignment using Client Certificates",a={id:"configuration/groupassignmentusingclientcerts",title:"Group Assignment using Client Certificates",description:"TAK Server can be configured to use only the information contained in a client certificate, when looking up group membership for a user. In this case, the TAK client is configured to use TLS and a client certificate when connecting to TAK server, but does not send an authentication message. This eliminates the requirement to cache credentials on the device, or enter credentials prior to establishment of each new connection. TAK Server will then filter messages according to common group membership, in a similar fashion to input-level filtering with filter groups. When analyzing the X.509 client certificate presented by the TAK client, TAK Server will look at the DN attribute in the certificate, extract the CN value from the DN (if present). The CN is regarded as the username, and is used to look up group membership in authentication backends. For example, consider this DN in a client certificate:",source:"@site/docs/configuration/groupassignmentusingclientcerts.md",sourceDirName:"configuration",slug:"/configuration/groupassignmentusingclientcerts",permalink:"/docs/configuration/groupassignmentusingclientcerts",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/configuration/groupassignmentusingclientcerts.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Group Assignment Using Authentication Messages",permalink:"/docs/configuration/groupassignmentusingauth"},next:{title:"Authentication Backends",permalink:"/docs/configuration/authenticationbackends"}},c={},u=[];function h(e){const t={code:"code",h1:"h1",p:"p",pre:"pre",...(0,r.a)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(t.h1,{id:"group-assignment-using-client-certificates",children:"Group Assignment using Client Certificates"}),"\n",(0,i.jsx)(t.p,{children:"TAK Server can be configured to use only the information contained in a client certificate, when looking up group membership for a user. In this case, the TAK client is configured to use TLS and a client certificate when connecting to TAK server, but does not send an authentication message. This eliminates the requirement to cache credentials on the device, or enter credentials prior to establishment of each new connection. TAK Server will then filter messages according to common group membership, in a similar fashion to input-level filtering with filter groups. When analyzing the X.509 client certificate presented by the TAK client, TAK Server will look at the DN attribute in the certificate, extract the CN value from the DN (if present). The CN is regarded as the username, and is used to look up group membership in authentication backends. For example, consider this DN in a client certificate:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:"CN=jkirk, O=TAK, C=US\n"})}),"\n",(0,i.jsx)(t.p,{children:"The CN value jkirk will be used as the username. The process for deciding which authentication backend to use depends on whether or not an Active Directory (AD) LDAP configuration is present in CoreConfig.xml. Valid service account credentials must be configured in CoreConfig.. If AD authentication is configured, the user account is matched by the sAMAccountName LDAP attribute. At client authentication time, if groups are found in AD for the user, those groups are used by TAK Server. If no groups are found, the flat-file authentication backend is searched for a match on the username. If no groups are found for the user in either repository, the user is assigned to the __ANON__ group."}),"\n",(0,i.jsx)(t.p,{children:"When configuring the input, a TLS input with an auth type of x509 directs TAK Server to use the client certificate for both authentication and group assignment. On the input configuration, the on or example, this input definition specifies streaming TCP encrypted with TLS, authenticating the user with a client certificate and also requiring an authentication message, and using the LDAP authentication backend:"}),"\n",(0,i.jsx)(t.pre,{children:(0,i.jsx)(t.code,{className:"language-bash",children:' <input _name="ldapssl" protocol="tls" port="8091" auth="x509"/>\n'})})]})}function l(e={}){const{wrapper:t}={...(0,r.a)(),...e.components};return t?(0,i.jsx)(t,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}},1151:(e,t,n)=>{n.d(t,{Z:()=>a,a:()=>s});var i=n(7294);const r={},o=i.createContext(r);function s(e){const t=i.useContext(o);return i.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function a(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(r):e.components||r:s(e.components),i.createElement(o.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e54870e4.43760141.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e54870e4.43760141.js new file mode 100644 index 00000000..7e22aee9 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/e54870e4.43760141.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[60],{3213:(e,t,n)=>{n.r(t),n.d(t,{assets:()=>c,contentTitle:()=>o,default:()=>h,frontMatter:()=>r,metadata:()=>s,toc:()=>d});var a=n(5893),i=n(1151);const r={},o="Federation Example",s={id:"federation/federationexample",title:"Federation Example",description:'The figure below shows a connectivity graph of two distinct administrative domains. Each administrative domain has multiple sub-groups (e.g. "CN=Alpha") utilizing the group-filtering. The color coding indicates the CA that is used to sign the certificate used for connections. Enclave 1\'s CA signs ATAK client certs and a server certificate. Enclave 2\'s CA also signs ATAK client certs and a server cert. The trust-store listing the allowed CAs for the "User Port" only contains a single CA (i.e. Enclave 1 CA for Enclave 1). To federate the servers, Enclave 1 and Enclave 2 send each other the "public" CA cert. Those certificates are put in a separate trust store that is used only for federation connections. The \u201cFed. Port\u201d is configured with this separate trust-store.',source:"@site/docs/federation/federationexample.md",sourceDirName:"federation",slug:"/federation/federationexample",permalink:"/docs/federation/federationexample",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/federation/federationexample.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Data Package and Mission File Blocker",permalink:"/docs/federation/datapackagemissionfileblocker"},next:{title:"Metrics",permalink:"/docs/metrics"}},c={},d=[{value:"Alternate Configuration",id:"alternate-configuration",level:2}];function l(e){const t={h1:"h1",h2:"h2",img:"img",p:"p",...(0,i.a)(),...e.components};return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(t.h1,{id:"federation-example",children:"Federation Example"}),"\n",(0,a.jsx)(t.p,{children:'The figure below shows a connectivity graph of two distinct administrative domains. Each administrative domain has multiple sub-groups (e.g. "CN=Alpha") utilizing the group-filtering. The color coding indicates the CA that is used to sign the certificate used for connections. Enclave 1\'s CA signs ATAK client certs and a server certificate. Enclave 2\'s CA also signs ATAK client certs and a server cert. The trust-store listing the allowed CAs for the "User Port" only contains a single CA (i.e. Enclave 1 CA for Enclave 1). To federate the servers, Enclave 1 and Enclave 2 send each other the "public" CA cert. Those certificates are put in a separate trust store that is used only for federation connections. The \u201cFed. Port\u201d is configured with this separate trust-store.\nThe server cert from each administrative domain can now be used to connect to the "Fed. Port" of the other domain.'}),"\n",(0,a.jsx)(t.p,{children:(0,a.jsx)(t.img,{alt:"Federation Example",src:n(584).Z+"",width:"560",height:"416"})}),"\n",(0,a.jsx)(t.h2,{id:"alternate-configuration",children:"Alternate Configuration"}),"\n",(0,a.jsx)(t.p,{children:"The first example had each federate using the same CA and server certificate for local and federate connections. If you are very paranoid, or don't want to share anything about the crypto being used for local clients, you can have a wholly separate CA+server certificate chain that is used for federation. Figure 5 shows how this would work."}),"\n",(0,a.jsx)(t.p,{children:(0,a.jsx)(t.img,{alt:"Alternate Federation Example",src:n(584).Z+"",width:"560",height:"416"})}),"\n",(0,a.jsx)(t.p,{children:"This adds some complexity, but can be used if you don't want to expose your 'internal' CA to the organizations that you are federating with."})]})}function h(e={}){const{wrapper:t}={...(0,i.a)(),...e.components};return t?(0,a.jsx)(t,{...e,children:(0,a.jsx)(l,{...e})}):l(e)}},584:(e,t,n)=>{n.d(t,{Z:()=>a});const a=n.p+"assets/images/fed_10-25b8d73c33c3f42065f26e4cf9ca0de4.png"},1151:(e,t,n)=>{n.d(t,{Z:()=>s,a:()=>o});var a=n(7294);const i={},r=a.createContext(i);function o(e){const t=a.useContext(r);return a.useMemo((function(){return"function"==typeof e?e(t):{...t,...e}}),[t,e])}function s(e){let t;return t=e.disableParentContext?"function"==typeof e.components?e.components(i):e.components||i:o(e.components),a.createElement(r.Provider,{value:t},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ee07fdd2.f6eccc4f.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ee07fdd2.f6eccc4f.js new file mode 100644 index 00000000..c84f0fca --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ee07fdd2.f6eccc4f.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[2546],{9967:(e,n,t)=>{t.r(n),t.d(n,{assets:()=>c,contentTitle:()=>s,default:()=>u,frontMatter:()=>a,metadata:()=>i,toc:()=>l});var r=t(5893),o=t(1151);const a={},s="IronBank",i={id:"dockerinstall/ironbank",title:"IronBank",description:"See https://ironbank.dso.mil/repomap?searchText=tak%20server (Platform One account required)",source:"@site/docs/dockerinstall/ironbank.md",sourceDirName:"dockerinstall",slug:"/dockerinstall/ironbank",permalink:"/docs/dockerinstall/ironbank",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/dockerinstall/ironbank.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Two-Server Upgrade",permalink:"/docs/upgrade/twoserver"},next:{title:"Building and Installing Container Images Using Docker",permalink:"/docs/dockerinstall/buildinstall"}},c={},l=[];function d(e){const n={a:"a",h1:"h1",p:"p",...(0,o.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(n.h1,{id:"ironbank",children:"IronBank"}),"\n",(0,r.jsxs)(n.p,{children:["See ",(0,r.jsx)(n.a,{href:"https://ironbank.dso.mil/repomap?searchText=tak%20server",children:"https://ironbank.dso.mil/repomap?searchText=tak%20server"})," (Platform One account required)\nTAK Server hardened container images are published to the Platform One Iron Bank container repository. See README for each container for installation instructions."]})]})}function u(e={}){const{wrapper:n}={...(0,o.a)(),...e.components};return n?(0,r.jsx)(n,{...e,children:(0,r.jsx)(d,{...e})}):d(e)}},1151:(e,n,t)=>{t.d(n,{Z:()=>i,a:()=>s});var r=t(7294);const o={},a=r.createContext(o);function s(e){const n=r.useContext(a);return r.useMemo((function(){return"function"==typeof e?e(n):{...n,...e}}),[n,e])}function i(e){let n;return n=e.disableParentContext?"function"==typeof e.components?e.components(o):e.components||o:s(e.components),r.createElement(a.Provider,{value:n},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/f4f34a3a.d66c6973.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/f4f34a3a.d66c6973.js new file mode 100644 index 00000000..43944528 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/f4f34a3a.d66c6973.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8636],{743:(t,o,e)=>{e.r(o),e.d(o,{assets:()=>u,contentTitle:()=>a,default:()=>d,frontMatter:()=>n,metadata:()=>c,toc:()=>l});var s=e(5893),r=e(1151);const n={slug:"mdx-blog-post",title:"MDX Blog Post",authors:["slorber"],tags:["docusaurus"]},a=void 0,c={permalink:"/blog/mdx-blog-post",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/blog/2021-08-01-mdx-blog-post.mdx",source:"@site/blog/2021-08-01-mdx-blog-post.mdx",title:"MDX Blog Post",description:"Blog posts support Docusaurus Markdown features, such as MDX.",date:"2021-08-01T00:00:00.000Z",formattedDate:"August 1, 2021",tags:[{label:"docusaurus",permalink:"/blog/tags/docusaurus"}],readingTime:.175,hasTruncateMarker:!1,authors:[{name:"S\xe9bastien Lorber",title:"Docusaurus maintainer",url:"https://sebastienlorber.com",imageURL:"https://github.com/slorber.png",key:"slorber"}],frontMatter:{slug:"mdx-blog-post",title:"MDX Blog Post",authors:["slorber"],tags:["docusaurus"]},unlisted:!1,prevItem:{title:"Welcome to Docusaurus!",permalink:"/blog/welcome"},nextItem:{title:"Long Blog Post",permalink:"/blog/long-blog-post"}},u={authorsImageUrls:[void 0]},l=[];function i(t){const o={a:"a",admonition:"admonition",code:"code",p:"p",pre:"pre",...(0,r.a)(),...t.components};return(0,s.jsxs)(s.Fragment,{children:[(0,s.jsxs)(o.p,{children:["Blog posts support ",(0,s.jsx)(o.a,{href:"https://docusaurus.io/docs/markdown-features",children:"Docusaurus Markdown features"}),", such as ",(0,s.jsx)(o.a,{href:"https://mdxjs.com/",children:"MDX"}),"."]}),"\n",(0,s.jsxs)(o.admonition,{type:"tip",children:[(0,s.jsx)(o.p,{children:"Use the power of React to create interactive blog posts."}),(0,s.jsx)(o.pre,{children:(0,s.jsx)(o.code,{className:"language-js",children:"<button onClick={() => alert('button clicked!')}>Click me!</button>\n"})}),(0,s.jsx)("button",{onClick:()=>alert("button clicked!"),children:"Click me!"})]})]})}function d(t={}){const{wrapper:o}={...(0,r.a)(),...t.components};return o?(0,s.jsx)(o,{...t,children:(0,s.jsx)(i,{...t})}):i(t)}},1151:(t,o,e)=>{e.d(o,{Z:()=>c,a:()=>a});var s=e(7294);const r={},n=s.createContext(r);function a(t){const o=s.useContext(n);return s.useMemo((function(){return"function"==typeof t?t(o):{...o,...t}}),[o,t])}function c(t){let o;return o=t.disableParentContext?"function"==typeof t.components?t.components(r):t.components||r:a(t.components),s.createElement(n.Provider,{value:o},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/f555bcff.dc659cba.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/f555bcff.dc659cba.js new file mode 100644 index 00000000..da4b8315 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/f555bcff.dc659cba.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[8654],{3640:(t,e,r)=>{r.r(e),r.d(e,{assets:()=>u,contentTitle:()=>a,default:()=>p,frontMatter:()=>n,metadata:()=>s,toc:()=>c});var o=r(5893),i=r(1151);const n={},a="Group Filtering for Multicast Networks",s={id:"groupfilteringformulticast",title:"Group Filtering for Multicast Networks",description:"The proxy attribute on the CoreConfig input element ( \\ ) was removed in TAK Server 4.1. The intent of the proxy attribute was to control bridging of multicast networks and federating multicast data. As TAK Server\u2019s group filtering capabilities have evolved, having a dedicated proxy attribute is no longer needed. Using filtergroup on the mcast input you can achieve greater control over multicast traffic.",source:"@site/docs/groupfilteringformulticast.md",sourceDirName:".",slug:"/groupfilteringformulticast",permalink:"/docs/groupfilteringformulticast",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/groupfilteringformulticast.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Logging",permalink:"/docs/logging"},next:{title:"OAuth2 Authentication",permalink:"/docs/oath2authentication"}},u={},c=[];function l(t){const e={code:"code",h1:"h1",p:"p",pre:"pre",...(0,i.a)(),...t.components};return(0,o.jsxs)(o.Fragment,{children:[(0,o.jsx)(e.h1,{id:"group-filtering-for-multicast-networks",children:"Group Filtering for Multicast Networks"}),"\n",(0,o.jsx)(e.p,{children:'The proxy attribute on the CoreConfig input element ( <input \u2026 proxy="true" \u2026 /> ) was removed in TAK Server 4.1. The intent of the proxy attribute was to control bridging of multicast networks and federating multicast data. As TAK Server\u2019s group filtering capabilities have evolved, having a dedicated proxy attribute is no longer needed. Using filtergroup on the mcast input you can achieve greater control over multicast traffic.\nThe default behavior in TAK Server 4.1 and higher is to put multicast traffic in the __ANON__ group. You can use a filtergroup on the mcast input to put your mcast traffic into a dedicated multicast group, for example:'}),"\n",(0,o.jsx)(e.pre,{children:(0,o.jsx)(e.code,{className:"language-bash",children:'<input auth="anonymous" _name=" SAproxy " protocol="mcast" port="6969" group="239.2.3.1">\n <filtergroup>__MCAST__</filtergroup>\n</input>\n'})}),"\n",(0,o.jsx)(e.p,{children:"Then add the __MCAST__ group as a filtergroup on any other inputs you wanted to share multicast traffic with. For example, to share multicast traffic with the tls/8089, configure your input filtergroups as follows:"}),"\n",(0,o.jsx)(e.pre,{children:(0,o.jsx)(e.code,{className:"language-bash",children:'<input auth="anonymous" _name="stdssl1" protocol="tls" port="8089" archive="true">\n <filtergroup>__ANON__</filtergroup>\n <filtergroup>__MCAST__</filtergroup>\n</input>\n'})}),"\n",(0,o.jsx)(e.p,{children:"This same approach works for federations. You can __MCAST__ as an outboundGroup on any federates that you wanted to share multicast traffic with. Using the filtergroup approach allows for creation of input specific multicast groups, allowing control of how messages from multicast networks are routed."})]})}function p(t={}){const{wrapper:e}={...(0,i.a)(),...t.components};return e?(0,o.jsx)(e,{...t,children:(0,o.jsx)(l,{...t})}):l(t)}},1151:(t,e,r)=>{r.d(e,{Z:()=>s,a:()=>a});var o=r(7294);const i={},n=o.createContext(i);function a(t){const e=o.useContext(n);return o.useMemo((function(){return"function"==typeof t?t(e):{...e,...t}}),[e,t])}function s(t){let e;return e=t.disableParentContext?"function"==typeof t.components?t.components(i):t.components||i:a(t.components),o.createElement(n.Provider,{value:e},t.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ff1782e3.f417be7e.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ff1782e3.f417be7e.js new file mode 100644 index 00000000..88eae564 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/ff1782e3.f417be7e.js @@ -0,0 +1 @@ +"use strict";(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[7995],{2878:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>d,contentTitle:()=>t,default:()=>u,frontMatter:()=>o,metadata:()=>l,toc:()=>i});var r=n(5893),a=n(1151);const o={},t="Single-Server Upgrade",l={id:"upgrade/singleserver",title:"Single-Server Upgrade",description:"Rocky Linux 8",source:"@site/docs/upgrade/singleserver.md",sourceDirName:"upgrade",slug:"/upgrade/singleserver",permalink:"/docs/upgrade/singleserver",draft:!1,unlisted:!1,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/docs/upgrade/singleserver.md",tags:[],version:"current",frontMatter:{},sidebar:"tutorialSidebar",previous:{title:"Overview",permalink:"/docs/upgrade/overview"},next:{title:"Two-Server Upgrade",permalink:"/docs/upgrade/twoserver"}},d={},i=[{value:"Rocky Linux 8",id:"rocky-linux-8",level:2},{value:"RHEL 8",id:"rhel-8",level:2},{value:"RHEL 7",id:"rhel-7",level:2},{value:"Ubuntu and Raspberry Pi OS",id:"ubuntu-and-raspberry-pi-os",level:2},{value:"Centos 7",id:"centos-7",level:2}];function c(e){const s={code:"code",em:"em",h1:"h1",h2:"h2",p:"p",pre:"pre",...(0,a.a)(),...e.components};return(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(s.h1,{id:"single-server-upgrade",children:"Single-Server Upgrade"}),"\n",(0,r.jsx)(s.h2,{id:"rocky-linux-8",children:"Rocky Linux 8"}),"\n",(0,r.jsx)(s.p,{children:"Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable PowerTools (needed for dependencies of postgis.) Install TAK server."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install epel-release -y\nsudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm\nsudo dnf -qy module disable postgresql && sudo dnf update -y\nsudo dnf install java-17-openjdk-devel -y\nsudo dnf config-manager --set-enabled powertools\n"})}),"\n",(0,r.jsx)(s.p,{children:"Upgrade Tak server"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install takserver-5.1-RELEASEx.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"rhel-8",children:"RHEL 8"}),"\n",(0,r.jsx)(s.p,{children:"Setup the extra postgres yum repo for the latest postgres and postgis. Install Java 17. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y\nsudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo dnf update -y && sudo dnf install java-17-openjdk-devel -y\nsudo dnf module disable postgresql\nsudo subscription-manager config --rhsm.manage_repos=1\nsudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms\n"})}),"\n",(0,r.jsx)(s.p,{children:(0,r.jsx)(s.em,{children:"Note: If you get the error \u2018This system has no repositories available through subscriptions\u2019, you need to subscribe your system with:"})}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach\n"})}),"\n",(0,r.jsx)(s.p,{children:"Upgrade Tak server"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo dnf install takserver-5.1-RELEASEx.noarch.rpm --setopt=clean_requirements_on_remove=false -y\n"})}),"\n",(0,r.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"rhel-7",children:"RHEL 7"}),"\n",(0,r.jsx)(s.p,{children:"If you have not previously done so, install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Upgrade TAK server"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\nsudo rpm -Uvh takserver-5.1-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on Centos 7 and RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"ubuntu-and-raspberry-pi-os",children:"Ubuntu and Raspberry Pi OS"}),"\n",(0,r.jsx)(s.p,{children:"Upgrade Tak server"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo apt install ./takserver-5.1-RELEASEx_all.deb\n"})}),"\n",(0,r.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})}),"\n",(0,r.jsx)(s.h2,{id:"centos-7",children:"Centos 7"}),"\n",(0,r.jsx)(s.p,{children:"If you have not previously done so, install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Upgrade Tak server."}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo yum install epel-release -y\nsudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y\nsudo yum install -y postgis33_15 postgis33_15-utils\nsudo yum install -y postgresql15-server postgresql15-contrib\nsudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm\nsudo rpm -Uvh takserver-5.1-RELEASEx.noarch.rpm --nodeps\n"})}),"\n",(0,r.jsx)(s.p,{children:"Note that the yum package manager does not currently support JDK 17 on Centos 7 and RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8."}),"\n",(0,r.jsx)(s.p,{children:"Check Java version:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"java -version\n"})}),"\n",(0,r.jsx)(s.p,{children:"This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:"}),"\n",(0,r.jsx)(s.pre,{children:(0,r.jsx)(s.code,{className:"language-bash",children:"sudo alternatives --config java\n"})})]})}function u(e={}){const{wrapper:s}={...(0,a.a)(),...e.components};return s?(0,r.jsx)(s,{...e,children:(0,r.jsx)(c,{...e})}):c(e)}},1151:(e,s,n)=>{n.d(s,{Z:()=>l,a:()=>t});var r=n(7294);const a={},o=r.createContext(a);function t(e){const s=r.useContext(o);return r.useMemo((function(){return"function"==typeof e?e(s):{...s,...e}}),[s,e])}function l(e){let s;return s=e.disableParentContext?"function"==typeof e.components?e.components(a):e.components||a:t(e.components),r.createElement(o.Provider,{value:s},e.children)}}}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/main.633f55f0.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/main.633f55f0.js new file mode 100644 index 00000000..c22f03ce --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/main.633f55f0.js @@ -0,0 +1,2 @@ +/*! For license information please see main.633f55f0.js.LICENSE.txt */ +(self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[]).push([[179],{723:(e,t,n)=>{"use strict";n.d(t,{Z:()=>p});n(7294);var r=n(8356),o=n.n(r),a=n(6887);const i={"00483006":[()=>n.e(6074).then(n.bind(n,8114)),"@site/docs/installation/setup_wizard.md",8114],"01a85c17":[()=>Promise.all([n.e(532),n.e(4013)]).then(n.bind(n,1223)),"@theme/BlogTagsListPage",1223],"031793e1":[()=>n.e(1633).then(n.t.bind(n,2511,19)),"~blog/default/blog-tags-facebook-038.json",2511],"062c1b27":[()=>n.e(8199).then(n.bind(n,6631)),"@site/docs/configuration/groupfiltering.md",6631],"08c34d24":[()=>n.e(7357).then(n.bind(n,395)),"@site/docs/configuration/configuremessagingrepositorywebui.md",395],"09573330":[()=>n.e(5017).then(n.bind(n,8854)),"@site/docs/system-requirements/awsrequirements.md",8854],"096bfee4":[()=>n.e(7178).then(n.t.bind(n,5010,19)),"~blog/default/blog-tags-facebook-038-list.json",5010],"0a229eeb":[()=>n.e(1925).then(n.bind(n,1563)),"@site/docs/upgrade/twoserver.md",1563],"0a2c82fb":[()=>n.e(54).then(n.bind(n,8871)),"@site/blog/2024-01-28-welcome-to-docusaurus-static.md?truncated=true",8871],"0f425520":[()=>n.e(7240).then(n.bind(n,3642)),"@site/docs/configuration/overview.md",3642],"17495a52":[()=>n.e(4620).then(n.bind(n,6535)),"@site/docs/federation/overview.md",6535],17896441:[()=>Promise.all([n.e(532),n.e(7395),n.e(7918)]).then(n.bind(n,8945)),"@theme/DocItem",8945],19826855:[()=>n.e(8875).then(n.bind(n,7982)),"@site/docs/usermanagementui.md",7982],"1b9776b6":[()=>n.e(8761).then(n.bind(n,664)),"@site/docs/federation/federatedgroupmapping.md",664],"1dba1ecf":[()=>n.e(2510).then(n.bind(n,7937)),"@site/docs/metrics.md",7937],"1f391b9e":[()=>Promise.all([n.e(532),n.e(7395),n.e(3085)]).then(n.bind(n,4247)),"@theme/MDXPage",4247],"20598c5e":[()=>n.e(2399).then(n.bind(n,8635)),"@site/docs/configuration/authenticationbackends.md",8635],"21d68595":[()=>n.e(8888).then(n.bind(n,3754)),"@site/docs/appendixe.md",3754],"24dd378c":[()=>n.e(191).then(n.bind(n,1187)),"@site/docs/imagetest.md",1187],"25f60d21":[()=>n.e(278).then(n.bind(n,7822)),"@site/docs/federation/maketheconnection.md",7822],"2808364d":[()=>n.e(2981).then(n.bind(n,3868)),"@site/docs/oath2authentication.md",3868],"2c26bb54":[()=>n.e(308).then(n.bind(n,1537)),"@site/docs/webtak.md",1537],30688810:[()=>n.e(6317).then(n.bind(n,5633)),"@site/docs/dataretentiontool.md",5633],"30a24c52":[()=>n.e(453).then(n.t.bind(n,8605,19)),"~blog/default/blog-tags-hello-039.json",8605],"3262a34b":[()=>n.e(8444).then(n.bind(n,7648)),"@site/docs/configuration/configurewebui.md",7648],"3660216b":[()=>n.e(7922).then(n.bind(n,1530)),"@site/blog/2024-01-28-welcome-to-docusaurus-static.md",1530],"3682177e":[()=>n.e(2537).then(n.bind(n,5558)),"@site/docs/deviceprofiles.md",5558],"393be207":[()=>n.e(7414).then(n.bind(n,1181)),"@site/src/pages/markdown-page.md",1181],"3abe8fb9":[()=>n.e(5521).then(n.bind(n,3438)),"@site/docs/logging.md",3438],"3d8d21df":[()=>n.e(6535).then(n.bind(n,4982)),"@site/docs/about.md",4982],"3e56fa0f":[()=>n.e(6425).then(n.bind(n,3650)),"@site/docs/installation/oneserver/prerequisite.md",3650],"4604b011":[()=>n.e(9647).then(n.t.bind(n,9019,19)),"~blog/default/blog-tags-docusaurus-static-ee3-list.json",9019],"46bbbc4b":[()=>n.e(3310).then(n.t.bind(n,3769,19)),"/builds/core/tak-docs/takserver/config-guide/takserver_docs/.docusaurus/docusaurus-plugin-content-docs/default/plugin-route-context-module-100.json",3769],"4a298a69":[()=>n.e(6203).then(n.bind(n,119)),"@site/docs/installation/twoserver/serverone/dependencysetup.md",119],"4b4e9c91":[()=>n.e(2914).then(n.bind(n,7179)),"@site/docs/system-requirements/serverrequirements.md",7179],"4c9e35b1":[()=>n.e(9035).then(n.t.bind(n,499,19)),"~blog/default/blog-tags-hola-ea2-list.json",499],"4cd5e786":[()=>n.e(4020).then(n.t.bind(n,5745,19)),"/builds/core/tak-docs/takserver/config-guide/takserver_docs/.docusaurus/docusaurus-plugin-content-pages/default/plugin-route-context-module-100.json",5745],"4e810807":[()=>n.e(9349).then(n.bind(n,5901)),"@site/docs/federation/uploadfederatecert.md",5901],"520e22a5":[()=>n.e(9713).then(n.bind(n,2022)),"@site/docs/installation/twoserver/servertwo/prerequisites.md",2022],"55d8b2aa":[()=>n.e(1506).then(n.bind(n,6863)),"@site/docs/firewall/overview.md",6863],"5817ad8d":[()=>n.e(5582).then(n.bind(n,4446)),"@site/docs/dockerinstall/buildinstall.md",4446],"587818ee":[()=>n.e(9516).then(n.bind(n,1600)),"@site/docs/installation/twoserver/servertwo/configuretakserver.md",1600],59362658:[()=>n.e(2267).then(n.bind(n,7797)),"@site/blog/2021-08-01-mdx-blog-post.mdx",7797],"5e95c892":[()=>n.e(9661).then(n.bind(n,1892)),"@theme/DocsRoot",1892],"5e9f5e1a":[()=>Promise.resolve().then(n.bind(n,6809)),"@generated/docusaurus.config",6809],"5f4ff9a9":[()=>n.e(722).then(n.bind(n,8107)),"@site/docs/appendixa.md",8107],"608ae6a4":[()=>n.e(6938).then(n.t.bind(n,4545,19)),"~blog/default/blog-tags-docusaurus-0e0-list.json",4545],"642e9f94":[()=>n.e(7861).then(n.bind(n,8111)),"@site/docs/installation/twoserver/overview.md",8111],"660a8848":[()=>n.e(1881).then(n.bind(n,682)),"@site/docs/upgrade/overview.md",682],66406991:[()=>n.e(110).then(n.t.bind(n,711,19)),"~blog/default/blog-tags-hello-039-list.json",711],"6875c492":[()=>Promise.all([n.e(532),n.e(7395),n.e(9677),n.e(8610)]).then(n.bind(n,1714)),"@theme/BlogTagsPostsPage",1714],"73664a40":[()=>n.e(3514).then(n.bind(n,1985)),"@site/blog/2019-05-29-long-blog-post.md",1985],"7661071f":[()=>n.e(9642).then(n.bind(n,3174)),"@site/blog/2021-08-26-welcome/index.md?truncated=true",3174],"7c7b5a9b":[()=>n.e(9317).then(n.bind(n,8621)),"@site/docs/appendixd.md",8621],"7feddd0b":[()=>n.e(9882).then(n.bind(n,7246)),"@site/docs/appendixc.md",7246],"814f3328":[()=>n.e(2535).then(n.t.bind(n,5641,19)),"~blog/default/blog-post-list-prop-default.json",5641],"867bb2d0":[()=>n.e(5243).then(n.bind(n,3998)),"@site/docs/configuration/groupassignmentbyinput.md",3998],"8717b14a":[()=>n.e(948).then(n.bind(n,7106)),"@site/blog/2019-05-29-long-blog-post.md?truncated=true",7106],"8838a7ac":[()=>n.e(3268).then(n.bind(n,7217)),"@site/docs/configuration/optionallydisableui.md",7217],"89d39a78":[()=>n.e(8954).then(n.bind(n,9215)),"@site/docs/installation/oneserver/takserverconfiguration.md",9215],"8c65a213":[()=>n.e(5097).then(n.bind(n,8752)),"@site/docs/federation/federationdisruptiontolerance.md",8752],"925b3f96":[()=>n.e(9003).then(n.bind(n,3902)),"@site/blog/2019-05-28-first-blog-post.md?truncated=true",3902],"935f2afb":[()=>n.e(53).then(n.t.bind(n,1109,19)),"~docs/default/version-current-metadata-prop-751.json",1109],"93acc585":[()=>n.e(4546).then(n.bind(n,8393)),"@site/docs/firewall/ubunturaspberrypi.md",8393],"995e17fc":[()=>n.e(5550).then(n.bind(n,5400)),"@site/docs/system-requirements/systemrequirements.md",5400],"9beb87c2":[()=>n.e(80).then(n.bind(n,4123)),"@site/docs/changelog.md",4123],"9e4087bc":[()=>n.e(3608).then(n.bind(n,3169)),"@theme/BlogArchivePage",3169],a5758fe9:[()=>n.e(894).then(n.t.bind(n,7011,19)),"~blog/default/blog-tags-docusaurus-static-ee3.json",7011],a6aa9e1f:[()=>Promise.all([n.e(532),n.e(7395),n.e(9677),n.e(3089)]).then(n.bind(n,46)),"@theme/BlogListPage",46],a7023ddc:[()=>n.e(1713).then(n.t.bind(n,3457,19)),"~blog/default/blog-tags-tags-4c2.json",3457],a7bd4aaa:[()=>n.e(8518).then(n.bind(n,8564)),"@theme/DocVersionRoot",8564],a80da1cf:[()=>n.e(3205).then(n.t.bind(n,4863,19)),"~blog/default/blog-tags-docusaurus-0e0.json",4863],a94703ab:[()=>Promise.all([n.e(532),n.e(4368)]).then(n.bind(n,2674)),"@theme/DocRoot",2674],b1e73d9b:[()=>n.e(1071).then(n.bind(n,3510)),"@site/docs/configuration/vbmadminconfig.md",3510],b23f3d07:[()=>n.e(9077).then(n.t.bind(n,4469,19)),"/builds/core/tak-docs/takserver/config-guide/takserver_docs/.docusaurus/docusaurus-plugin-content-blog/default/plugin-route-context-module-100.json",4469],b2b675dd:[()=>n.e(533).then(n.t.bind(n,8017,19)),"~blog/default/blog-c06.json",8017],b2f554cd:[()=>n.e(1477).then(n.t.bind(n,10,19)),"~blog/default/blog-archive-80c.json",10],b31dded6:[()=>n.e(9261).then(n.bind(n,260)),"@site/docs/federation/datapackagemissionfileblocker.md",260],bb1b26fa:[()=>n.e(2587).then(n.bind(n,6660)),"@site/docs/firewall/rhelrockycentos.md",6660],c451c5ed:[()=>n.e(8278).then(n.bind(n,7895)),"@site/docs/federation/enablefederation.md",7895],c4f5d8e4:[()=>Promise.all([n.e(532),n.e(4195)]).then(n.bind(n,3261)),"@site/src/pages/index.js",3261],c7687382:[()=>n.e(2547).then(n.bind(n,2952)),"@site/docs/appendixb.md",2952],c92107ec:[()=>n.e(6129).then(n.bind(n,4848)),"@site/docs/installation/twoserver/servertwo/installtakserver.md",4848],ccc49370:[()=>Promise.all([n.e(532),n.e(7395),n.e(9677),n.e(6103)]).then(n.bind(n,5203)),"@theme/BlogPostPage",5203],ce40d3b6:[()=>n.e(3511).then(n.bind(n,7781)),"@site/docs/configuration/groupassignmentusingauth.md",7781],cfff6e91:[()=>n.e(9740).then(n.bind(n,734)),"@site/docs/installation/overview.md",734],d43d2919:[()=>n.e(3354).then(n.bind(n,9475)),"@site/docs/installation/oneserver/takserverinstallation.md",9475],d9f32620:[()=>n.e(1914).then(n.bind(n,8123)),"@site/blog/2021-08-26-welcome/index.md",8123],da5cb8ba:[()=>n.e(2200).then(n.bind(n,9850)),"@site/docs/softwareinstallationlocation.md",9850],e16015ca:[()=>n.e(9700).then(n.t.bind(n,5688,19)),"~blog/default/blog-tags-hola-ea2.json",5688],e273c56f:[()=>n.e(2362).then(n.bind(n,9954)),"@site/blog/2019-05-28-first-blog-post.md",9954],e3eb95dd:[()=>n.e(2372).then(n.bind(n,3124)),"@site/docs/configuration/groupassignmentusingclientcerts.md",3124],e54870e4:[()=>n.e(60).then(n.bind(n,3213)),"@site/docs/federation/federationexample.md",3213],ee07fdd2:[()=>n.e(2546).then(n.bind(n,9967)),"@site/docs/dockerinstall/ironbank.md",9967],f4f34a3a:[()=>n.e(8636).then(n.bind(n,743)),"@site/blog/2021-08-01-mdx-blog-post.mdx?truncated=true",743],f555bcff:[()=>n.e(8654).then(n.bind(n,3640)),"@site/docs/groupfilteringformulticast.md",3640],ff1782e3:[()=>n.e(7995).then(n.bind(n,2878)),"@site/docs/upgrade/singleserver.md",2878]};var l=n(5893);function s(e){let{error:t,retry:n,pastDelay:r}=e;return t?(0,l.jsxs)("div",{style:{textAlign:"center",color:"#fff",backgroundColor:"#fa383e",borderColor:"#fa383e",borderStyle:"solid",borderRadius:"0.25rem",borderWidth:"1px",boxSizing:"border-box",display:"block",padding:"1rem",flex:"0 0 50%",marginLeft:"25%",marginRight:"25%",marginTop:"5rem",maxWidth:"50%",width:"100%"},children:[(0,l.jsx)("p",{children:String(t)}),(0,l.jsx)("div",{children:(0,l.jsx)("button",{type:"button",onClick:n,children:"Retry"})})]}):r?(0,l.jsx)("div",{style:{display:"flex",justifyContent:"center",alignItems:"center",height:"100vh"},children:(0,l.jsx)("svg",{id:"loader",style:{width:128,height:110,position:"absolute",top:"calc(100vh - 64%)"},viewBox:"0 0 45 45",xmlns:"http://www.w3.org/2000/svg",stroke:"#61dafb",children:(0,l.jsxs)("g",{fill:"none",fillRule:"evenodd",transform:"translate(1 1)",strokeWidth:"2",children:[(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"1.5s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"1.5s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"1.5s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsxs)("circle",{cx:"22",cy:"22",r:"6",strokeOpacity:"0",children:[(0,l.jsx)("animate",{attributeName:"r",begin:"3s",dur:"3s",values:"6;22",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-opacity",begin:"3s",dur:"3s",values:"1;0",calcMode:"linear",repeatCount:"indefinite"}),(0,l.jsx)("animate",{attributeName:"stroke-width",begin:"3s",dur:"3s",values:"2;0",calcMode:"linear",repeatCount:"indefinite"})]}),(0,l.jsx)("circle",{cx:"22",cy:"22",r:"8",children:(0,l.jsx)("animate",{attributeName:"r",begin:"0s",dur:"1.5s",values:"6;1;2;3;4;5;6",calcMode:"linear",repeatCount:"indefinite"})})]})})}):null}var c=n(9670),u=n(226);function d(e,t){if("*"===e)return o()({loading:s,loader:()=>n.e(1772).then(n.bind(n,1772)),modules:["@theme/NotFound"],webpack:()=>[1772],render(e,t){const n=e.default;return(0,l.jsx)(u.z,{value:{plugin:{name:"native",id:"default"}},children:(0,l.jsx)(n,{...t})})}});const r=a[`${e}-${t}`],d={},p=[],f=[],m=(0,c.Z)(r);return Object.entries(m).forEach((e=>{let[t,n]=e;const r=i[n];r&&(d[t]=r[0],p.push(r[1]),f.push(r[2]))})),o().Map({loading:s,loader:d,modules:p,webpack:()=>f,render(t,n){const o=JSON.parse(JSON.stringify(r));Object.entries(t).forEach((t=>{let[n,r]=t;const a=r.default;if(!a)throw new Error(`The page component at ${e} doesn't have a default export. This makes it impossible to render anything. Consider default-exporting a React component.`);"object"!=typeof a&&"function"!=typeof a||Object.keys(r).filter((e=>"default"!==e)).forEach((e=>{a[e]=r[e]}));let i=o;const l=n.split(".");l.slice(0,-1).forEach((e=>{i=i[e]})),i[l[l.length-1]]=a}));const a=o.__comp;delete o.__comp;const i=o.__context;return delete o.__context,(0,l.jsx)(u.z,{value:i,children:(0,l.jsx)(a,{...o,...n})})}})}const p=[{path:"/blog",component:d("/blog","1ae"),exact:!0},{path:"/blog/2024/01/28/welcome-to-docusaurus-static",component:d("/blog/2024/01/28/welcome-to-docusaurus-static","fb0"),exact:!0},{path:"/blog/archive",component:d("/blog/archive","c5b"),exact:!0},{path:"/blog/first-blog-post",component:d("/blog/first-blog-post","3c5"),exact:!0},{path:"/blog/long-blog-post",component:d("/blog/long-blog-post","d8b"),exact:!0},{path:"/blog/mdx-blog-post",component:d("/blog/mdx-blog-post","2ec"),exact:!0},{path:"/blog/tags",component:d("/blog/tags","8e7"),exact:!0},{path:"/blog/tags/docusaurus",component:d("/blog/tags/docusaurus","85a"),exact:!0},{path:"/blog/tags/docusaurus-static",component:d("/blog/tags/docusaurus-static","057"),exact:!0},{path:"/blog/tags/facebook",component:d("/blog/tags/facebook","045"),exact:!0},{path:"/blog/tags/hello",component:d("/blog/tags/hello","e34"),exact:!0},{path:"/blog/tags/hola",component:d("/blog/tags/hola","e26"),exact:!0},{path:"/blog/welcome",component:d("/blog/welcome","d6d"),exact:!0},{path:"/markdown-page",component:d("/markdown-page","567"),exact:!0},{path:"/docs",component:d("/docs","2ed"),routes:[{path:"/docs",component:d("/docs","be7"),routes:[{path:"/docs",component:d("/docs","882"),routes:[{path:"/docs/about",component:d("/docs/about","99a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/appendixa",component:d("/docs/appendixa","ba3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/appendixb",component:d("/docs/appendixb","27f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/appendixc",component:d("/docs/appendixc","512"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/appendixd",component:d("/docs/appendixd","cad"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/appendixe",component:d("/docs/appendixe","af4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/changelog",component:d("/docs/changelog","fe5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/authenticationbackends",component:d("/docs/configuration/authenticationbackends","6e3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/configuremessagingrepositorywebui",component:d("/docs/configuration/configuremessagingrepositorywebui","f84"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/configurewebui",component:d("/docs/configuration/configurewebui","3e5"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/groupassignmentbyinput",component:d("/docs/configuration/groupassignmentbyinput","7db"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/groupassignmentusingauth",component:d("/docs/configuration/groupassignmentusingauth","301"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/groupassignmentusingclientcerts",component:d("/docs/configuration/groupassignmentusingclientcerts","cae"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/groupfiltering",component:d("/docs/configuration/groupfiltering","b22"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/optionallydisableui",component:d("/docs/configuration/optionallydisableui","3a8"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/overview",component:d("/docs/configuration/overview","99e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/configuration/vbmadminconfig",component:d("/docs/configuration/vbmadminconfig","c39"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/dataretentiontool",component:d("/docs/dataretentiontool","a6a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/deviceprofiles",component:d("/docs/deviceprofiles","a69"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/dockerinstall/buildinstall",component:d("/docs/dockerinstall/buildinstall","56e"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/dockerinstall/ironbank",component:d("/docs/dockerinstall/ironbank","52c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/datapackagemissionfileblocker",component:d("/docs/federation/datapackagemissionfileblocker","35a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/enablefederation",component:d("/docs/federation/enablefederation","c3a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/federatedgroupmapping",component:d("/docs/federation/federatedgroupmapping","d22"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/federationdisruptiontolerance",component:d("/docs/federation/federationdisruptiontolerance","6bc"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/federationexample",component:d("/docs/federation/federationexample","67f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/maketheconnection",component:d("/docs/federation/maketheconnection","5a3"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/overview",component:d("/docs/federation/overview","847"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/federation/uploadfederatecert",component:d("/docs/federation/uploadfederatecert","ea8"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/firewall/overview",component:d("/docs/firewall/overview","f8f"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/firewall/rhelrockycentos",component:d("/docs/firewall/rhelrockycentos","b08"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/firewall/ubunturaspberrypi",component:d("/docs/firewall/ubunturaspberrypi","c94"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/groupfilteringformulticast",component:d("/docs/groupfilteringformulticast","f11"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/imagetest",component:d("/docs/imagetest","0d6"),exact:!0},{path:"/docs/installation/oneserver/prerequisite",component:d("/docs/installation/oneserver/prerequisite","2a7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/oneserver/takserverconfiguration",component:d("/docs/installation/oneserver/takserverconfiguration","ccf"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/oneserver/takserverinstallation",component:d("/docs/installation/oneserver/takserverinstallation","338"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/overview",component:d("/docs/installation/overview","2d7"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/setup_wizard",component:d("/docs/installation/setup_wizard","372"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/twoserver/overview",component:d("/docs/installation/twoserver/overview","5ad"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/twoserver/serverone/dependencysetup",component:d("/docs/installation/twoserver/serverone/dependencysetup","7e8"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/twoserver/servertwo/configuretakserver",component:d("/docs/installation/twoserver/servertwo/configuretakserver","b42"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/twoserver/servertwo/installtakserver",component:d("/docs/installation/twoserver/servertwo/installtakserver","638"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/installation/twoserver/servertwo/prerequisites",component:d("/docs/installation/twoserver/servertwo/prerequisites","69c"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/logging",component:d("/docs/logging","9cf"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/metrics",component:d("/docs/metrics","d90"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/oath2authentication",component:d("/docs/oath2authentication","72a"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/softwareinstallationlocation",component:d("/docs/softwareinstallationlocation","f33"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/system-requirements/awsrequirements",component:d("/docs/system-requirements/awsrequirements","9aa"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/system-requirements/serverrequirements",component:d("/docs/system-requirements/serverrequirements","081"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/system-requirements/systemrequirements",component:d("/docs/system-requirements/systemrequirements","1f4"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/upgrade/overview",component:d("/docs/upgrade/overview","bbd"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/upgrade/singleserver",component:d("/docs/upgrade/singleserver","f22"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/upgrade/twoserver",component:d("/docs/upgrade/twoserver","b08"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/usermanagementui",component:d("/docs/usermanagementui","7ab"),exact:!0,sidebar:"tutorialSidebar"},{path:"/docs/webtak",component:d("/docs/webtak","a03"),exact:!0,sidebar:"tutorialSidebar"}]}]}]},{path:"/",component:d("/","141"),exact:!0},{path:"*",component:d("*")}]},8934:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,t:()=>i});var r=n(7294),o=n(5893);const a=r.createContext(!1);function i(e){let{children:t}=e;const[n,i]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{i(!0)}),[]),(0,o.jsx)(a.Provider,{value:n,children:t})}},7221:(e,t,n)=>{"use strict";var r=n(7294),o=n(745),a=n(3727),i=n(405),l=n(412);const s=[n(2497),n(3310),n(8320),n(2295)];var c=n(723),u=n(6550),d=n(8790),p=n(5893);function f(e){let{children:t}=e;return(0,p.jsx)(p.Fragment,{children:t})}var m=n(5742),g=n(2263),h=n(4996),b=n(6668),y=n(1944),v=n(4711),w=n(9727),k=n(3320),x=n(8780),S=n(197);function E(){const{i18n:{currentLocale:e,defaultLocale:t,localeConfigs:n}}=(0,g.Z)(),r=(0,v.l)(),o=n[e].htmlLang,a=e=>e.replace("-","_");return(0,p.jsxs)(m.Z,{children:[Object.entries(n).map((e=>{let[t,{htmlLang:n}]=e;return(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:n},t)})),(0,p.jsx)("link",{rel:"alternate",href:r.createUrl({locale:t,fullyQualified:!0}),hrefLang:"x-default"}),(0,p.jsx)("meta",{property:"og:locale",content:a(o)}),Object.values(n).filter((e=>o!==e.htmlLang)).map((e=>(0,p.jsx)("meta",{property:"og:locale:alternate",content:a(e.htmlLang)},`meta-og-${e.htmlLang}`)))]})}function _(e){let{permalink:t}=e;const{siteConfig:{url:n}}=(0,g.Z)(),r=function(){const{siteConfig:{url:e,baseUrl:t,trailingSlash:n}}=(0,g.Z)(),{pathname:r}=(0,u.TH)();return e+(0,x.applyTrailingSlash)((0,h.Z)(r),{trailingSlash:n,baseUrl:t})}(),o=t?`${n}${t}`:r;return(0,p.jsxs)(m.Z,{children:[(0,p.jsx)("meta",{property:"og:url",content:o}),(0,p.jsx)("link",{rel:"canonical",href:o})]})}function C(){const{i18n:{currentLocale:e}}=(0,g.Z)(),{metadata:t,image:n}=(0,b.L)();return(0,p.jsxs)(p.Fragment,{children:[(0,p.jsxs)(m.Z,{children:[(0,p.jsx)("meta",{name:"twitter:card",content:"summary_large_image"}),(0,p.jsx)("body",{className:w.h})]}),n&&(0,p.jsx)(y.d,{image:n}),(0,p.jsx)(_,{}),(0,p.jsx)(E,{}),(0,p.jsx)(S.Z,{tag:k.HX,locale:e}),(0,p.jsx)(m.Z,{children:t.map(((e,t)=>(0,p.jsx)("meta",{...e},t)))})]})}const T=new Map;function j(e){if(T.has(e.pathname))return{...e,pathname:T.get(e.pathname)};if((0,d.f)(c.Z,e.pathname).some((e=>{let{route:t}=e;return!0===t.exact})))return T.set(e.pathname,e.pathname),e;const t=e.pathname.trim().replace(/(?:\/index)?\.html$/,"")||"/";return T.set(e.pathname,t),{...e,pathname:t}}var A=n(8934),L=n(8940),N=n(469);function R(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;r<t;r++)n[r-1]=arguments[r];const o=s.map((t=>{const r=t.default?.[e]??t[e];return r?.(...n)}));return()=>o.forEach((e=>e?.()))}const P=function(e){let{children:t,location:n,previousLocation:r}=e;return(0,N.Z)((()=>{r!==n&&(!function(e){let{location:t,previousLocation:n}=e;if(!n)return;const r=t.pathname===n.pathname,o=t.hash===n.hash,a=t.search===n.search;if(r&&o&&!a)return;const{hash:i}=t;if(i){const e=decodeURIComponent(i.substring(1)),t=document.getElementById(e);t?.scrollIntoView()}else window.scrollTo(0,0)}({location:n,previousLocation:r}),R("onRouteDidUpdate",{previousLocation:r,location:n}))}),[r,n]),t};function O(e){const t=Array.from(new Set([e,decodeURI(e)])).map((e=>(0,d.f)(c.Z,e))).flat();return Promise.all(t.map((e=>e.route.component.preload?.())))}class D extends r.Component{previousLocation;routeUpdateCleanupCb;constructor(e){super(e),this.previousLocation=null,this.routeUpdateCleanupCb=l.Z.canUseDOM?R("onRouteUpdate",{previousLocation:null,location:this.props.location}):()=>{},this.state={nextRouteHasLoaded:!0}}shouldComponentUpdate(e,t){if(e.location===this.props.location)return t.nextRouteHasLoaded;const n=e.location;return this.previousLocation=this.props.location,this.setState({nextRouteHasLoaded:!1}),this.routeUpdateCleanupCb=R("onRouteUpdate",{previousLocation:this.previousLocation,location:n}),O(n.pathname).then((()=>{this.routeUpdateCleanupCb(),this.setState({nextRouteHasLoaded:!0})})).catch((e=>{console.warn(e),window.location.reload()})),!1}render(){const{children:e,location:t}=this.props;return(0,p.jsx)(P,{previousLocation:this.previousLocation,location:t,children:(0,p.jsx)(u.AW,{location:t,render:()=>e})})}}const I=D,F="__docusaurus-base-url-issue-banner-container",M="__docusaurus-base-url-issue-banner",z="__docusaurus-base-url-issue-banner-suggestion-container";function B(e){return`\ndocument.addEventListener('DOMContentLoaded', function maybeInsertBanner() {\n var shouldInsert = typeof window['docusaurus'] === 'undefined';\n shouldInsert && insertBanner();\n});\n\nfunction insertBanner() {\n var bannerContainer = document.createElement('div');\n bannerContainer.id = '${F}';\n var bannerHtml = ${JSON.stringify(function(e){return`\n<div id="${M}" style="border: thick solid red; background-color: rgb(255, 230, 179); margin: 20px; padding: 20px; font-size: 20px;">\n <p style="font-weight: bold; font-size: 30px;">Your Docusaurus site did not load properly.</p>\n <p>A very common reason is a wrong site <a href="https://docusaurus.io/docs/docusaurus.config.js/#baseUrl" style="font-weight: bold;">baseUrl configuration</a>.</p>\n <p>Current configured baseUrl = <span style="font-weight: bold; color: red;">${e}</span> ${"/"===e?" (default value)":""}</p>\n <p>We suggest trying baseUrl = <span id="${z}" style="font-weight: bold; color: green;"></span></p>\n</div>\n`}(e)).replace(/</g,"\\<")};\n bannerContainer.innerHTML = bannerHtml;\n document.body.prepend(bannerContainer);\n var suggestionContainer = document.getElementById('${z}');\n var actualHomePagePath = window.location.pathname;\n var suggestedBaseUrl = actualHomePagePath.substr(-1) === '/'\n ? actualHomePagePath\n : actualHomePagePath + '/';\n suggestionContainer.innerHTML = suggestedBaseUrl;\n}\n`}function $(){const{siteConfig:{baseUrl:e}}=(0,g.Z)();return(0,p.jsx)(p.Fragment,{children:!l.Z.canUseDOM&&(0,p.jsx)(m.Z,{children:(0,p.jsx)("script",{children:B(e)})})})}function U(){const{siteConfig:{baseUrl:e,baseUrlIssueBanner:t}}=(0,g.Z)(),{pathname:n}=(0,u.TH)();return t&&n===e?(0,p.jsx)($,{}):null}function q(){const{siteConfig:{favicon:e,title:t,noIndex:n},i18n:{currentLocale:r,localeConfigs:o}}=(0,g.Z)(),a=(0,h.Z)(e),{htmlLang:i,direction:l}=o[r];return(0,p.jsxs)(m.Z,{children:[(0,p.jsx)("html",{lang:i,dir:l}),(0,p.jsx)("title",{children:t}),(0,p.jsx)("meta",{property:"og:title",content:t}),(0,p.jsx)("meta",{name:"viewport",content:"width=device-width, initial-scale=1.0"}),n&&(0,p.jsx)("meta",{name:"robots",content:"noindex, nofollow"}),e&&(0,p.jsx)("link",{rel:"icon",href:a})]})}var H=n(4763),Z=n(2389);function G(){const e=(0,Z.Z)();return(0,p.jsx)(m.Z,{children:(0,p.jsx)("html",{"data-has-hydrated":e})})}function V(){const e=(0,d.H)(c.Z),t=(0,u.TH)();return(0,p.jsx)(H.Z,{children:(0,p.jsx)(L.M,{children:(0,p.jsxs)(A.t,{children:[(0,p.jsxs)(f,{children:[(0,p.jsx)(q,{}),(0,p.jsx)(C,{}),(0,p.jsx)(U,{}),(0,p.jsx)(I,{location:j(t),children:e})]}),(0,p.jsx)(G,{})]})})})}var W=n(6887);const Q=function(e){try{return document.createElement("link").relList.supports(e)}catch{return!1}}("prefetch")?function(e){return new Promise(((t,n)=>{if("undefined"==typeof document)return void n();const r=document.createElement("link");r.setAttribute("rel","prefetch"),r.setAttribute("href",e),r.onload=()=>t(),r.onerror=()=>n();const o=document.getElementsByTagName("head")[0]??document.getElementsByName("script")[0]?.parentNode;o?.appendChild(r)}))}:function(e){return new Promise(((t,n)=>{const r=new XMLHttpRequest;r.open("GET",e,!0),r.withCredentials=!0,r.onload=()=>{200===r.status?t():n()},r.send(null)}))};var K=n(9670);const Y=new Set,X=new Set,J=()=>navigator.connection?.effectiveType.includes("2g")||navigator.connection?.saveData,ee={prefetch(e){if(!(e=>!J()&&!X.has(e)&&!Y.has(e))(e))return!1;Y.add(e);const t=(0,d.f)(c.Z,e).flatMap((e=>{return t=e.route.path,Object.entries(W).filter((e=>{let[n]=e;return n.replace(/-[^-]+$/,"")===t})).flatMap((e=>{let[,t]=e;return Object.values((0,K.Z)(t))}));var t}));return Promise.all(t.map((e=>{const t=n.gca(e);return t&&!t.includes("undefined")?Q(t).catch((()=>{})):Promise.resolve()})))},preload:e=>!!(e=>!J()&&!X.has(e))(e)&&(X.add(e),O(e))},te=Object.freeze(ee),ne=Boolean(!0);if(l.Z.canUseDOM){window.docusaurus=te;const e=document.getElementById("__docusaurus"),t=(0,p.jsx)(i.B6,{children:(0,p.jsx)(a.VK,{children:(0,p.jsx)(V,{})})}),n=(e,t)=>{console.error("Docusaurus React Root onRecoverableError:",e,t)},l=()=>{if(ne)r.startTransition((()=>{o.hydrateRoot(e,t,{onRecoverableError:n})}));else{const a=o.createRoot(e,{onRecoverableError:n});r.startTransition((()=>{a.render(t)}))}};O(window.location.pathname).then(l)}},8940:(e,t,n)=>{"use strict";n.d(t,{_:()=>d,M:()=>p});var r=n(7294),o=n(6809);const a=JSON.parse('{"docusaurus-plugin-content-docs":{"default":{"path":"/docs","versions":[{"name":"current","label":"Next","isLast":true,"path":"/docs","mainDocId":"about","docs":[{"id":"about","path":"/docs/about","sidebar":"tutorialSidebar"},{"id":"appendixa","path":"/docs/appendixa","sidebar":"tutorialSidebar"},{"id":"appendixb","path":"/docs/appendixb","sidebar":"tutorialSidebar"},{"id":"appendixc","path":"/docs/appendixc","sidebar":"tutorialSidebar"},{"id":"appendixd","path":"/docs/appendixd","sidebar":"tutorialSidebar"},{"id":"appendixe","path":"/docs/appendixe","sidebar":"tutorialSidebar"},{"id":"changelog","path":"/docs/changelog","sidebar":"tutorialSidebar"},{"id":"configuration/authenticationbackends","path":"/docs/configuration/authenticationbackends","sidebar":"tutorialSidebar"},{"id":"configuration/configuremessagingrepositorywebui","path":"/docs/configuration/configuremessagingrepositorywebui","sidebar":"tutorialSidebar"},{"id":"configuration/configurewebui","path":"/docs/configuration/configurewebui","sidebar":"tutorialSidebar"},{"id":"configuration/groupassignmentbyinput","path":"/docs/configuration/groupassignmentbyinput","sidebar":"tutorialSidebar"},{"id":"configuration/groupassignmentusingauth","path":"/docs/configuration/groupassignmentusingauth","sidebar":"tutorialSidebar"},{"id":"configuration/groupassignmentusingclientcerts","path":"/docs/configuration/groupassignmentusingclientcerts","sidebar":"tutorialSidebar"},{"id":"configuration/groupfiltering","path":"/docs/configuration/groupfiltering","sidebar":"tutorialSidebar"},{"id":"configuration/optionallydisableui","path":"/docs/configuration/optionallydisableui","sidebar":"tutorialSidebar"},{"id":"configuration/overview","path":"/docs/configuration/overview","sidebar":"tutorialSidebar"},{"id":"configuration/vbmadminconfig","path":"/docs/configuration/vbmadminconfig","sidebar":"tutorialSidebar"},{"id":"dataretentiontool","path":"/docs/dataretentiontool","sidebar":"tutorialSidebar"},{"id":"deviceprofiles","path":"/docs/deviceprofiles","sidebar":"tutorialSidebar"},{"id":"dockerinstall/buildinstall","path":"/docs/dockerinstall/buildinstall","sidebar":"tutorialSidebar"},{"id":"dockerinstall/ironbank","path":"/docs/dockerinstall/ironbank","sidebar":"tutorialSidebar"},{"id":"federation/datapackagemissionfileblocker","path":"/docs/federation/datapackagemissionfileblocker","sidebar":"tutorialSidebar"},{"id":"federation/enablefederation","path":"/docs/federation/enablefederation","sidebar":"tutorialSidebar"},{"id":"federation/federatedgroupmapping","path":"/docs/federation/federatedgroupmapping","sidebar":"tutorialSidebar"},{"id":"federation/federationdisruptiontolerance","path":"/docs/federation/federationdisruptiontolerance","sidebar":"tutorialSidebar"},{"id":"federation/federationexample","path":"/docs/federation/federationexample","sidebar":"tutorialSidebar"},{"id":"federation/maketheconnection","path":"/docs/federation/maketheconnection","sidebar":"tutorialSidebar"},{"id":"federation/overview","path":"/docs/federation/overview","sidebar":"tutorialSidebar"},{"id":"federation/uploadfederatecert","path":"/docs/federation/uploadfederatecert","sidebar":"tutorialSidebar"},{"id":"firewall/overview","path":"/docs/firewall/overview","sidebar":"tutorialSidebar"},{"id":"firewall/rhelrockycentos","path":"/docs/firewall/rhelrockycentos","sidebar":"tutorialSidebar"},{"id":"firewall/ubunturaspberrypi","path":"/docs/firewall/ubunturaspberrypi","sidebar":"tutorialSidebar"},{"id":"groupfilteringformulticast","path":"/docs/groupfilteringformulticast","sidebar":"tutorialSidebar"},{"id":"imagetest","path":"/docs/imagetest"},{"id":"installation/oneserver/prerequisite","path":"/docs/installation/oneserver/prerequisite","sidebar":"tutorialSidebar"},{"id":"installation/oneserver/takserverconfiguration","path":"/docs/installation/oneserver/takserverconfiguration","sidebar":"tutorialSidebar"},{"id":"installation/oneserver/takserverinstallation","path":"/docs/installation/oneserver/takserverinstallation","sidebar":"tutorialSidebar"},{"id":"installation/overview","path":"/docs/installation/overview","sidebar":"tutorialSidebar"},{"id":"installation/setup_wizard","path":"/docs/installation/setup_wizard","sidebar":"tutorialSidebar"},{"id":"installation/twoserver/overview","path":"/docs/installation/twoserver/overview","sidebar":"tutorialSidebar"},{"id":"installation/twoserver/serverone/dependencysetup","path":"/docs/installation/twoserver/serverone/dependencysetup","sidebar":"tutorialSidebar"},{"id":"installation/twoserver/servertwo/configuretakserver","path":"/docs/installation/twoserver/servertwo/configuretakserver","sidebar":"tutorialSidebar"},{"id":"installation/twoserver/servertwo/installtakserver","path":"/docs/installation/twoserver/servertwo/installtakserver","sidebar":"tutorialSidebar"},{"id":"installation/twoserver/servertwo/prerequisites","path":"/docs/installation/twoserver/servertwo/prerequisites","sidebar":"tutorialSidebar"},{"id":"logging","path":"/docs/logging","sidebar":"tutorialSidebar"},{"id":"metrics","path":"/docs/metrics","sidebar":"tutorialSidebar"},{"id":"oath2authentication","path":"/docs/oath2authentication","sidebar":"tutorialSidebar"},{"id":"softwareinstallationlocation","path":"/docs/softwareinstallationlocation","sidebar":"tutorialSidebar"},{"id":"system-requirements/awsrequirements","path":"/docs/system-requirements/awsrequirements","sidebar":"tutorialSidebar"},{"id":"system-requirements/serverrequirements","path":"/docs/system-requirements/serverrequirements","sidebar":"tutorialSidebar"},{"id":"system-requirements/systemrequirements","path":"/docs/system-requirements/systemrequirements","sidebar":"tutorialSidebar"},{"id":"upgrade/overview","path":"/docs/upgrade/overview","sidebar":"tutorialSidebar"},{"id":"upgrade/singleserver","path":"/docs/upgrade/singleserver","sidebar":"tutorialSidebar"},{"id":"upgrade/twoserver","path":"/docs/upgrade/twoserver","sidebar":"tutorialSidebar"},{"id":"usermanagementui","path":"/docs/usermanagementui","sidebar":"tutorialSidebar"},{"id":"webtak","path":"/docs/webtak","sidebar":"tutorialSidebar"}],"draftIds":[],"sidebars":{"tutorialSidebar":{"link":{"path":"/docs/about","label":"about"}}}}],"breadcrumbs":true}}}'),i=JSON.parse('{"defaultLocale":"en","locales":["en"],"path":"i18n","currentLocale":"en","localeConfigs":{"en":{"label":"English","direction":"ltr","htmlLang":"en","calendar":"gregory","path":"en"}}}');var l=n(7529);const s=JSON.parse('{"docusaurusVersion":"3.1.1","siteVersion":"0.0.2","pluginVersions":{"docusaurus-plugin-content-docs":{"type":"package","name":"@docusaurus/plugin-content-docs","version":"3.1.1"},"docusaurus-plugin-content-blog":{"type":"package","name":"@docusaurus/plugin-content-blog","version":"3.1.1"},"docusaurus-plugin-content-pages":{"type":"package","name":"@docusaurus/plugin-content-pages","version":"3.1.1"},"docusaurus-plugin-sitemap":{"type":"package","name":"@docusaurus/plugin-sitemap","version":"3.1.1"},"docusaurus-theme-classic":{"type":"package","name":"@docusaurus/theme-classic","version":"3.1.1"}}}');var c=n(5893);const u={siteConfig:o.default,siteMetadata:s,globalData:a,i18n:i,codeTranslations:l},d=r.createContext(u);function p(e){let{children:t}=e;return(0,c.jsx)(d.Provider,{value:u,children:t})}},4763:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(7294),o=n(412),a=n(5742),i=n(8780),l=n(6040),s=n(5893);function c(e){let{error:t,tryAgain:n}=e;return(0,s.jsxs)("div",{style:{display:"flex",flexDirection:"column",justifyContent:"center",alignItems:"flex-start",minHeight:"100vh",width:"100%",maxWidth:"80ch",fontSize:"20px",margin:"0 auto",padding:"1rem"},children:[(0,s.jsx)("h1",{style:{fontSize:"3rem"},children:"This page crashed"}),(0,s.jsx)("button",{type:"button",onClick:n,style:{margin:"1rem 0",fontSize:"2rem",cursor:"pointer",borderRadius:20,padding:"1rem"},children:"Try again"}),(0,s.jsx)(u,{error:t})]})}function u(e){let{error:t}=e;const n=(0,i.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,s.jsx)("p",{style:{whiteSpace:"pre-wrap"},children:n})}function d(e){let{error:t,tryAgain:n}=e;return(0,s.jsxs)(f,{fallback:()=>(0,s.jsx)(c,{error:t,tryAgain:n}),children:[(0,s.jsx)(a.Z,{children:(0,s.jsx)("title",{children:"Page Error"})}),(0,s.jsx)(l.Z,{children:(0,s.jsx)(c,{error:t,tryAgain:n})})]})}const p=e=>(0,s.jsx)(d,{...e});class f extends r.Component{constructor(e){super(e),this.state={error:null}}componentDidCatch(e){o.Z.canUseDOM&&this.setState({error:e})}render(){const{children:e}=this.props,{error:t}=this.state;if(t){const e={error:t,tryAgain:()=>this.setState({error:null})};return(this.props.fallback??p)(e)}return e??null}}},412:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r="undefined"!=typeof window&&"document"in window&&"createElement"in window.document,o={canUseDOM:r,canUseEventListeners:r&&("addEventListener"in window||"attachEvent"in window),canUseIntersectionObserver:r&&"IntersectionObserver"in window,canUseViewport:r&&"screen"in window}},5742:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);var r=n(405),o=n(5893);function a(e){return(0,o.jsx)(r.ql,{...e})}},3692:(e,t,n)=>{"use strict";n.d(t,{Z:()=>f});var r=n(7294),o=n(3727),a=n(8780),i=n(2263),l=n(3919),s=n(412),c=n(8138),u=n(4996),d=n(5893);function p(e,t){let{isNavLink:n,to:p,href:f,activeClassName:m,isActive:g,"data-noBrokenLinkCheck":h,autoAddBaseUrl:b=!0,...y}=e;const{siteConfig:{trailingSlash:v,baseUrl:w}}=(0,i.Z)(),{withBaseUrl:k}=(0,u.C)(),x=(0,c.Z)(),S=(0,r.useRef)(null);(0,r.useImperativeHandle)(t,(()=>S.current));const E=p||f;const _=(0,l.Z)(E),C=E?.replace("pathname://","");let T=void 0!==C?(j=C,b&&(e=>e.startsWith("/"))(j)?k(j):j):void 0;var j;T&&_&&(T=(0,a.applyTrailingSlash)(T,{trailingSlash:v,baseUrl:w}));const A=(0,r.useRef)(!1),L=n?o.OL:o.rU,N=s.Z.canUseIntersectionObserver,R=(0,r.useRef)(),P=()=>{A.current||null==T||(window.docusaurus.preload(T),A.current=!0)};(0,r.useEffect)((()=>(!N&&_&&null!=T&&window.docusaurus.prefetch(T),()=>{N&&R.current&&R.current.disconnect()})),[R,T,N,_]);const O=T?.startsWith("#")??!1,D=!y.target||"_self"===y.target,I=!T||!_||!D||O;return h||!O&&I||x.collectLink(T),y.id&&x.collectAnchor(y.id),I?(0,d.jsx)("a",{ref:S,href:T,...E&&!_&&{target:"_blank",rel:"noopener noreferrer"},...y}):(0,d.jsx)(L,{...y,onMouseEnter:P,onTouchStart:P,innerRef:e=>{S.current=e,N&&e&&_&&(R.current=new window.IntersectionObserver((t=>{t.forEach((t=>{e===t.target&&(t.isIntersecting||t.intersectionRatio>0)&&(R.current.unobserve(e),R.current.disconnect(),null!=T&&window.docusaurus.prefetch(T))}))})),R.current.observe(e))},to:T,...n&&{isActive:g,activeClassName:m}})}const f=r.forwardRef(p)},1875:(e,t,n)=>{"use strict";n.d(t,{Z:()=>r});const r=()=>null},5999:(e,t,n)=>{"use strict";n.d(t,{Z:()=>c,I:()=>s});var r=n(7294),o=n(5893);function a(e,t){const n=e.split(/(\{\w+\})/).map(((e,n)=>{if(n%2==1){const n=t?.[e.slice(1,-1)];if(void 0!==n)return n}return e}));return n.some((e=>(0,r.isValidElement)(e)))?n.map(((e,t)=>(0,r.isValidElement)(e)?r.cloneElement(e,{key:t}):e)).filter((e=>""!==e)):n.join("")}var i=n(7529);function l(e){let{id:t,message:n}=e;if(void 0===t&&void 0===n)throw new Error("Docusaurus translation declarations must have at least a translation id or a default translation message");return i[t??n]??n??t}function s(e,t){let{message:n,id:r}=e;return a(l({message:n,id:r}),t)}function c(e){let{children:t,id:n,values:r}=e;if(t&&"string"!=typeof t)throw console.warn("Illegal <Translate> children",t),new Error("The Docusaurus <Translate> component only accept simple string values");const i=l({message:t,id:n});return(0,o.jsx)(o.Fragment,{children:a(i,r)})}},9935:(e,t,n)=>{"use strict";n.d(t,{m:()=>r});const r="default"},3919:(e,t,n)=>{"use strict";function r(e){return/^(?:\w*:|\/\/)/.test(e)}function o(e){return void 0!==e&&!r(e)}n.d(t,{Z:()=>o,b:()=>r})},4996:(e,t,n)=>{"use strict";n.d(t,{C:()=>i,Z:()=>l});var r=n(7294),o=n(2263),a=n(3919);function i(){const{siteConfig:{baseUrl:e,url:t}}=(0,o.Z)(),n=(0,r.useCallback)(((n,r)=>function(e,t,n,r){let{forcePrependBaseUrl:o=!1,absolute:i=!1}=void 0===r?{}:r;if(!n||n.startsWith("#")||(0,a.b)(n))return n;if(o)return t+n.replace(/^\//,"");if(n===t.replace(/\/$/,""))return t;const l=n.startsWith(t)?n:t+n.replace(/^\//,"");return i?e+l:l}(t,e,n,r)),[t,e]);return{withBaseUrl:n}}function l(e,t){void 0===t&&(t={});const{withBaseUrl:n}=i();return n(e,t)}},8138:(e,t,n)=>{"use strict";n.d(t,{Z:()=>i});var r=n(7294);n(5893);const o=r.createContext({collectAnchor:()=>{},collectLink:()=>{}}),a=()=>(0,r.useContext)(o);function i(){return a()}},2263:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(8940);function a(){return(0,r.useContext)(o._)}},2389:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=n(7294),o=n(8934);function a(){return(0,r.useContext)(o._)}},469:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});var r=n(7294);const o=n(412).Z.canUseDOM?r.useLayoutEffect:r.useEffect},9670:(e,t,n)=>{"use strict";n.d(t,{Z:()=>o});const r=e=>"object"==typeof e&&!!e&&Object.keys(e).length>0;function o(e){const t={};return function e(n,o){Object.entries(n).forEach((n=>{let[a,i]=n;const l=o?`${o}.${a}`:a;r(i)?e(i,l):t[l]=i}))}(e),t}},226:(e,t,n)=>{"use strict";n.d(t,{_:()=>a,z:()=>i});var r=n(7294),o=n(5893);const a=r.createContext(null);function i(e){let{children:t,value:n}=e;const i=r.useContext(a),l=(0,r.useMemo)((()=>function(e){let{parent:t,value:n}=e;if(!t){if(!n)throw new Error("Unexpected: no Docusaurus route context found");if(!("plugin"in n))throw new Error("Unexpected: Docusaurus topmost route context has no `plugin` attribute");return n}const r={...t.data,...n?.data};return{plugin:t.plugin,data:r}}({parent:i,value:n})),[i,n]);return(0,o.jsx)(a.Provider,{value:l,children:t})}},143:(e,t,n)=>{"use strict";n.d(t,{Iw:()=>g,gA:()=>p,_r:()=>u,Jo:()=>h,zh:()=>d,yW:()=>m,gB:()=>f});var r=n(6550),o=n(2263),a=n(9935);function i(e,t){void 0===t&&(t={});const n=function(){const{globalData:e}=(0,o.Z)();return e}()[e];if(!n&&t.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin.`);return n}const l=e=>e.versions.find((e=>e.isLast));function s(e,t){const n=function(e,t){const n=l(e);return[...e.versions.filter((e=>e!==n)),n].find((e=>!!(0,r.LX)(t,{path:e.path,exact:!1,strict:!1})))}(e,t),o=n?.docs.find((e=>!!(0,r.LX)(t,{path:e.path,exact:!0,strict:!1})));return{activeVersion:n,activeDoc:o,alternateDocVersions:o?function(t){const n={};return e.versions.forEach((e=>{e.docs.forEach((r=>{r.id===t&&(n[e.name]=r)}))})),n}(o.id):{}}}const c={},u=()=>i("docusaurus-plugin-content-docs")??c,d=e=>function(e,t,n){void 0===t&&(t=a.m),void 0===n&&(n={});const r=i(e),o=r?.[t];if(!o&&n.failfast)throw new Error(`Docusaurus plugin global data not found for "${e}" plugin with id "${t}".`);return o}("docusaurus-plugin-content-docs",e,{failfast:!0});function p(e){void 0===e&&(e={});const t=u(),{pathname:n}=(0,r.TH)();return function(e,t,n){void 0===n&&(n={});const o=Object.entries(e).sort(((e,t)=>t[1].path.localeCompare(e[1].path))).find((e=>{let[,n]=e;return!!(0,r.LX)(t,{path:n.path,exact:!1,strict:!1})})),a=o?{pluginId:o[0],pluginData:o[1]}:void 0;if(!a&&n.failfast)throw new Error(`Can't find active docs plugin for "${t}" pathname, while it was expected to be found. Maybe you tried to use a docs feature that can only be used on a docs-related page? Existing docs plugin paths are: ${Object.values(e).map((e=>e.path)).join(", ")}`);return a}(t,n,e)}function f(e){return d(e).versions}function m(e){const t=d(e);return l(t)}function g(e){const t=d(e),{pathname:n}=(0,r.TH)();return s(t,n)}function h(e){const t=d(e),{pathname:n}=(0,r.TH)();return function(e,t){const n=l(e);return{latestDocSuggestion:s(e,t).alternateDocVersions[n.name],latestVersionSuggestion:n}}(t,n)}},8320:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>a});var r=n(4865),o=n.n(r);o().configure({showSpinner:!1});const a={onRouteUpdate(e){let{location:t,previousLocation:n}=e;if(n&&t.pathname!==n.pathname){const e=window.setTimeout((()=>{o().start()}),200);return()=>window.clearTimeout(e)}},onRouteDidUpdate(){o().done()}}},3310:(e,t,n)=>{"use strict";n.r(t);var r=n(2573),o=n(6809);!function(e){const{themeConfig:{prism:t}}=o.default,{additionalLanguages:r}=t;globalThis.Prism=e,r.forEach((e=>{"php"===e&&n(6854),n(6726)(`./prism-${e}`)})),delete globalThis.Prism}(r.p1)},2503:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(7294);var r=n(512),o=n(5999),a=n(6668),i=n(3692),l=n(8138);const s={anchorWithStickyNavbar:"anchorWithStickyNavbar_LWe7",anchorWithHideOnScrollNavbar:"anchorWithHideOnScrollNavbar_WYt5"};var c=n(5893);function u(e){let{as:t,id:n,...u}=e;const d=(0,l.Z)(),{navbar:{hideOnScroll:p}}=(0,a.L)();if("h1"===t||!n)return(0,c.jsx)(t,{...u,id:void 0});d.collectAnchor(n);const f=(0,o.I)({id:"theme.common.headingLinkTitle",message:"Direct link to {heading}",description:"Title for link to heading"},{heading:"string"==typeof u.children?u.children:n});return(0,c.jsxs)(t,{...u,className:(0,r.Z)("anchor",p?s.anchorWithHideOnScrollNavbar:s.anchorWithStickyNavbar,u.className),id:n,children:[u.children,(0,c.jsx)(i.Z,{className:"hash-link",to:`#${n}`,"aria-label":f,title:f,children:"\u200b"})]})}},9471:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);const r={iconExternalLink:"iconExternalLink_nPIU"};var o=n(5893);function a(e){let{width:t=13.5,height:n=13.5}=e;return(0,o.jsx)("svg",{width:t,height:n,"aria-hidden":"true",viewBox:"0 0 24 24",className:r.iconExternalLink,children:(0,o.jsx)("path",{fill:"currentColor",d:"M21 13v10h-21v-19h12v2h-10v15h17v-8h2zm3-12h-10.988l4.035 4-6.977 7.07 2.828 2.828 6.977-7.07 4.125 4.172v-11z"})})}},6040:(e,t,n)=>{"use strict";n.d(t,{Z:()=>ft});var r=n(7294),o=n(512),a=n(4763),i=n(1944),l=n(6550),s=n(5999),c=n(5936),u=n(5893);const d="__docusaurus_skipToContent_fallback";function p(e){e.setAttribute("tabindex","-1"),e.focus(),e.removeAttribute("tabindex")}function f(){const e=(0,r.useRef)(null),{action:t}=(0,l.k6)(),n=(0,r.useCallback)((e=>{e.preventDefault();const t=document.querySelector("main:first-of-type")??document.getElementById(d);t&&p(t)}),[]);return(0,c.S)((n=>{let{location:r}=n;e.current&&!r.hash&&"PUSH"===t&&p(e.current)})),{containerRef:e,onClick:n}}const m=(0,s.I)({id:"theme.common.skipToMainContent",description:"The skip to content label used for accessibility, allowing to rapidly navigate to main content with keyboard tab/enter navigation",message:"Skip to main content"});function g(e){const t=e.children??m,{containerRef:n,onClick:r}=f();return(0,u.jsx)("div",{ref:n,role:"region","aria-label":m,children:(0,u.jsx)("a",{...e,href:`#${d}`,onClick:r,children:t})})}var h=n(5281),b=n(9727);const y={skipToContent:"skipToContent_fXgn"};function v(){return(0,u.jsx)(g,{className:y.skipToContent})}var w=n(6668),k=n(9689);function x(e){let{width:t=21,height:n=21,color:r="currentColor",strokeWidth:o=1.2,className:a,...i}=e;return(0,u.jsx)("svg",{viewBox:"0 0 15 15",width:t,height:n,...i,children:(0,u.jsx)("g",{stroke:r,strokeWidth:o,children:(0,u.jsx)("path",{d:"M.75.75l13.5 13.5M14.25.75L.75 14.25"})})})}const S={closeButton:"closeButton_CVFx"};function E(e){return(0,u.jsx)("button",{type:"button","aria-label":(0,s.I)({id:"theme.AnnouncementBar.closeButtonAriaLabel",message:"Close",description:"The ARIA label for close button of announcement bar"}),...e,className:(0,o.Z)("clean-btn close",S.closeButton,e.className),children:(0,u.jsx)(x,{width:14,height:14,strokeWidth:3.1})})}const _={content:"content_knG7"};function C(e){const{announcementBar:t}=(0,w.L)(),{content:n}=t;return(0,u.jsx)("div",{...e,className:(0,o.Z)(_.content,e.className),dangerouslySetInnerHTML:{__html:n}})}const T={announcementBar:"announcementBar_mb4j",announcementBarPlaceholder:"announcementBarPlaceholder_vyr4",announcementBarClose:"announcementBarClose_gvF7",announcementBarContent:"announcementBarContent_xLdY"};function j(){const{announcementBar:e}=(0,w.L)(),{isActive:t,close:n}=(0,k.nT)();if(!t)return null;const{backgroundColor:r,textColor:o,isCloseable:a}=e;return(0,u.jsxs)("div",{className:T.announcementBar,style:{backgroundColor:r,color:o},role:"banner",children:[a&&(0,u.jsx)("div",{className:T.announcementBarPlaceholder}),(0,u.jsx)(C,{className:T.announcementBarContent}),a&&(0,u.jsx)(E,{onClick:n,className:T.announcementBarClose})]})}var A=n(2961),L=n(2466);var N=n(902),R=n(3102);const P=r.createContext(null);function O(e){let{children:t}=e;const n=function(){const e=(0,A.e)(),t=(0,R.HY)(),[n,o]=(0,r.useState)(!1),a=null!==t.component,i=(0,N.D9)(a);return(0,r.useEffect)((()=>{a&&!i&&o(!0)}),[a,i]),(0,r.useEffect)((()=>{a?e.shown||o(!0):o(!1)}),[e.shown,a]),(0,r.useMemo)((()=>[n,o]),[n])}();return(0,u.jsx)(P.Provider,{value:n,children:t})}function D(e){if(e.component){const t=e.component;return(0,u.jsx)(t,{...e.props})}}function I(){const e=(0,r.useContext)(P);if(!e)throw new N.i6("NavbarSecondaryMenuDisplayProvider");const[t,n]=e,o=(0,r.useCallback)((()=>n(!1)),[n]),a=(0,R.HY)();return(0,r.useMemo)((()=>({shown:t,hide:o,content:D(a)})),[o,a,t])}function F(e){let{header:t,primaryMenu:n,secondaryMenu:r}=e;const{shown:a}=I();return(0,u.jsxs)("div",{className:"navbar-sidebar",children:[t,(0,u.jsxs)("div",{className:(0,o.Z)("navbar-sidebar__items",{"navbar-sidebar__items--show-secondary":a}),children:[(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:n}),(0,u.jsx)("div",{className:"navbar-sidebar__item menu",children:r})]})]})}var M=n(2949),z=n(2389);function B(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12,9c1.65,0,3,1.35,3,3s-1.35,3-3,3s-3-1.35-3-3S10.35,9,12,9 M12,7c-2.76,0-5,2.24-5,5s2.24,5,5,5s5-2.24,5-5 S14.76,7,12,7L12,7z M2,13l2,0c0.55,0,1-0.45,1-1s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S1.45,13,2,13z M20,13l2,0c0.55,0,1-0.45,1-1 s-0.45-1-1-1l-2,0c-0.55,0-1,0.45-1,1S19.45,13,20,13z M11,2v2c0,0.55,0.45,1,1,1s1-0.45,1-1V2c0-0.55-0.45-1-1-1S11,1.45,11,2z M11,20v2c0,0.55,0.45,1,1,1s1-0.45,1-1v-2c0-0.55-0.45-1-1-1C11.45,19,11,19.45,11,20z M5.99,4.58c-0.39-0.39-1.03-0.39-1.41,0 c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0s0.39-1.03,0-1.41L5.99,4.58z M18.36,16.95 c-0.39-0.39-1.03-0.39-1.41,0c-0.39,0.39-0.39,1.03,0,1.41l1.06,1.06c0.39,0.39,1.03,0.39,1.41,0c0.39-0.39,0.39-1.03,0-1.41 L18.36,16.95z M19.42,5.99c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06c-0.39,0.39-0.39,1.03,0,1.41 s1.03,0.39,1.41,0L19.42,5.99z M7.05,18.36c0.39-0.39,0.39-1.03,0-1.41c-0.39-0.39-1.03-0.39-1.41,0l-1.06,1.06 c-0.39,0.39-0.39,1.03,0,1.41s1.03,0.39,1.41,0L7.05,18.36z"})})}function $(e){return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:24,height:24,...e,children:(0,u.jsx)("path",{fill:"currentColor",d:"M9.37,5.51C9.19,6.15,9.1,6.82,9.1,7.5c0,4.08,3.32,7.4,7.4,7.4c0.68,0,1.35-0.09,1.99-0.27C17.45,17.19,14.93,19,12,19 c-3.86,0-7-3.14-7-7C5,9.07,6.81,6.55,9.37,5.51z M12,3c-4.97,0-9,4.03-9,9s4.03,9,9,9s9-4.03,9-9c0-0.46-0.04-0.92-0.1-1.36 c-0.98,1.37-2.58,2.26-4.4,2.26c-2.98,0-5.4-2.42-5.4-5.4c0-1.81,0.89-3.42,2.26-4.4C12.92,3.04,12.46,3,12,3L12,3z"})})}const U={toggle:"toggle_vylO",toggleButton:"toggleButton_gllP",darkToggleIcon:"darkToggleIcon_wfgR",lightToggleIcon:"lightToggleIcon_pyhR",toggleButtonDisabled:"toggleButtonDisabled_aARS"};function q(e){let{className:t,buttonClassName:n,value:r,onChange:a}=e;const i=(0,z.Z)(),l=(0,s.I)({message:"Switch between dark and light mode (currently {mode})",id:"theme.colorToggle.ariaLabel",description:"The ARIA label for the navbar color mode toggle"},{mode:"dark"===r?(0,s.I)({message:"dark mode",id:"theme.colorToggle.ariaLabel.mode.dark",description:"The name for the dark color mode"}):(0,s.I)({message:"light mode",id:"theme.colorToggle.ariaLabel.mode.light",description:"The name for the light color mode"})});return(0,u.jsx)("div",{className:(0,o.Z)(U.toggle,t),children:(0,u.jsxs)("button",{className:(0,o.Z)("clean-btn",U.toggleButton,!i&&U.toggleButtonDisabled,n),type:"button",onClick:()=>a("dark"===r?"light":"dark"),disabled:!i,title:l,"aria-label":l,"aria-live":"polite",children:[(0,u.jsx)(B,{className:(0,o.Z)(U.toggleIcon,U.lightToggleIcon)}),(0,u.jsx)($,{className:(0,o.Z)(U.toggleIcon,U.darkToggleIcon)})]})})}const H=r.memo(q),Z={darkNavbarColorModeToggle:"darkNavbarColorModeToggle_X3D1"};function G(e){let{className:t}=e;const n=(0,w.L)().navbar.style,r=(0,w.L)().colorMode.disableSwitch,{colorMode:o,setColorMode:a}=(0,M.I)();return r?null:(0,u.jsx)(H,{className:t,buttonClassName:"dark"===n?Z.darkNavbarColorModeToggle:void 0,value:o,onChange:a})}var V=n(1327);function W(){return(0,u.jsx)(V.Z,{className:"navbar__brand",imageClassName:"navbar__logo",titleClassName:"navbar__title text--truncate"})}function Q(){const e=(0,A.e)();return(0,u.jsx)("button",{type:"button","aria-label":(0,s.I)({id:"theme.docs.sidebar.closeSidebarButtonAriaLabel",message:"Close navigation bar",description:"The ARIA label for close button of mobile sidebar"}),className:"clean-btn navbar-sidebar__close",onClick:()=>e.toggle(),children:(0,u.jsx)(x,{color:"var(--ifm-color-emphasis-600)"})})}function K(){return(0,u.jsxs)("div",{className:"navbar-sidebar__brand",children:[(0,u.jsx)(W,{}),(0,u.jsx)(G,{className:"margin-right--md"}),(0,u.jsx)(Q,{})]})}var Y=n(3692),X=n(4996),J=n(3919);function ee(e,t){return void 0!==e&&void 0!==t&&new RegExp(e,"gi").test(t)}var te=n(9471);function ne(e){let{activeBasePath:t,activeBaseRegex:n,to:r,href:o,label:a,html:i,isDropdownLink:l,prependBaseUrlToHref:s,...c}=e;const d=(0,X.Z)(r),p=(0,X.Z)(t),f=(0,X.Z)(o,{forcePrependBaseUrl:!0}),m=a&&o&&!(0,J.Z)(o),g=i?{dangerouslySetInnerHTML:{__html:i}}:{children:(0,u.jsxs)(u.Fragment,{children:[a,m&&(0,u.jsx)(te.Z,{...l&&{width:12,height:12}})]})};return o?(0,u.jsx)(Y.Z,{href:s?f:o,...c,...g}):(0,u.jsx)(Y.Z,{to:d,isNavLink:!0,...(t||n)&&{isActive:(e,t)=>n?ee(n,t.pathname):t.pathname.startsWith(p)},...c,...g})}function re(e){let{className:t,isDropdownItem:n=!1,...r}=e;const a=(0,u.jsx)(ne,{className:(0,o.Z)(n?"dropdown__link":"navbar__item navbar__link",t),isDropdownLink:n,...r});return n?(0,u.jsx)("li",{children:a}):a}function oe(e){let{className:t,isDropdownItem:n,...r}=e;return(0,u.jsx)("li",{className:"menu__list-item",children:(0,u.jsx)(ne,{className:(0,o.Z)("menu__link",t),...r})})}function ae(e){let{mobile:t=!1,position:n,...r}=e;const o=t?oe:re;return(0,u.jsx)(o,{...r,activeClassName:r.activeClassName??(t?"menu__link--active":"navbar__link--active")})}var ie=n(6043),le=n(8596),se=n(2263);const ce={dropdownNavbarItemMobile:"dropdownNavbarItemMobile_S0Fm"};function ue(e,t){return e.some((e=>function(e,t){return!!(0,le.Mg)(e.to,t)||!!ee(e.activeBaseRegex,t)||!(!e.activeBasePath||!t.startsWith(e.activeBasePath))}(e,t)))}function de(e){let{items:t,position:n,className:a,onClick:i,...l}=e;const s=(0,r.useRef)(null),[c,d]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{const e=e=>{s.current&&!s.current.contains(e.target)&&d(!1)};return document.addEventListener("mousedown",e),document.addEventListener("touchstart",e),document.addEventListener("focusin",e),()=>{document.removeEventListener("mousedown",e),document.removeEventListener("touchstart",e),document.removeEventListener("focusin",e)}}),[s]),(0,u.jsxs)("div",{ref:s,className:(0,o.Z)("navbar__item","dropdown","dropdown--hoverable",{"dropdown--right":"right"===n,"dropdown--show":c}),children:[(0,u.jsx)(ne,{"aria-haspopup":"true","aria-expanded":c,role:"button",href:l.to?void 0:"#",className:(0,o.Z)("navbar__link",a),...l,onClick:l.to?void 0:e=>e.preventDefault(),onKeyDown:e=>{"Enter"===e.key&&(e.preventDefault(),d(!c))},children:l.children??l.label}),(0,u.jsx)("ul",{className:"dropdown__menu",children:t.map(((e,t)=>(0,r.createElement)(_e,{isDropdownItem:!0,activeClassName:"dropdown__link--active",...e,key:t})))})]})}function pe(e){let{items:t,className:n,position:a,onClick:i,...s}=e;const c=function(){const{siteConfig:{baseUrl:e}}=(0,se.Z)(),{pathname:t}=(0,l.TH)();return t.replace(e,"/")}(),d=ue(t,c),{collapsed:p,toggleCollapsed:f,setCollapsed:m}=(0,ie.u)({initialState:()=>!d});return(0,r.useEffect)((()=>{d&&m(!d)}),[c,d,m]),(0,u.jsxs)("li",{className:(0,o.Z)("menu__list-item",{"menu__list-item--collapsed":p}),children:[(0,u.jsx)(ne,{role:"button",className:(0,o.Z)(ce.dropdownNavbarItemMobile,"menu__link menu__link--sublist menu__link--sublist-caret",n),...s,onClick:e=>{e.preventDefault(),f()},children:s.children??s.label}),(0,u.jsx)(ie.z,{lazy:!0,as:"ul",className:"menu__list",collapsed:p,children:t.map(((e,t)=>(0,r.createElement)(_e,{mobile:!0,isDropdownItem:!0,onClick:i,activeClassName:"menu__link--active",...e,key:t})))})]})}function fe(e){let{mobile:t=!1,...n}=e;const r=t?pe:de;return(0,u.jsx)(r,{...n})}var me=n(4711);function ge(e){let{width:t=20,height:n=20,...r}=e;return(0,u.jsx)("svg",{viewBox:"0 0 24 24",width:t,height:n,"aria-hidden":!0,...r,children:(0,u.jsx)("path",{fill:"currentColor",d:"M12.87 15.07l-2.54-2.51.03-.03c1.74-1.94 2.98-4.17 3.71-6.53H17V4h-7V2H8v2H1v1.99h11.17C11.5 7.92 10.44 9.75 9 11.35 8.07 10.32 7.3 9.19 6.69 8h-2c.73 1.63 1.73 3.17 2.98 4.56l-5.09 5.02L4 19l5-5 3.11 3.11.76-2.04zM18.5 10h-2L12 22h2l1.12-3h4.75L21 22h2l-4.5-12zm-2.62 7l1.62-4.33L19.12 17h-3.24z"})})}const he="iconLanguage_nlXk";var be=n(1875);const ye={navbarSearchContainer:"navbarSearchContainer_Bca1"};function ve(e){let{children:t,className:n}=e;return(0,u.jsx)("div",{className:(0,o.Z)(n,ye.navbarSearchContainer),children:t})}var we=n(143),ke=n(2802);var xe=n(373);const Se=e=>e.docs.find((t=>t.id===e.mainDocId));const Ee={default:ae,localeDropdown:function(e){let{mobile:t,dropdownItemsBefore:n,dropdownItemsAfter:r,queryString:o="",...a}=e;const{i18n:{currentLocale:i,locales:c,localeConfigs:d}}=(0,se.Z)(),p=(0,me.l)(),{search:f,hash:m}=(0,l.TH)(),g=[...n,...c.map((e=>{const n=`${`pathname://${p.createUrl({locale:e,fullyQualified:!1})}`}${f}${m}${o}`;return{label:d[e].label,lang:d[e].htmlLang,to:n,target:"_self",autoAddBaseUrl:!1,className:e===i?t?"menu__link--active":"dropdown__link--active":""}})),...r],h=t?(0,s.I)({message:"Languages",id:"theme.navbar.mobileLanguageDropdown.label",description:"The label for the mobile language switcher dropdown"}):d[i].label;return(0,u.jsx)(fe,{...a,mobile:t,label:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(ge,{className:he}),h]}),items:g})},search:function(e){let{mobile:t,className:n}=e;return t?null:(0,u.jsx)(ve,{className:n,children:(0,u.jsx)(be.Z,{})})},dropdown:fe,html:function(e){let{value:t,className:n,mobile:r=!1,isDropdownItem:a=!1}=e;const i=a?"li":"div";return(0,u.jsx)(i,{className:(0,o.Z)({navbar__item:!r&&!a,"menu__list-item":r},n),dangerouslySetInnerHTML:{__html:t}})},doc:function(e){let{docId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,we.Iw)(r),i=(0,ke.vY)(t,r),l=a?.path===i?.path;return null===i||i.unlisted&&!l?null:(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>l||!!a?.sidebar&&a.sidebar===i.sidebar,label:n??i.id,to:i.path})},docSidebar:function(e){let{sidebarId:t,label:n,docsPluginId:r,...o}=e;const{activeDoc:a}=(0,we.Iw)(r),i=(0,ke.oz)(t,r).link;if(!i)throw new Error(`DocSidebarNavbarItem: Sidebar with ID "${t}" doesn't have anything to be linked to.`);return(0,u.jsx)(ae,{exact:!0,...o,isActive:()=>a?.sidebar===t,label:n??i.label,to:i.path})},docsVersion:function(e){let{label:t,to:n,docsPluginId:r,...o}=e;const a=(0,ke.lO)(r)[0],i=t??a.label,l=n??(e=>e.docs.find((t=>t.id===e.mainDocId)))(a).path;return(0,u.jsx)(ae,{...o,label:i,to:l})},docsVersionDropdown:function(e){let{mobile:t,docsPluginId:n,dropdownActiveClassDisabled:r,dropdownItemsBefore:o,dropdownItemsAfter:a,...i}=e;const{search:c,hash:d}=(0,l.TH)(),p=(0,we.Iw)(n),f=(0,we.gB)(n),{savePreferredVersionName:m}=(0,xe.J)(n),g=[...o,...f.map((e=>{const t=p.alternateDocVersions[e.name]??Se(e);return{label:e.label,to:`${t.path}${c}${d}`,isActive:()=>e===p.activeVersion,onClick:()=>m(e.name)}})),...a],h=(0,ke.lO)(n)[0],b=t&&g.length>1?(0,s.I)({id:"theme.navbar.mobileVersionsDropdown.label",message:"Versions",description:"The label for the navbar versions dropdown on mobile view"}):h.label,y=t&&g.length>1?void 0:Se(h).path;return g.length<=1?(0,u.jsx)(ae,{...i,mobile:t,label:b,to:y,isActive:r?()=>!1:void 0}):(0,u.jsx)(fe,{...i,mobile:t,label:b,to:y,items:g,isActive:r?()=>!1:void 0})}};function _e(e){let{type:t,...n}=e;const r=function(e,t){return e&&"default"!==e?e:"items"in t?"dropdown":"default"}(t,n),o=Ee[r];if(!o)throw new Error(`No NavbarItem component found for type "${t}".`);return(0,u.jsx)(o,{...n})}function Ce(){const e=(0,A.e)(),t=(0,w.L)().navbar.items;return(0,u.jsx)("ul",{className:"menu__list",children:t.map(((t,n)=>(0,r.createElement)(_e,{mobile:!0,...t,onClick:()=>e.toggle(),key:n})))})}function Te(e){return(0,u.jsx)("button",{...e,type:"button",className:"clean-btn navbar-sidebar__back",children:(0,u.jsx)(s.Z,{id:"theme.navbar.mobileSidebarSecondaryMenu.backButtonLabel",description:"The label of the back button to return to main menu, inside the mobile navbar sidebar secondary menu (notably used to display the docs sidebar)",children:"\u2190 Back to main menu"})})}function je(){const e=0===(0,w.L)().navbar.items.length,t=I();return(0,u.jsxs)(u.Fragment,{children:[!e&&(0,u.jsx)(Te,{onClick:()=>t.hide()}),t.content]})}function Ae(){const e=(0,A.e)();var t;return void 0===(t=e.shown)&&(t=!0),(0,r.useEffect)((()=>(document.body.style.overflow=t?"hidden":"visible",()=>{document.body.style.overflow="visible"})),[t]),e.shouldRender?(0,u.jsx)(F,{header:(0,u.jsx)(K,{}),primaryMenu:(0,u.jsx)(Ce,{}),secondaryMenu:(0,u.jsx)(je,{})}):null}const Le={navbarHideable:"navbarHideable_m1mJ",navbarHidden:"navbarHidden_jGov"};function Ne(e){return(0,u.jsx)("div",{role:"presentation",...e,className:(0,o.Z)("navbar-sidebar__backdrop",e.className)})}function Re(e){let{children:t}=e;const{navbar:{hideOnScroll:n,style:a}}=(0,w.L)(),i=(0,A.e)(),{navbarRef:l,isNavbarVisible:d}=function(e){const[t,n]=(0,r.useState)(e),o=(0,r.useRef)(!1),a=(0,r.useRef)(0),i=(0,r.useCallback)((e=>{null!==e&&(a.current=e.getBoundingClientRect().height)}),[]);return(0,L.RF)(((t,r)=>{let{scrollY:i}=t;if(!e)return;if(i<a.current)return void n(!0);if(o.current)return void(o.current=!1);const l=r?.scrollY,s=document.documentElement.scrollHeight-a.current,c=window.innerHeight;l&&i>=l?n(!1):i+c<s&&n(!0)})),(0,c.S)((t=>{if(!e)return;const r=t.location.hash;if(r?document.getElementById(r.substring(1)):void 0)return o.current=!0,void n(!1);n(!0)})),{navbarRef:i,isNavbarVisible:t}}(n);return(0,u.jsxs)("nav",{ref:l,"aria-label":(0,s.I)({id:"theme.NavBar.navAriaLabel",message:"Main",description:"The ARIA label for the main navigation"}),className:(0,o.Z)("navbar","navbar--fixed-top",n&&[Le.navbarHideable,!d&&Le.navbarHidden],{"navbar--dark":"dark"===a,"navbar--primary":"primary"===a,"navbar-sidebar--show":i.shown}),children:[t,(0,u.jsx)(Ne,{onClick:i.toggle}),(0,u.jsx)(Ae,{})]})}var Pe=n(8780);const Oe={errorBoundaryError:"errorBoundaryError_a6uf",errorBoundaryFallback:"errorBoundaryFallback_VBag"};function De(e){return(0,u.jsx)("button",{type:"button",...e,children:(0,u.jsx)(s.Z,{id:"theme.ErrorPageContent.tryAgain",description:"The label of the button to try again rendering when the React error boundary captures an error",children:"Try again"})})}function Ie(e){let{error:t}=e;const n=(0,Pe.getErrorCausalChain)(t).map((e=>e.message)).join("\n\nCause:\n");return(0,u.jsx)("p",{className:Oe.errorBoundaryError,children:n})}class Fe extends r.Component{componentDidCatch(e,t){throw this.props.onError(e,t)}render(){return this.props.children}}const Me="right";function ze(e){let{width:t=30,height:n=30,className:r,...o}=e;return(0,u.jsx)("svg",{className:r,width:t,height:n,viewBox:"0 0 30 30","aria-hidden":"true",...o,children:(0,u.jsx)("path",{stroke:"currentColor",strokeLinecap:"round",strokeMiterlimit:"10",strokeWidth:"2",d:"M4 7h22M4 15h22M4 23h22"})})}function Be(){const{toggle:e,shown:t}=(0,A.e)();return(0,u.jsx)("button",{onClick:e,"aria-label":(0,s.I)({id:"theme.docs.sidebar.toggleSidebarButtonAriaLabel",message:"Toggle navigation bar",description:"The ARIA label for hamburger menu button of mobile navigation"}),"aria-expanded":t,className:"navbar__toggle clean-btn",type:"button",children:(0,u.jsx)(ze,{})})}const $e={colorModeToggle:"colorModeToggle_DEke"};function Ue(e){let{items:t}=e;return(0,u.jsx)(u.Fragment,{children:t.map(((e,t)=>(0,u.jsx)(Fe,{onError:t=>new Error(`A theme navbar item failed to render.\nPlease double-check the following navbar item (themeConfig.navbar.items) of your Docusaurus config:\n${JSON.stringify(e,null,2)}`,{cause:t}),children:(0,u.jsx)(_e,{...e})},t)))})}function qe(e){let{left:t,right:n}=e;return(0,u.jsxs)("div",{className:"navbar__inner",children:[(0,u.jsx)("div",{className:"navbar__items",children:t}),(0,u.jsx)("div",{className:"navbar__items navbar__items--right",children:n})]})}function He(){const e=(0,A.e)(),t=(0,w.L)().navbar.items,[n,r]=function(e){function t(e){return"left"===(e.position??Me)}return[e.filter(t),e.filter((e=>!t(e)))]}(t),o=t.find((e=>"search"===e.type));return(0,u.jsx)(qe,{left:(0,u.jsxs)(u.Fragment,{children:[!e.disabled&&(0,u.jsx)(Be,{}),(0,u.jsx)(W,{}),(0,u.jsx)(Ue,{items:n})]}),right:(0,u.jsxs)(u.Fragment,{children:[(0,u.jsx)(Ue,{items:r}),(0,u.jsx)(G,{className:$e.colorModeToggle}),!o&&(0,u.jsx)(ve,{children:(0,u.jsx)(be.Z,{})})]})})}function Ze(){return(0,u.jsx)(Re,{children:(0,u.jsx)(He,{})})}function Ge(e){let{item:t}=e;const{to:n,href:r,label:o,prependBaseUrlToHref:a,...i}=t,l=(0,X.Z)(n),s=(0,X.Z)(r,{forcePrependBaseUrl:!0});return(0,u.jsxs)(Y.Z,{className:"footer__link-item",...r?{href:a?s:r}:{to:l},...i,children:[o,r&&!(0,J.Z)(r)&&(0,u.jsx)(te.Z,{})]})}function Ve(e){let{item:t}=e;return t.html?(0,u.jsx)("li",{className:"footer__item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)("li",{className:"footer__item",children:(0,u.jsx)(Ge,{item:t})},t.href??t.to)}function We(e){let{column:t}=e;return(0,u.jsxs)("div",{className:"col footer__col",children:[(0,u.jsx)("div",{className:"footer__title",children:t.title}),(0,u.jsx)("ul",{className:"footer__items clean-list",children:t.items.map(((e,t)=>(0,u.jsx)(Ve,{item:e},t)))})]})}function Qe(e){let{columns:t}=e;return(0,u.jsx)("div",{className:"row footer__links",children:t.map(((e,t)=>(0,u.jsx)(We,{column:e},t)))})}function Ke(){return(0,u.jsx)("span",{className:"footer__link-separator",children:"\xb7"})}function Ye(e){let{item:t}=e;return t.html?(0,u.jsx)("span",{className:"footer__link-item",dangerouslySetInnerHTML:{__html:t.html}}):(0,u.jsx)(Ge,{item:t})}function Xe(e){let{links:t}=e;return(0,u.jsx)("div",{className:"footer__links text--center",children:(0,u.jsx)("div",{className:"footer__links",children:t.map(((e,n)=>(0,u.jsxs)(r.Fragment,{children:[(0,u.jsx)(Ye,{item:e}),t.length!==n+1&&(0,u.jsx)(Ke,{})]},n)))})})}function Je(e){let{links:t}=e;return function(e){return"title"in e[0]}(t)?(0,u.jsx)(Qe,{columns:t}):(0,u.jsx)(Xe,{links:t})}var et=n(9965);const tt={footerLogoLink:"footerLogoLink_BH7S"};function nt(e){let{logo:t}=e;const{withBaseUrl:n}=(0,X.C)(),r={light:n(t.src),dark:n(t.srcDark??t.src)};return(0,u.jsx)(et.Z,{className:(0,o.Z)("footer__logo",t.className),alt:t.alt,sources:r,width:t.width,height:t.height,style:t.style})}function rt(e){let{logo:t}=e;return t.href?(0,u.jsx)(Y.Z,{href:t.href,className:tt.footerLogoLink,target:t.target,children:(0,u.jsx)(nt,{logo:t})}):(0,u.jsx)(nt,{logo:t})}function ot(e){let{copyright:t}=e;return(0,u.jsx)("div",{className:"footer__copyright",dangerouslySetInnerHTML:{__html:t}})}function at(e){let{style:t,links:n,logo:r,copyright:a}=e;return(0,u.jsx)("footer",{className:(0,o.Z)("footer",{"footer--dark":"dark"===t}),children:(0,u.jsxs)("div",{className:"container container-fluid",children:[n,(r||a)&&(0,u.jsxs)("div",{className:"footer__bottom text--center",children:[r&&(0,u.jsx)("div",{className:"margin-bottom--sm",children:r}),a]})]})})}function it(){const{footer:e}=(0,w.L)();if(!e)return null;const{copyright:t,links:n,logo:r,style:o}=e;return(0,u.jsx)(at,{style:o,links:n&&n.length>0&&(0,u.jsx)(Je,{links:n}),logo:r&&(0,u.jsx)(rt,{logo:r}),copyright:t&&(0,u.jsx)(ot,{copyright:t})})}const lt=r.memo(it),st=(0,N.Qc)([M.S,k.pl,L.OC,xe.L5,i.VC,function(e){let{children:t}=e;return(0,u.jsx)(R.n2,{children:(0,u.jsx)(A.M,{children:(0,u.jsx)(O,{children:t})})})}]);function ct(e){let{children:t}=e;return(0,u.jsx)(st,{children:t})}var ut=n(2503);function dt(e){let{error:t,tryAgain:n}=e;return(0,u.jsx)("main",{className:"container margin-vert--xl",children:(0,u.jsx)("div",{className:"row",children:(0,u.jsxs)("div",{className:"col col--6 col--offset-3",children:[(0,u.jsx)(ut.Z,{as:"h1",className:"hero__title",children:(0,u.jsx)(s.Z,{id:"theme.ErrorPageContent.title",description:"The title of the fallback page when the page crashed",children:"This page crashed."})}),(0,u.jsx)("div",{className:"margin-vert--lg",children:(0,u.jsx)(De,{onClick:n,className:"button button--primary shadow--lw"})}),(0,u.jsx)("hr",{}),(0,u.jsx)("div",{className:"margin-vert--md",children:(0,u.jsx)(Ie,{error:t})})]})})})}const pt={mainWrapper:"mainWrapper_z2l0"};function ft(e){const{children:t,noFooter:n,wrapperClassName:r,title:l,description:s}=e;return(0,b.t)(),(0,u.jsxs)(ct,{children:[(0,u.jsx)(i.d,{title:l,description:s}),(0,u.jsx)(v,{}),(0,u.jsx)(j,{}),(0,u.jsx)(Ze,{}),(0,u.jsx)("div",{id:d,className:(0,o.Z)(h.k.wrapper.main,pt.mainWrapper,r),children:(0,u.jsx)(a.Z,{fallback:e=>(0,u.jsx)(dt,{...e}),children:t})}),!n&&(0,u.jsx)(lt,{})]})}},1327:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});n(7294);var r=n(3692),o=n(4996),a=n(2263),i=n(6668),l=n(9965),s=n(5893);function c(e){let{logo:t,alt:n,imageClassName:r}=e;const a={light:(0,o.Z)(t.src),dark:(0,o.Z)(t.srcDark||t.src)},i=(0,s.jsx)(l.Z,{className:t.className,sources:a,height:t.height,width:t.width,alt:n,style:t.style});return r?(0,s.jsx)("div",{className:r,children:i}):i}function u(e){const{siteConfig:{title:t}}=(0,a.Z)(),{navbar:{title:n,logo:l}}=(0,i.L)(),{imageClassName:u,titleClassName:d,...p}=e,f=(0,o.Z)(l?.href||"/"),m=n?"":t,g=l?.alt??m;return(0,s.jsxs)(r.Z,{to:f,...p,...l?.target&&{target:l.target},children:[l&&(0,s.jsx)(c,{logo:l,alt:g,imageClassName:u}),null!=n&&(0,s.jsx)("b",{className:d,children:n})]})}},197:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});n(7294);var r=n(5742),o=n(5893);function a(e){let{locale:t,version:n,tag:a}=e;const i=t;return(0,o.jsxs)(r.Z,{children:[t&&(0,o.jsx)("meta",{name:"docusaurus_locale",content:t}),n&&(0,o.jsx)("meta",{name:"docusaurus_version",content:n}),a&&(0,o.jsx)("meta",{name:"docusaurus_tag",content:a}),i&&(0,o.jsx)("meta",{name:"docsearch:language",content:i}),n&&(0,o.jsx)("meta",{name:"docsearch:version",content:n}),a&&(0,o.jsx)("meta",{name:"docsearch:docusaurus_tag",content:a})]})}},9965:(e,t,n)=>{"use strict";n.d(t,{Z:()=>u});var r=n(7294),o=n(512),a=n(2389),i=n(2949);const l={themedComponent:"themedComponent_mlkZ","themedComponent--light":"themedComponent--light_NVdE","themedComponent--dark":"themedComponent--dark_xIcU"};var s=n(5893);function c(e){let{className:t,children:n}=e;const c=(0,a.Z)(),{colorMode:u}=(0,i.I)();return(0,s.jsx)(s.Fragment,{children:(c?"dark"===u?["dark"]:["light"]:["light","dark"]).map((e=>{const a=n({theme:e,className:(0,o.Z)(t,l.themedComponent,l[`themedComponent--${e}`])});return(0,s.jsx)(r.Fragment,{children:a},e)}))})}function u(e){const{sources:t,className:n,alt:r,...o}=e;return(0,s.jsx)(c,{className:n,children:e=>{let{theme:n,className:a}=e;return(0,s.jsx)("img",{src:t[n],alt:r,className:a,...o})}})}},6043:(e,t,n)=>{"use strict";n.d(t,{u:()=>c,z:()=>b});var r=n(7294),o=n(412),a=n(469),i=n(1442),l=n(5893);const s="ease-in-out";function c(e){let{initialState:t}=e;const[n,o]=(0,r.useState)(t??!1),a=(0,r.useCallback)((()=>{o((e=>!e))}),[]);return{collapsed:n,setCollapsed:o,toggleCollapsed:a}}const u={display:"none",overflow:"hidden",height:"0px"},d={display:"block",overflow:"visible",height:"auto"};function p(e,t){const n=t?u:d;e.style.display=n.display,e.style.overflow=n.overflow,e.style.height=n.height}function f(e){let{collapsibleRef:t,collapsed:n,animation:o}=e;const a=(0,r.useRef)(!1);(0,r.useEffect)((()=>{const e=t.current;function r(){const t=e.scrollHeight,n=o?.duration??function(e){if((0,i.n)())return 1;const t=e/36;return Math.round(10*(4+15*t**.25+t/5))}(t);return{transition:`height ${n}ms ${o?.easing??s}`,height:`${t}px`}}function l(){const t=r();e.style.transition=t.transition,e.style.height=t.height}if(!a.current)return p(e,n),void(a.current=!0);return e.style.willChange="height",function(){const t=requestAnimationFrame((()=>{n?(l(),requestAnimationFrame((()=>{e.style.height=u.height,e.style.overflow=u.overflow}))):(e.style.display="block",requestAnimationFrame((()=>{l()})))}));return()=>cancelAnimationFrame(t)}()}),[t,n,o])}function m(e){if(!o.Z.canUseDOM)return e?u:d}function g(e){let{as:t="div",collapsed:n,children:o,animation:a,onCollapseTransitionEnd:i,className:s,disableSSRStyle:c}=e;const u=(0,r.useRef)(null);return f({collapsibleRef:u,collapsed:n,animation:a}),(0,l.jsx)(t,{ref:u,style:c?void 0:m(n),onTransitionEnd:e=>{"height"===e.propertyName&&(p(u.current,n),i?.(n))},className:s,children:o})}function h(e){let{collapsed:t,...n}=e;const[o,i]=(0,r.useState)(!t),[s,c]=(0,r.useState)(t);return(0,a.Z)((()=>{t||i(!0)}),[t]),(0,a.Z)((()=>{o&&c(t)}),[o,t]),o?(0,l.jsx)(g,{...n,collapsed:s}):null}function b(e){let{lazy:t,...n}=e;const r=t?h:g;return(0,l.jsx)(r,{...n})}},9689:(e,t,n)=>{"use strict";n.d(t,{nT:()=>g,pl:()=>m});var r=n(7294),o=n(2389),a=n(12),i=n(902),l=n(6668),s=n(5893);const c=(0,a.WA)("docusaurus.announcement.dismiss"),u=(0,a.WA)("docusaurus.announcement.id"),d=()=>"true"===c.get(),p=e=>c.set(String(e)),f=r.createContext(null);function m(e){let{children:t}=e;const n=function(){const{announcementBar:e}=(0,l.L)(),t=(0,o.Z)(),[n,a]=(0,r.useState)((()=>!!t&&d()));(0,r.useEffect)((()=>{a(d())}),[]);const i=(0,r.useCallback)((()=>{p(!0),a(!0)}),[]);return(0,r.useEffect)((()=>{if(!e)return;const{id:t}=e;let n=u.get();"annoucement-bar"===n&&(n="announcement-bar");const r=t!==n;u.set(t),r&&p(!1),!r&&d()||a(!1)}),[e]),(0,r.useMemo)((()=>({isActive:!!e&&!n,close:i})),[e,n,i])}();return(0,s.jsx)(f.Provider,{value:n,children:t})}function g(){const e=(0,r.useContext)(f);if(!e)throw new i.i6("AnnouncementBarProvider");return e}},2949:(e,t,n)=>{"use strict";n.d(t,{I:()=>b,S:()=>h});var r=n(7294),o=n(412),a=n(902),i=n(12),l=n(6668),s=n(5893);const c=r.createContext(void 0),u="theme",d=(0,i.WA)(u),p={light:"light",dark:"dark"},f=e=>e===p.dark?p.dark:p.light,m=e=>o.Z.canUseDOM?f(document.documentElement.getAttribute("data-theme")):f(e),g=e=>{d.set(f(e))};function h(e){let{children:t}=e;const n=function(){const{colorMode:{defaultMode:e,disableSwitch:t,respectPrefersColorScheme:n}}=(0,l.L)(),[o,a]=(0,r.useState)(m(e));(0,r.useEffect)((()=>{t&&d.del()}),[t]);const i=(0,r.useCallback)((function(t,r){void 0===r&&(r={});const{persist:o=!0}=r;t?(a(t),o&&g(t)):(a(n?window.matchMedia("(prefers-color-scheme: dark)").matches?p.dark:p.light:e),d.del())}),[n,e]);(0,r.useEffect)((()=>{document.documentElement.setAttribute("data-theme",f(o))}),[o]),(0,r.useEffect)((()=>{if(t)return;const e=e=>{if(e.key!==u)return;const t=d.get();null!==t&&i(f(t))};return window.addEventListener("storage",e),()=>window.removeEventListener("storage",e)}),[t,i]);const s=(0,r.useRef)(!1);return(0,r.useEffect)((()=>{if(t&&!n)return;const e=window.matchMedia("(prefers-color-scheme: dark)"),r=()=>{window.matchMedia("print").matches||s.current?s.current=window.matchMedia("print").matches:i(null)};return e.addListener(r),()=>e.removeListener(r)}),[i,t,n]),(0,r.useMemo)((()=>({colorMode:o,setColorMode:i,get isDarkTheme(){return o===p.dark},setLightTheme(){i(p.light)},setDarkTheme(){i(p.dark)}})),[o,i])}();return(0,s.jsx)(c.Provider,{value:n,children:t})}function b(){const e=(0,r.useContext)(c);if(null==e)throw new a.i6("ColorModeProvider","Please see https://docusaurus.io/docs/api/themes/configuration#use-color-mode.");return e}},373:(e,t,n)=>{"use strict";n.d(t,{J:()=>v,L5:()=>b});var r=n(7294),o=n(143),a=n(9935),i=n(6668),l=n(2802),s=n(902),c=n(12),u=n(5893);const d=e=>`docs-preferred-version-${e}`,p={save:(e,t,n)=>{(0,c.WA)(d(e),{persistence:t}).set(n)},read:(e,t)=>(0,c.WA)(d(e),{persistence:t}).get(),clear:(e,t)=>{(0,c.WA)(d(e),{persistence:t}).del()}},f=e=>Object.fromEntries(e.map((e=>[e,{preferredVersionName:null}])));const m=r.createContext(null);function g(){const e=(0,o._r)(),t=(0,i.L)().docs.versionPersistence,n=(0,r.useMemo)((()=>Object.keys(e)),[e]),[a,l]=(0,r.useState)((()=>f(n)));(0,r.useEffect)((()=>{l(function(e){let{pluginIds:t,versionPersistence:n,allDocsData:r}=e;function o(e){const t=p.read(e,n);return r[e].versions.some((e=>e.name===t))?{preferredVersionName:t}:(p.clear(e,n),{preferredVersionName:null})}return Object.fromEntries(t.map((e=>[e,o(e)])))}({allDocsData:e,versionPersistence:t,pluginIds:n}))}),[e,t,n]);return[a,(0,r.useMemo)((()=>({savePreferredVersion:function(e,n){p.save(e,t,n),l((t=>({...t,[e]:{preferredVersionName:n}})))}})),[t])]}function h(e){let{children:t}=e;const n=g();return(0,u.jsx)(m.Provider,{value:n,children:t})}function b(e){let{children:t}=e;return l.cE?(0,u.jsx)(h,{children:t}):(0,u.jsx)(u.Fragment,{children:t})}function y(){const e=(0,r.useContext)(m);if(!e)throw new s.i6("DocsPreferredVersionContextProvider");return e}function v(e){void 0===e&&(e=a.m);const t=(0,o.zh)(e),[n,i]=y(),{preferredVersionName:l}=n[e];return{preferredVersion:t.versions.find((e=>e.name===l))??null,savePreferredVersionName:(0,r.useCallback)((t=>{i.savePreferredVersion(e,t)}),[i,e])}}},1116:(e,t,n)=>{"use strict";n.d(t,{V:()=>c,b:()=>s});var r=n(7294),o=n(902),a=n(5893);const i=Symbol("EmptyContext"),l=r.createContext(i);function s(e){let{children:t,name:n,items:o}=e;const i=(0,r.useMemo)((()=>n&&o?{name:n,items:o}:null),[n,o]);return(0,a.jsx)(l.Provider,{value:i,children:t})}function c(){const e=(0,r.useContext)(l);if(e===i)throw new o.i6("DocsSidebarProvider");return e}},4477:(e,t,n)=>{"use strict";n.d(t,{E:()=>s,q:()=>l});var r=n(7294),o=n(902),a=n(5893);const i=r.createContext(null);function l(e){let{children:t,version:n}=e;return(0,a.jsx)(i.Provider,{value:n,children:t})}function s(){const e=(0,r.useContext)(i);if(null===e)throw new o.i6("DocsVersionProvider");return e}},2961:(e,t,n)=>{"use strict";n.d(t,{M:()=>f,e:()=>m});var r=n(7294),o=n(3102),a=n(7524),i=n(6550),l=n(902);function s(e){!function(e){const t=(0,i.k6)(),n=(0,l.zX)(e);(0,r.useEffect)((()=>t.block(((e,t)=>n(e,t)))),[t,n])}(((t,n)=>{if("POP"===n)return e(t,n)}))}var c=n(6668),u=n(5893);const d=r.createContext(void 0);function p(){const e=function(){const e=(0,o.HY)(),{items:t}=(0,c.L)().navbar;return 0===t.length&&!e.component}(),t=(0,a.i)(),n=!e&&"mobile"===t,[i,l]=(0,r.useState)(!1);s((()=>{if(i)return l(!1),!1}));const u=(0,r.useCallback)((()=>{l((e=>!e))}),[]);return(0,r.useEffect)((()=>{"desktop"===t&&l(!1)}),[t]),(0,r.useMemo)((()=>({disabled:e,shouldRender:n,toggle:u,shown:i})),[e,n,u,i])}function f(e){let{children:t}=e;const n=p();return(0,u.jsx)(d.Provider,{value:n,children:t})}function m(){const e=r.useContext(d);if(void 0===e)throw new l.i6("NavbarMobileSidebarProvider");return e}},3102:(e,t,n)=>{"use strict";n.d(t,{HY:()=>s,Zo:()=>c,n2:()=>l});var r=n(7294),o=n(902),a=n(5893);const i=r.createContext(null);function l(e){let{children:t}=e;const n=(0,r.useState)({component:null,props:null});return(0,a.jsx)(i.Provider,{value:n,children:t})}function s(){const e=(0,r.useContext)(i);if(!e)throw new o.i6("NavbarSecondaryMenuContentProvider");return e[0]}function c(e){let{component:t,props:n}=e;const a=(0,r.useContext)(i);if(!a)throw new o.i6("NavbarSecondaryMenuContentProvider");const[,l]=a,s=(0,o.Ql)(n);return(0,r.useEffect)((()=>{l({component:t,props:s})}),[l,t,s]),(0,r.useEffect)((()=>()=>l({component:null,props:null})),[l]),null}},9727:(e,t,n)=>{"use strict";n.d(t,{h:()=>o,t:()=>a});var r=n(7294);const o="navigation-with-keyboard";function a(){(0,r.useEffect)((()=>{function e(e){"keydown"===e.type&&"Tab"===e.key&&document.body.classList.add(o),"mousedown"===e.type&&document.body.classList.remove(o)}return document.addEventListener("keydown",e),document.addEventListener("mousedown",e),()=>{document.body.classList.remove(o),document.removeEventListener("keydown",e),document.removeEventListener("mousedown",e)}}),[])}},7524:(e,t,n)=>{"use strict";n.d(t,{i:()=>l});var r=n(7294),o=n(412);const a={desktop:"desktop",mobile:"mobile",ssr:"ssr"},i=996;function l(e){let{desktopBreakpoint:t=i}=void 0===e?{}:e;const[n,l]=(0,r.useState)((()=>"ssr"));return(0,r.useEffect)((()=>{function e(){l(function(e){if(!o.Z.canUseDOM)throw new Error("getWindowSize() should only be called after React hydration");return window.innerWidth>e?a.desktop:a.mobile}(t))}return e(),window.addEventListener("resize",e),()=>{window.removeEventListener("resize",e)}}),[t]),n}},5281:(e,t,n)=>{"use strict";n.d(t,{k:()=>r});const r={page:{blogListPage:"blog-list-page",blogPostPage:"blog-post-page",blogTagsListPage:"blog-tags-list-page",blogTagPostListPage:"blog-tags-post-list-page",docsDocPage:"docs-doc-page",docsTagsListPage:"docs-tags-list-page",docsTagDocListPage:"docs-tags-doc-list-page",mdxPage:"mdx-page"},wrapper:{main:"main-wrapper",blogPages:"blog-wrapper",docsPages:"docs-wrapper",mdxPages:"mdx-wrapper"},common:{editThisPage:"theme-edit-this-page",lastUpdated:"theme-last-updated",backToTopButton:"theme-back-to-top-button",codeBlock:"theme-code-block",admonition:"theme-admonition",unlistedBanner:"theme-unlisted-banner",admonitionType:e=>`theme-admonition-${e}`},layout:{},docs:{docVersionBanner:"theme-doc-version-banner",docVersionBadge:"theme-doc-version-badge",docBreadcrumbs:"theme-doc-breadcrumbs",docMarkdown:"theme-doc-markdown",docTocMobile:"theme-doc-toc-mobile",docTocDesktop:"theme-doc-toc-desktop",docFooter:"theme-doc-footer",docFooterTagsRow:"theme-doc-footer-tags-row",docFooterEditMetaRow:"theme-doc-footer-edit-meta-row",docSidebarContainer:"theme-doc-sidebar-container",docSidebarMenu:"theme-doc-sidebar-menu",docSidebarItemCategory:"theme-doc-sidebar-item-category",docSidebarItemLink:"theme-doc-sidebar-item-link",docSidebarItemCategoryLevel:e=>`theme-doc-sidebar-item-category-level-${e}`,docSidebarItemLinkLevel:e=>`theme-doc-sidebar-item-link-level-${e}`},blog:{}}},1442:(e,t,n)=>{"use strict";function r(){return window.matchMedia("(prefers-reduced-motion: reduce)").matches}n.d(t,{n:()=>r})},2802:(e,t,n)=>{"use strict";n.d(t,{LM:()=>f,_F:()=>h,cE:()=>p,SN:()=>E,lO:()=>k,vY:()=>S,oz:()=>x,s1:()=>w,f:()=>y});var r=n(7294),o=n(6550),a=n(8790),i=n(143),l=n(373),s=n(4477),c=n(1116);function u(e){return Array.from(new Set(e))}var d=n(8596);const p=!!i._r;function f(e){return"link"!==e.type||e.unlisted?"category"===e.type?function(e){if(e.href&&!e.linkUnlisted)return e.href;for(const t of e.items){const e=f(t);if(e)return e}}(e):void 0:e.href}const m=(e,t)=>void 0!==e&&(0,d.Mg)(e,t),g=(e,t)=>e.some((e=>h(e,t)));function h(e,t){return"link"===e.type?m(e.href,t):"category"===e.type&&(m(e.href,t)||g(e.items,t))}function b(e,t){switch(e.type){case"category":return h(e,t)||e.items.some((e=>b(e,t)));case"link":return!e.unlisted||h(e,t);default:return!0}}function y(e,t){return(0,r.useMemo)((()=>e.filter((e=>b(e,t)))),[e,t])}function v(e){let{sidebarItems:t,pathname:n,onlyCategories:r=!1}=e;const o=[];return function e(t){for(const a of t)if("category"===a.type&&((0,d.Mg)(a.href,n)||e(a.items))||"link"===a.type&&(0,d.Mg)(a.href,n)){return r&&"category"!==a.type||o.unshift(a),!0}return!1}(t),o}function w(){const e=(0,c.V)(),{pathname:t}=(0,o.TH)(),n=(0,i.gA)()?.pluginData.breadcrumbs;return!1!==n&&e?v({sidebarItems:e.items,pathname:t}):null}function k(e){const{activeVersion:t}=(0,i.Iw)(e),{preferredVersion:n}=(0,l.J)(e),o=(0,i.yW)(e);return(0,r.useMemo)((()=>u([t,n,o].filter(Boolean))),[t,n,o])}function x(e,t){const n=k(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.sidebars?Object.entries(e.sidebars):[])),r=t.find((t=>t[0]===e));if(!r)throw new Error(`Can't find any sidebar with id "${e}" in version${n.length>1?"s":""} ${n.map((e=>e.name)).join(", ")}".\nAvailable sidebar ids are:\n- ${t.map((e=>e[0])).join("\n- ")}`);return r[1]}),[e,n])}function S(e,t){const n=k(t);return(0,r.useMemo)((()=>{const t=n.flatMap((e=>e.docs)),r=t.find((t=>t.id===e));if(!r){if(n.flatMap((e=>e.draftIds)).includes(e))return null;throw new Error(`Couldn't find any doc with id "${e}" in version${n.length>1?"s":""} "${n.map((e=>e.name)).join(", ")}".\nAvailable doc ids are:\n- ${u(t.map((e=>e.id))).join("\n- ")}`)}return r}),[e,n])}function E(e){let{route:t}=e;const n=(0,o.TH)(),r=(0,s.E)(),i=t.routes,l=i.find((e=>(0,o.LX)(n.pathname,e)));if(!l)return null;const c=l.sidebar,u=c?r.docsSidebars[c]:void 0;return{docElement:(0,a.H)(i),sidebarName:c,sidebarItems:u}}},1944:(e,t,n)=>{"use strict";n.d(t,{FG:()=>f,d:()=>d,VC:()=>m});var r=n(7294),o=n(512),a=n(5742),i=n(226);function l(){const e=r.useContext(i._);if(!e)throw new Error("Unexpected: no Docusaurus route context found");return e}var s=n(4996),c=n(2263);var u=n(5893);function d(e){let{title:t,description:n,keywords:r,image:o,children:i}=e;const l=function(e){const{siteConfig:t}=(0,c.Z)(),{title:n,titleDelimiter:r}=t;return e?.trim().length?`${e.trim()} ${r} ${n}`:n}(t),{withBaseUrl:d}=(0,s.C)(),p=o?d(o,{absolute:!0}):void 0;return(0,u.jsxs)(a.Z,{children:[t&&(0,u.jsx)("title",{children:l}),t&&(0,u.jsx)("meta",{property:"og:title",content:l}),n&&(0,u.jsx)("meta",{name:"description",content:n}),n&&(0,u.jsx)("meta",{property:"og:description",content:n}),r&&(0,u.jsx)("meta",{name:"keywords",content:Array.isArray(r)?r.join(","):r}),p&&(0,u.jsx)("meta",{property:"og:image",content:p}),p&&(0,u.jsx)("meta",{name:"twitter:image",content:p}),i]})}const p=r.createContext(void 0);function f(e){let{className:t,children:n}=e;const i=r.useContext(p),l=(0,o.Z)(i,t);return(0,u.jsxs)(p.Provider,{value:l,children:[(0,u.jsx)(a.Z,{children:(0,u.jsx)("html",{className:l})}),n]})}function m(e){let{children:t}=e;const n=l(),r=`plugin-${n.plugin.name.replace(/docusaurus-(?:plugin|theme)-(?:content-)?/gi,"")}`;const a=`plugin-id-${n.plugin.id}`;return(0,u.jsx)(f,{className:(0,o.Z)(r,a),children:t})}},902:(e,t,n)=>{"use strict";n.d(t,{D9:()=>l,Qc:()=>u,Ql:()=>c,i6:()=>s,zX:()=>i});var r=n(7294),o=n(469),a=n(5893);function i(e){const t=(0,r.useRef)(e);return(0,o.Z)((()=>{t.current=e}),[e]),(0,r.useCallback)((function(){return t.current(...arguments)}),[])}function l(e){const t=(0,r.useRef)();return(0,o.Z)((()=>{t.current=e})),t.current}class s extends Error{constructor(e,t){super(),this.name="ReactContextError",this.message=`Hook ${this.stack?.split("\n")[1]?.match(/at (?:\w+\.)?(?<name>\w+)/)?.groups.name??""} is called outside the <${e}>. ${t??""}`}}function c(e){const t=Object.entries(e);return t.sort(((e,t)=>e[0].localeCompare(t[0]))),(0,r.useMemo)((()=>e),t.flat())}function u(e){return t=>{let{children:n}=t;return(0,a.jsx)(a.Fragment,{children:e.reduceRight(((e,t)=>(0,a.jsx)(t,{children:e})),n)})}}},8596:(e,t,n)=>{"use strict";n.d(t,{Mg:()=>i,Ns:()=>l});var r=n(7294),o=n(723),a=n(2263);function i(e,t){const n=e=>(!e||e.endsWith("/")?e:`${e}/`)?.toLowerCase();return n(e)===n(t)}function l(){const{baseUrl:e}=(0,a.Z)().siteConfig;return(0,r.useMemo)((()=>function(e){let{baseUrl:t,routes:n}=e;function r(e){return e.path===t&&!0===e.exact}function o(e){return e.path===t&&!e.exact}return function e(t){if(0===t.length)return;return t.find(r)||e(t.filter(o).flatMap((e=>e.routes??[])))}(n)}({routes:o.Z,baseUrl:e})),[e])}},2466:(e,t,n)=>{"use strict";n.d(t,{Ct:()=>f,OC:()=>c,RF:()=>p});var r=n(7294),o=n(412),a=n(2389),i=(n(469),n(902)),l=n(5893);const s=r.createContext(void 0);function c(e){let{children:t}=e;const n=function(){const e=(0,r.useRef)(!0);return(0,r.useMemo)((()=>({scrollEventsEnabledRef:e,enableScrollEvents:()=>{e.current=!0},disableScrollEvents:()=>{e.current=!1}})),[])}();return(0,l.jsx)(s.Provider,{value:n,children:t})}function u(){const e=(0,r.useContext)(s);if(null==e)throw new i.i6("ScrollControllerProvider");return e}const d=()=>o.Z.canUseDOM?{scrollX:window.pageXOffset,scrollY:window.pageYOffset}:null;function p(e,t){void 0===t&&(t=[]);const{scrollEventsEnabledRef:n}=u(),o=(0,r.useRef)(d()),a=(0,i.zX)(e);(0,r.useEffect)((()=>{const e=()=>{if(!n.current)return;const e=d();a(e,o.current),o.current=e},t={passive:!0};return e(),window.addEventListener("scroll",e,t),()=>window.removeEventListener("scroll",e,t)}),[a,n,...t])}function f(){const e=(0,r.useRef)(null),t=(0,a.Z)()&&"smooth"===getComputedStyle(document.documentElement).scrollBehavior;return{startScroll:n=>{e.current=t?function(e){return window.scrollTo({top:e,behavior:"smooth"}),()=>{}}(n):function(e){let t=null;const n=document.documentElement.scrollTop>e;return function r(){const o=document.documentElement.scrollTop;(n&&o>e||!n&&o<e)&&(t=requestAnimationFrame(r),window.scrollTo(0,Math.floor(.85*(o-e))+e))}(),()=>t&&cancelAnimationFrame(t)}(n)},cancelScroll:()=>e.current?.()}}},3320:(e,t,n)=>{"use strict";n.d(t,{HX:()=>r,os:()=>o});n(2263);const r="default";function o(e,t){return`docs-${e}-${t}`}},12:(e,t,n)=>{"use strict";n.d(t,{WA:()=>s});n(7294);const r="localStorage";function o(e){let{key:t,oldValue:n,newValue:r,storage:o}=e;if(n===r)return;const a=document.createEvent("StorageEvent");a.initStorageEvent("storage",!1,!1,t,n,r,window.location.href,o),window.dispatchEvent(a)}function a(e){if(void 0===e&&(e=r),"undefined"==typeof window)throw new Error("Browser storage is not available on Node.js/Docusaurus SSR process.");if("none"===e)return null;try{return window[e]}catch(n){return t=n,i||(console.warn("Docusaurus browser storage is not available.\nPossible reasons: running Docusaurus in an iframe, in an incognito browser session, or using too strict browser privacy settings.",t),i=!0),null}var t}let i=!1;const l={get:()=>null,set:()=>{},del:()=>{},listen:()=>()=>{}};function s(e,t){if("undefined"==typeof window)return function(e){function t(){throw new Error(`Illegal storage API usage for storage key "${e}".\nDocusaurus storage APIs are not supposed to be called on the server-rendering process.\nPlease only call storage APIs in effects and event handlers.`)}return{get:t,set:t,del:t,listen:t}}(e);const n=a(t?.persistence);return null===n?l:{get:()=>{try{return n.getItem(e)}catch(t){return console.error(`Docusaurus storage error, can't get key=${e}`,t),null}},set:t=>{try{const r=n.getItem(e);n.setItem(e,t),o({key:e,oldValue:r,newValue:t,storage:n})}catch(r){console.error(`Docusaurus storage error, can't set ${e}=${t}`,r)}},del:()=>{try{const t=n.getItem(e);n.removeItem(e),o({key:e,oldValue:t,newValue:null,storage:n})}catch(t){console.error(`Docusaurus storage error, can't delete key=${e}`,t)}},listen:t=>{try{const r=r=>{r.storageArea===n&&r.key===e&&t(r)};return window.addEventListener("storage",r),()=>window.removeEventListener("storage",r)}catch(r){return console.error(`Docusaurus storage error, can't listen for changes of key=${e}`,r),()=>{}}}}}},4711:(e,t,n)=>{"use strict";n.d(t,{l:()=>i});var r=n(2263),o=n(6550),a=n(8780);function i(){const{siteConfig:{baseUrl:e,url:t,trailingSlash:n},i18n:{defaultLocale:i,currentLocale:l}}=(0,r.Z)(),{pathname:s}=(0,o.TH)(),c=(0,a.applyTrailingSlash)(s,{trailingSlash:n,baseUrl:e}),u=l===i?e:e.replace(`/${l}/`,"/"),d=c.replace(e,"");return{createUrl:function(e){let{locale:n,fullyQualified:r}=e;return`${r?t:""}${function(e){return e===i?`${u}`:`${u}${e}/`}(n)}${d}`}}}},5936:(e,t,n)=>{"use strict";n.d(t,{S:()=>i});var r=n(7294),o=n(6550),a=n(902);function i(e){const t=(0,o.TH)(),n=(0,a.D9)(t),i=(0,a.zX)(e);(0,r.useEffect)((()=>{n&&t!==n&&i({location:t,previousLocation:n})}),[i,t,n])}},6668:(e,t,n)=>{"use strict";n.d(t,{L:()=>o});var r=n(2263);function o(){return(0,r.Z)().siteConfig.themeConfig}},8802:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.default=function(e,t){const{trailingSlash:n,baseUrl:r}=t;if(e.startsWith("#"))return e;if(void 0===n)return e;const[o]=e.split(/[#?]/),a="/"===o||o===r?o:(i=o,n?function(e){return e.endsWith("/")?e:`${e}/`}(i):function(e){return e.endsWith("/")?e.slice(0,-1):e}(i));var i;return e.replace(o,a)}},4143:(e,t)=>{"use strict";Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=void 0,t.getErrorCausalChain=function e(t){return t.cause?[t,...e(t.cause)]:[t]}},8780:function(e,t,n){"use strict";var r=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.getErrorCausalChain=t.applyTrailingSlash=t.blogPostContainerID=void 0,t.blogPostContainerID="__blog-post-container";var o=n(8802);Object.defineProperty(t,"applyTrailingSlash",{enumerable:!0,get:function(){return r(o).default}});var a=n(4143);Object.defineProperty(t,"getErrorCausalChain",{enumerable:!0,get:function(){return a.getErrorCausalChain}})},9318:(e,t,n)=>{"use strict";n.d(t,{lX:()=>w,q_:()=>C,ob:()=>f,PP:()=>j,Ep:()=>p});var r=n(7462);function o(e){return"/"===e.charAt(0)}function a(e,t){for(var n=t,r=n+1,o=e.length;r<o;n+=1,r+=1)e[n]=e[r];e.pop()}const i=function(e,t){void 0===t&&(t="");var n,r=e&&e.split("/")||[],i=t&&t.split("/")||[],l=e&&o(e),s=t&&o(t),c=l||s;if(e&&o(e)?i=r:r.length&&(i.pop(),i=i.concat(r)),!i.length)return"/";if(i.length){var u=i[i.length-1];n="."===u||".."===u||""===u}else n=!1;for(var d=0,p=i.length;p>=0;p--){var f=i[p];"."===f?a(i,p):".."===f?(a(i,p),d++):d&&(a(i,p),d--)}if(!c)for(;d--;d)i.unshift("..");!c||""===i[0]||i[0]&&o(i[0])||i.unshift("");var m=i.join("/");return n&&"/"!==m.substr(-1)&&(m+="/"),m};var l=n(8776);function s(e){return"/"===e.charAt(0)?e:"/"+e}function c(e){return"/"===e.charAt(0)?e.substr(1):e}function u(e,t){return function(e,t){return 0===e.toLowerCase().indexOf(t.toLowerCase())&&-1!=="/?#".indexOf(e.charAt(t.length))}(e,t)?e.substr(t.length):e}function d(e){return"/"===e.charAt(e.length-1)?e.slice(0,-1):e}function p(e){var t=e.pathname,n=e.search,r=e.hash,o=t||"/";return n&&"?"!==n&&(o+="?"===n.charAt(0)?n:"?"+n),r&&"#"!==r&&(o+="#"===r.charAt(0)?r:"#"+r),o}function f(e,t,n,o){var a;"string"==typeof e?(a=function(e){var t=e||"/",n="",r="",o=t.indexOf("#");-1!==o&&(r=t.substr(o),t=t.substr(0,o));var a=t.indexOf("?");return-1!==a&&(n=t.substr(a),t=t.substr(0,a)),{pathname:t,search:"?"===n?"":n,hash:"#"===r?"":r}}(e),a.state=t):(void 0===(a=(0,r.Z)({},e)).pathname&&(a.pathname=""),a.search?"?"!==a.search.charAt(0)&&(a.search="?"+a.search):a.search="",a.hash?"#"!==a.hash.charAt(0)&&(a.hash="#"+a.hash):a.hash="",void 0!==t&&void 0===a.state&&(a.state=t));try{a.pathname=decodeURI(a.pathname)}catch(l){throw l instanceof URIError?new URIError('Pathname "'+a.pathname+'" could not be decoded. This is likely caused by an invalid percent-encoding.'):l}return n&&(a.key=n),o?a.pathname?"/"!==a.pathname.charAt(0)&&(a.pathname=i(a.pathname,o.pathname)):a.pathname=o.pathname:a.pathname||(a.pathname="/"),a}function m(){var e=null;var t=[];return{setPrompt:function(t){return e=t,function(){e===t&&(e=null)}},confirmTransitionTo:function(t,n,r,o){if(null!=e){var a="function"==typeof e?e(t,n):e;"string"==typeof a?"function"==typeof r?r(a,o):o(!0):o(!1!==a)}else o(!0)},appendListener:function(e){var n=!0;function r(){n&&e.apply(void 0,arguments)}return t.push(r),function(){n=!1,t=t.filter((function(e){return e!==r}))}},notifyListeners:function(){for(var e=arguments.length,n=new Array(e),r=0;r<e;r++)n[r]=arguments[r];t.forEach((function(e){return e.apply(void 0,n)}))}}}var g=!("undefined"==typeof window||!window.document||!window.document.createElement);function h(e,t){t(window.confirm(e))}var b="popstate",y="hashchange";function v(){try{return window.history.state||{}}catch(e){return{}}}function w(e){void 0===e&&(e={}),g||(0,l.Z)(!1);var t,n=window.history,o=(-1===(t=window.navigator.userAgent).indexOf("Android 2.")&&-1===t.indexOf("Android 4.0")||-1===t.indexOf("Mobile Safari")||-1!==t.indexOf("Chrome")||-1!==t.indexOf("Windows Phone"))&&window.history&&"pushState"in window.history,a=!(-1===window.navigator.userAgent.indexOf("Trident")),i=e,c=i.forceRefresh,w=void 0!==c&&c,k=i.getUserConfirmation,x=void 0===k?h:k,S=i.keyLength,E=void 0===S?6:S,_=e.basename?d(s(e.basename)):"";function C(e){var t=e||{},n=t.key,r=t.state,o=window.location,a=o.pathname+o.search+o.hash;return _&&(a=u(a,_)),f(a,r,n)}function T(){return Math.random().toString(36).substr(2,E)}var j=m();function A(e){(0,r.Z)($,e),$.length=n.length,j.notifyListeners($.location,$.action)}function L(e){(function(e){return void 0===e.state&&-1===navigator.userAgent.indexOf("CriOS")})(e)||P(C(e.state))}function N(){P(C(v()))}var R=!1;function P(e){if(R)R=!1,A();else{j.confirmTransitionTo(e,"POP",x,(function(t){t?A({action:"POP",location:e}):function(e){var t=$.location,n=D.indexOf(t.key);-1===n&&(n=0);var r=D.indexOf(e.key);-1===r&&(r=0);var o=n-r;o&&(R=!0,F(o))}(e)}))}}var O=C(v()),D=[O.key];function I(e){return _+p(e)}function F(e){n.go(e)}var M=0;function z(e){1===(M+=e)&&1===e?(window.addEventListener(b,L),a&&window.addEventListener(y,N)):0===M&&(window.removeEventListener(b,L),a&&window.removeEventListener(y,N))}var B=!1;var $={length:n.length,action:"POP",location:O,createHref:I,push:function(e,t){var r="PUSH",a=f(e,t,T(),$.location);j.confirmTransitionTo(a,r,x,(function(e){if(e){var t=I(a),i=a.key,l=a.state;if(o)if(n.pushState({key:i,state:l},null,t),w)window.location.href=t;else{var s=D.indexOf($.location.key),c=D.slice(0,s+1);c.push(a.key),D=c,A({action:r,location:a})}else window.location.href=t}}))},replace:function(e,t){var r="REPLACE",a=f(e,t,T(),$.location);j.confirmTransitionTo(a,r,x,(function(e){if(e){var t=I(a),i=a.key,l=a.state;if(o)if(n.replaceState({key:i,state:l},null,t),w)window.location.replace(t);else{var s=D.indexOf($.location.key);-1!==s&&(D[s]=a.key),A({action:r,location:a})}else window.location.replace(t)}}))},go:F,goBack:function(){F(-1)},goForward:function(){F(1)},block:function(e){void 0===e&&(e=!1);var t=j.setPrompt(e);return B||(z(1),B=!0),function(){return B&&(B=!1,z(-1)),t()}},listen:function(e){var t=j.appendListener(e);return z(1),function(){z(-1),t()}}};return $}var k="hashchange",x={hashbang:{encodePath:function(e){return"!"===e.charAt(0)?e:"!/"+c(e)},decodePath:function(e){return"!"===e.charAt(0)?e.substr(1):e}},noslash:{encodePath:c,decodePath:s},slash:{encodePath:s,decodePath:s}};function S(e){var t=e.indexOf("#");return-1===t?e:e.slice(0,t)}function E(){var e=window.location.href,t=e.indexOf("#");return-1===t?"":e.substring(t+1)}function _(e){window.location.replace(S(window.location.href)+"#"+e)}function C(e){void 0===e&&(e={}),g||(0,l.Z)(!1);var t=window.history,n=(window.navigator.userAgent.indexOf("Firefox"),e),o=n.getUserConfirmation,a=void 0===o?h:o,i=n.hashType,c=void 0===i?"slash":i,b=e.basename?d(s(e.basename)):"",y=x[c],v=y.encodePath,w=y.decodePath;function C(){var e=w(E());return b&&(e=u(e,b)),f(e)}var T=m();function j(e){(0,r.Z)(B,e),B.length=t.length,T.notifyListeners(B.location,B.action)}var A=!1,L=null;function N(){var e,t,n=E(),r=v(n);if(n!==r)_(r);else{var o=C(),i=B.location;if(!A&&(t=o,(e=i).pathname===t.pathname&&e.search===t.search&&e.hash===t.hash))return;if(L===p(o))return;L=null,function(e){if(A)A=!1,j();else{var t="POP";T.confirmTransitionTo(e,t,a,(function(n){n?j({action:t,location:e}):function(e){var t=B.location,n=D.lastIndexOf(p(t));-1===n&&(n=0);var r=D.lastIndexOf(p(e));-1===r&&(r=0);var o=n-r;o&&(A=!0,I(o))}(e)}))}}(o)}}var R=E(),P=v(R);R!==P&&_(P);var O=C(),D=[p(O)];function I(e){t.go(e)}var F=0;function M(e){1===(F+=e)&&1===e?window.addEventListener(k,N):0===F&&window.removeEventListener(k,N)}var z=!1;var B={length:t.length,action:"POP",location:O,createHref:function(e){var t=document.querySelector("base"),n="";return t&&t.getAttribute("href")&&(n=S(window.location.href)),n+"#"+v(b+p(e))},push:function(e,t){var n="PUSH",r=f(e,void 0,void 0,B.location);T.confirmTransitionTo(r,n,a,(function(e){if(e){var t=p(r),o=v(b+t);if(E()!==o){L=t,function(e){window.location.hash=e}(o);var a=D.lastIndexOf(p(B.location)),i=D.slice(0,a+1);i.push(t),D=i,j({action:n,location:r})}else j()}}))},replace:function(e,t){var n="REPLACE",r=f(e,void 0,void 0,B.location);T.confirmTransitionTo(r,n,a,(function(e){if(e){var t=p(r),o=v(b+t);E()!==o&&(L=t,_(o));var a=D.indexOf(p(B.location));-1!==a&&(D[a]=t),j({action:n,location:r})}}))},go:I,goBack:function(){I(-1)},goForward:function(){I(1)},block:function(e){void 0===e&&(e=!1);var t=T.setPrompt(e);return z||(M(1),z=!0),function(){return z&&(z=!1,M(-1)),t()}},listen:function(e){var t=T.appendListener(e);return M(1),function(){M(-1),t()}}};return B}function T(e,t,n){return Math.min(Math.max(e,t),n)}function j(e){void 0===e&&(e={});var t=e,n=t.getUserConfirmation,o=t.initialEntries,a=void 0===o?["/"]:o,i=t.initialIndex,l=void 0===i?0:i,s=t.keyLength,c=void 0===s?6:s,u=m();function d(e){(0,r.Z)(w,e),w.length=w.entries.length,u.notifyListeners(w.location,w.action)}function g(){return Math.random().toString(36).substr(2,c)}var h=T(l,0,a.length-1),b=a.map((function(e){return f(e,void 0,"string"==typeof e?g():e.key||g())})),y=p;function v(e){var t=T(w.index+e,0,w.entries.length-1),r=w.entries[t];u.confirmTransitionTo(r,"POP",n,(function(e){e?d({action:"POP",location:r,index:t}):d()}))}var w={length:b.length,action:"POP",location:b[h],index:h,entries:b,createHref:y,push:function(e,t){var r="PUSH",o=f(e,t,g(),w.location);u.confirmTransitionTo(o,r,n,(function(e){if(e){var t=w.index+1,n=w.entries.slice(0);n.length>t?n.splice(t,n.length-t,o):n.push(o),d({action:r,location:o,index:t,entries:n})}}))},replace:function(e,t){var r="REPLACE",o=f(e,t,g(),w.location);u.confirmTransitionTo(o,r,n,(function(e){e&&(w.entries[w.index]=o,d({action:r,location:o}))}))},go:v,goBack:function(){v(-1)},goForward:function(){v(1)},canGo:function(e){var t=w.index+e;return t>=0&&t<w.entries.length},block:function(e){return void 0===e&&(e=!1),u.setPrompt(e)},listen:function(e){return u.appendListener(e)}};return w}},8679:(e,t,n)=>{"use strict";var r=n(9864),o={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},a={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},i={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},l={};function s(e){return r.isMemo(e)?i:l[e.$$typeof]||o}l[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},l[r.Memo]=i;var c=Object.defineProperty,u=Object.getOwnPropertyNames,d=Object.getOwnPropertySymbols,p=Object.getOwnPropertyDescriptor,f=Object.getPrototypeOf,m=Object.prototype;e.exports=function e(t,n,r){if("string"!=typeof n){if(m){var o=f(n);o&&o!==m&&e(t,o,r)}var i=u(n);d&&(i=i.concat(d(n)));for(var l=s(t),g=s(n),h=0;h<i.length;++h){var b=i[h];if(!(a[b]||r&&r[b]||g&&g[b]||l&&l[b])){var y=p(n,b);try{c(t,b,y)}catch(v){}}}}return t}},1143:e=>{"use strict";e.exports=function(e,t,n,r,o,a,i,l){if(!e){var s;if(void 0===t)s=new Error("Minified exception occurred; use the non-minified dev environment for the full error message and additional helpful warnings.");else{var c=[n,r,o,a,i,l],u=0;(s=new Error(t.replace(/%s/g,(function(){return c[u++]})))).name="Invariant Violation"}throw s.framesToPop=1,s}}},5826:e=>{e.exports=Array.isArray||function(e){return"[object Array]"==Object.prototype.toString.call(e)}},2497:(e,t,n)=>{"use strict";n.r(t)},2295:(e,t,n)=>{"use strict";n.r(t)},4865:function(e,t,n){var r,o;r=function(){var e,t,n={version:"0.2.0"},r=n.settings={minimum:.08,easing:"ease",positionUsing:"",speed:200,trickle:!0,trickleRate:.02,trickleSpeed:800,showSpinner:!0,barSelector:'[role="bar"]',spinnerSelector:'[role="spinner"]',parent:"body",template:'<div class="bar" role="bar"><div class="peg"></div></div><div class="spinner" role="spinner"><div class="spinner-icon"></div></div>'};function o(e,t,n){return e<t?t:e>n?n:e}function a(e){return 100*(-1+e)}function i(e,t,n){var o;return(o="translate3d"===r.positionUsing?{transform:"translate3d("+a(e)+"%,0,0)"}:"translate"===r.positionUsing?{transform:"translate("+a(e)+"%,0)"}:{"margin-left":a(e)+"%"}).transition="all "+t+"ms "+n,o}n.configure=function(e){var t,n;for(t in e)void 0!==(n=e[t])&&e.hasOwnProperty(t)&&(r[t]=n);return this},n.status=null,n.set=function(e){var t=n.isStarted();e=o(e,r.minimum,1),n.status=1===e?null:e;var a=n.render(!t),c=a.querySelector(r.barSelector),u=r.speed,d=r.easing;return a.offsetWidth,l((function(t){""===r.positionUsing&&(r.positionUsing=n.getPositioningCSS()),s(c,i(e,u,d)),1===e?(s(a,{transition:"none",opacity:1}),a.offsetWidth,setTimeout((function(){s(a,{transition:"all "+u+"ms linear",opacity:0}),setTimeout((function(){n.remove(),t()}),u)}),u)):setTimeout(t,u)})),this},n.isStarted=function(){return"number"==typeof n.status},n.start=function(){n.status||n.set(0);var e=function(){setTimeout((function(){n.status&&(n.trickle(),e())}),r.trickleSpeed)};return r.trickle&&e(),this},n.done=function(e){return e||n.status?n.inc(.3+.5*Math.random()).set(1):this},n.inc=function(e){var t=n.status;return t?("number"!=typeof e&&(e=(1-t)*o(Math.random()*t,.1,.95)),t=o(t+e,0,.994),n.set(t)):n.start()},n.trickle=function(){return n.inc(Math.random()*r.trickleRate)},e=0,t=0,n.promise=function(r){return r&&"resolved"!==r.state()?(0===t&&n.start(),e++,t++,r.always((function(){0==--t?(e=0,n.done()):n.set((e-t)/e)})),this):this},n.render=function(e){if(n.isRendered())return document.getElementById("nprogress");u(document.documentElement,"nprogress-busy");var t=document.createElement("div");t.id="nprogress",t.innerHTML=r.template;var o,i=t.querySelector(r.barSelector),l=e?"-100":a(n.status||0),c=document.querySelector(r.parent);return s(i,{transition:"all 0 linear",transform:"translate3d("+l+"%,0,0)"}),r.showSpinner||(o=t.querySelector(r.spinnerSelector))&&f(o),c!=document.body&&u(c,"nprogress-custom-parent"),c.appendChild(t),t},n.remove=function(){d(document.documentElement,"nprogress-busy"),d(document.querySelector(r.parent),"nprogress-custom-parent");var e=document.getElementById("nprogress");e&&f(e)},n.isRendered=function(){return!!document.getElementById("nprogress")},n.getPositioningCSS=function(){var e=document.body.style,t="WebkitTransform"in e?"Webkit":"MozTransform"in e?"Moz":"msTransform"in e?"ms":"OTransform"in e?"O":"";return t+"Perspective"in e?"translate3d":t+"Transform"in e?"translate":"margin"};var l=function(){var e=[];function t(){var n=e.shift();n&&n(t)}return function(n){e.push(n),1==e.length&&t()}}(),s=function(){var e=["Webkit","O","Moz","ms"],t={};function n(e){return e.replace(/^-ms-/,"ms-").replace(/-([\da-z])/gi,(function(e,t){return t.toUpperCase()}))}function r(t){var n=document.body.style;if(t in n)return t;for(var r,o=e.length,a=t.charAt(0).toUpperCase()+t.slice(1);o--;)if((r=e[o]+a)in n)return r;return t}function o(e){return e=n(e),t[e]||(t[e]=r(e))}function a(e,t,n){t=o(t),e.style[t]=n}return function(e,t){var n,r,o=arguments;if(2==o.length)for(n in t)void 0!==(r=t[n])&&t.hasOwnProperty(n)&&a(e,n,r);else a(e,o[1],o[2])}}();function c(e,t){return("string"==typeof e?e:p(e)).indexOf(" "+t+" ")>=0}function u(e,t){var n=p(e),r=n+t;c(n,t)||(e.className=r.substring(1))}function d(e,t){var n,r=p(e);c(e,t)&&(n=r.replace(" "+t+" "," "),e.className=n.substring(1,n.length-1))}function p(e){return(" "+(e.className||"")+" ").replace(/\s+/gi," ")}function f(e){e&&e.parentNode&&e.parentNode.removeChild(e)}return n},void 0===(o="function"==typeof r?r.call(t,n,t,e):r)||(e.exports=o)},4779:(e,t,n)=>{var r=n(5826);e.exports=f,e.exports.parse=a,e.exports.compile=function(e,t){return l(a(e,t),t)},e.exports.tokensToFunction=l,e.exports.tokensToRegExp=p;var o=new RegExp(["(\\\\.)","([\\/.])?(?:(?:\\:(\\w+)(?:\\(((?:\\\\.|[^\\\\()])+)\\))?|\\(((?:\\\\.|[^\\\\()])+)\\))([+*?])?|(\\*))"].join("|"),"g");function a(e,t){for(var n,r=[],a=0,i=0,l="",u=t&&t.delimiter||"/";null!=(n=o.exec(e));){var d=n[0],p=n[1],f=n.index;if(l+=e.slice(i,f),i=f+d.length,p)l+=p[1];else{var m=e[i],g=n[2],h=n[3],b=n[4],y=n[5],v=n[6],w=n[7];l&&(r.push(l),l="");var k=null!=g&&null!=m&&m!==g,x="+"===v||"*"===v,S="?"===v||"*"===v,E=n[2]||u,_=b||y;r.push({name:h||a++,prefix:g||"",delimiter:E,optional:S,repeat:x,partial:k,asterisk:!!w,pattern:_?c(_):w?".*":"[^"+s(E)+"]+?"})}}return i<e.length&&(l+=e.substr(i)),l&&r.push(l),r}function i(e){return encodeURI(e).replace(/[\/?#]/g,(function(e){return"%"+e.charCodeAt(0).toString(16).toUpperCase()}))}function l(e,t){for(var n=new Array(e.length),o=0;o<e.length;o++)"object"==typeof e[o]&&(n[o]=new RegExp("^(?:"+e[o].pattern+")$",d(t)));return function(t,o){for(var a="",l=t||{},s=(o||{}).pretty?i:encodeURIComponent,c=0;c<e.length;c++){var u=e[c];if("string"!=typeof u){var d,p=l[u.name];if(null==p){if(u.optional){u.partial&&(a+=u.prefix);continue}throw new TypeError('Expected "'+u.name+'" to be defined')}if(r(p)){if(!u.repeat)throw new TypeError('Expected "'+u.name+'" to not repeat, but received `'+JSON.stringify(p)+"`");if(0===p.length){if(u.optional)continue;throw new TypeError('Expected "'+u.name+'" to not be empty')}for(var f=0;f<p.length;f++){if(d=s(p[f]),!n[c].test(d))throw new TypeError('Expected all "'+u.name+'" to match "'+u.pattern+'", but received `'+JSON.stringify(d)+"`");a+=(0===f?u.prefix:u.delimiter)+d}}else{if(d=u.asterisk?encodeURI(p).replace(/[?#]/g,(function(e){return"%"+e.charCodeAt(0).toString(16).toUpperCase()})):s(p),!n[c].test(d))throw new TypeError('Expected "'+u.name+'" to match "'+u.pattern+'", but received "'+d+'"');a+=u.prefix+d}}else a+=u}return a}}function s(e){return e.replace(/([.+*?=^!:${}()[\]|\/\\])/g,"\\$1")}function c(e){return e.replace(/([=!:$\/()])/g,"\\$1")}function u(e,t){return e.keys=t,e}function d(e){return e&&e.sensitive?"":"i"}function p(e,t,n){r(t)||(n=t||n,t=[]);for(var o=(n=n||{}).strict,a=!1!==n.end,i="",l=0;l<e.length;l++){var c=e[l];if("string"==typeof c)i+=s(c);else{var p=s(c.prefix),f="(?:"+c.pattern+")";t.push(c),c.repeat&&(f+="(?:"+p+f+")*"),i+=f=c.optional?c.partial?p+"("+f+")?":"(?:"+p+"("+f+"))?":p+"("+f+")"}}var m=s(n.delimiter||"/"),g=i.slice(-m.length)===m;return o||(i=(g?i.slice(0,-m.length):i)+"(?:"+m+"(?=$))?"),i+=a?"$":o&&g?"":"(?="+m+"|$)",u(new RegExp("^"+i,d(n)),t)}function f(e,t,n){return r(t)||(n=t||n,t=[]),n=n||{},e instanceof RegExp?function(e,t){var n=e.source.match(/\((?!\?)/g);if(n)for(var r=0;r<n.length;r++)t.push({name:r,prefix:null,delimiter:null,optional:!1,repeat:!1,partial:!1,asterisk:!1,pattern:null});return u(e,t)}(e,t):r(e)?function(e,t,n){for(var r=[],o=0;o<e.length;o++)r.push(f(e[o],t,n).source);return u(new RegExp("(?:"+r.join("|")+")",d(n)),t)}(e,t,n):function(e,t,n){return p(a(e,n),t,n)}(e,t,n)}},9901:e=>{e.exports&&(e.exports={core:{meta:{path:"components/prism-core.js",option:"mandatory"},core:"Core"},themes:{meta:{path:"themes/{id}.css",link:"index.html?theme={id}",exclusive:!0},prism:{title:"Default",option:"default"},"prism-dark":"Dark","prism-funky":"Funky","prism-okaidia":{title:"Okaidia",owner:"ocodia"},"prism-twilight":{title:"Twilight",owner:"remybach"},"prism-coy":{title:"Coy",owner:"tshedor"},"prism-solarizedlight":{title:"Solarized Light",owner:"hectormatos2011 "},"prism-tomorrow":{title:"Tomorrow Night",owner:"Rosey"}},languages:{meta:{path:"components/prism-{id}",noCSS:!0,examplesPath:"examples/prism-{id}",addCheckAll:!0},markup:{title:"Markup",alias:["html","xml","svg","mathml","ssml","atom","rss"],aliasTitles:{html:"HTML",xml:"XML",svg:"SVG",mathml:"MathML",ssml:"SSML",atom:"Atom",rss:"RSS"},option:"default"},css:{title:"CSS",option:"default",modify:"markup"},clike:{title:"C-like",option:"default"},javascript:{title:"JavaScript",require:"clike",modify:"markup",optional:"regex",alias:"js",option:"default"},abap:{title:"ABAP",owner:"dellagustin"},abnf:{title:"ABNF",owner:"RunDevelopment"},actionscript:{title:"ActionScript",require:"javascript",modify:"markup",owner:"Golmote"},ada:{title:"Ada",owner:"Lucretia"},agda:{title:"Agda",owner:"xy-ren"},al:{title:"AL",owner:"RunDevelopment"},antlr4:{title:"ANTLR4",alias:"g4",owner:"RunDevelopment"},apacheconf:{title:"Apache Configuration",owner:"GuiTeK"},apex:{title:"Apex",require:["clike","sql"],owner:"RunDevelopment"},apl:{title:"APL",owner:"ngn"},applescript:{title:"AppleScript",owner:"Golmote"},aql:{title:"AQL",owner:"RunDevelopment"},arduino:{title:"Arduino",require:"cpp",alias:"ino",owner:"dkern"},arff:{title:"ARFF",owner:"Golmote"},armasm:{title:"ARM Assembly",alias:"arm-asm",owner:"RunDevelopment"},arturo:{title:"Arturo",alias:"art",optional:["bash","css","javascript","markup","markdown","sql"],owner:"drkameleon"},asciidoc:{alias:"adoc",title:"AsciiDoc",owner:"Golmote"},aspnet:{title:"ASP.NET (C#)",require:["markup","csharp"],owner:"nauzilus"},asm6502:{title:"6502 Assembly",owner:"kzurawel"},asmatmel:{title:"Atmel AVR Assembly",owner:"cerkit"},autohotkey:{title:"AutoHotkey",owner:"aviaryan"},autoit:{title:"AutoIt",owner:"Golmote"},avisynth:{title:"AviSynth",alias:"avs",owner:"Zinfidel"},"avro-idl":{title:"Avro IDL",alias:"avdl",owner:"RunDevelopment"},awk:{title:"AWK",alias:"gawk",aliasTitles:{gawk:"GAWK"},owner:"RunDevelopment"},bash:{title:"Bash",alias:["sh","shell"],aliasTitles:{sh:"Shell",shell:"Shell"},owner:"zeitgeist87"},basic:{title:"BASIC",owner:"Golmote"},batch:{title:"Batch",owner:"Golmote"},bbcode:{title:"BBcode",alias:"shortcode",aliasTitles:{shortcode:"Shortcode"},owner:"RunDevelopment"},bbj:{title:"BBj",owner:"hyyan"},bicep:{title:"Bicep",owner:"johnnyreilly"},birb:{title:"Birb",require:"clike",owner:"Calamity210"},bison:{title:"Bison",require:"c",owner:"Golmote"},bnf:{title:"BNF",alias:"rbnf",aliasTitles:{rbnf:"RBNF"},owner:"RunDevelopment"},bqn:{title:"BQN",owner:"yewscion"},brainfuck:{title:"Brainfuck",owner:"Golmote"},brightscript:{title:"BrightScript",owner:"RunDevelopment"},bro:{title:"Bro",owner:"wayward710"},bsl:{title:"BSL (1C:Enterprise)",alias:"oscript",aliasTitles:{oscript:"OneScript"},owner:"Diversus23"},c:{title:"C",require:"clike",owner:"zeitgeist87"},csharp:{title:"C#",require:"clike",alias:["cs","dotnet"],owner:"mvalipour"},cpp:{title:"C++",require:"c",owner:"zeitgeist87"},cfscript:{title:"CFScript",require:"clike",alias:"cfc",owner:"mjclemente"},chaiscript:{title:"ChaiScript",require:["clike","cpp"],owner:"RunDevelopment"},cil:{title:"CIL",owner:"sbrl"},cilkc:{title:"Cilk/C",require:"c",alias:"cilk-c",owner:"OpenCilk"},cilkcpp:{title:"Cilk/C++",require:"cpp",alias:["cilk-cpp","cilk"],owner:"OpenCilk"},clojure:{title:"Clojure",owner:"troglotit"},cmake:{title:"CMake",owner:"mjrogozinski"},cobol:{title:"COBOL",owner:"RunDevelopment"},coffeescript:{title:"CoffeeScript",require:"javascript",alias:"coffee",owner:"R-osey"},concurnas:{title:"Concurnas",alias:"conc",owner:"jasontatton"},csp:{title:"Content-Security-Policy",owner:"ScottHelme"},cooklang:{title:"Cooklang",owner:"ahue"},coq:{title:"Coq",owner:"RunDevelopment"},crystal:{title:"Crystal",require:"ruby",owner:"MakeNowJust"},"css-extras":{title:"CSS Extras",require:"css",modify:"css",owner:"milesj"},csv:{title:"CSV",owner:"RunDevelopment"},cue:{title:"CUE",owner:"RunDevelopment"},cypher:{title:"Cypher",owner:"RunDevelopment"},d:{title:"D",require:"clike",owner:"Golmote"},dart:{title:"Dart",require:"clike",owner:"Golmote"},dataweave:{title:"DataWeave",owner:"machaval"},dax:{title:"DAX",owner:"peterbud"},dhall:{title:"Dhall",owner:"RunDevelopment"},diff:{title:"Diff",owner:"uranusjr"},django:{title:"Django/Jinja2",require:"markup-templating",alias:"jinja2",owner:"romanvm"},"dns-zone-file":{title:"DNS zone file",owner:"RunDevelopment",alias:"dns-zone"},docker:{title:"Docker",alias:"dockerfile",owner:"JustinBeckwith"},dot:{title:"DOT (Graphviz)",alias:"gv",optional:"markup",owner:"RunDevelopment"},ebnf:{title:"EBNF",owner:"RunDevelopment"},editorconfig:{title:"EditorConfig",owner:"osipxd"},eiffel:{title:"Eiffel",owner:"Conaclos"},ejs:{title:"EJS",require:["javascript","markup-templating"],owner:"RunDevelopment",alias:"eta",aliasTitles:{eta:"Eta"}},elixir:{title:"Elixir",owner:"Golmote"},elm:{title:"Elm",owner:"zwilias"},etlua:{title:"Embedded Lua templating",require:["lua","markup-templating"],owner:"RunDevelopment"},erb:{title:"ERB",require:["ruby","markup-templating"],owner:"Golmote"},erlang:{title:"Erlang",owner:"Golmote"},"excel-formula":{title:"Excel Formula",alias:["xlsx","xls"],owner:"RunDevelopment"},fsharp:{title:"F#",require:"clike",owner:"simonreynolds7"},factor:{title:"Factor",owner:"catb0t"},false:{title:"False",owner:"edukisto"},"firestore-security-rules":{title:"Firestore security rules",require:"clike",owner:"RunDevelopment"},flow:{title:"Flow",require:"javascript",owner:"Golmote"},fortran:{title:"Fortran",owner:"Golmote"},ftl:{title:"FreeMarker Template Language",require:"markup-templating",owner:"RunDevelopment"},gml:{title:"GameMaker Language",alias:"gamemakerlanguage",require:"clike",owner:"LiarOnce"},gap:{title:"GAP (CAS)",owner:"RunDevelopment"},gcode:{title:"G-code",owner:"RunDevelopment"},gdscript:{title:"GDScript",owner:"RunDevelopment"},gedcom:{title:"GEDCOM",owner:"Golmote"},gettext:{title:"gettext",alias:"po",owner:"RunDevelopment"},gherkin:{title:"Gherkin",owner:"hason"},git:{title:"Git",owner:"lgiraudel"},glsl:{title:"GLSL",require:"c",owner:"Golmote"},gn:{title:"GN",alias:"gni",owner:"RunDevelopment"},"linker-script":{title:"GNU Linker Script",alias:"ld",owner:"RunDevelopment"},go:{title:"Go",require:"clike",owner:"arnehormann"},"go-module":{title:"Go module",alias:"go-mod",owner:"RunDevelopment"},gradle:{title:"Gradle",require:"clike",owner:"zeabdelkhalek-badido18"},graphql:{title:"GraphQL",optional:"markdown",owner:"Golmote"},groovy:{title:"Groovy",require:"clike",owner:"robfletcher"},haml:{title:"Haml",require:"ruby",optional:["css","css-extras","coffeescript","erb","javascript","less","markdown","scss","textile"],owner:"Golmote"},handlebars:{title:"Handlebars",require:"markup-templating",alias:["hbs","mustache"],aliasTitles:{mustache:"Mustache"},owner:"Golmote"},haskell:{title:"Haskell",alias:"hs",owner:"bholst"},haxe:{title:"Haxe",require:"clike",optional:"regex",owner:"Golmote"},hcl:{title:"HCL",owner:"outsideris"},hlsl:{title:"HLSL",require:"c",owner:"RunDevelopment"},hoon:{title:"Hoon",owner:"matildepark"},http:{title:"HTTP",optional:["csp","css","hpkp","hsts","javascript","json","markup","uri"],owner:"danielgtaylor"},hpkp:{title:"HTTP Public-Key-Pins",owner:"ScottHelme"},hsts:{title:"HTTP Strict-Transport-Security",owner:"ScottHelme"},ichigojam:{title:"IchigoJam",owner:"BlueCocoa"},icon:{title:"Icon",owner:"Golmote"},"icu-message-format":{title:"ICU Message Format",owner:"RunDevelopment"},idris:{title:"Idris",alias:"idr",owner:"KeenS",require:"haskell"},ignore:{title:".ignore",owner:"osipxd",alias:["gitignore","hgignore","npmignore"],aliasTitles:{gitignore:".gitignore",hgignore:".hgignore",npmignore:".npmignore"}},inform7:{title:"Inform 7",owner:"Golmote"},ini:{title:"Ini",owner:"aviaryan"},io:{title:"Io",owner:"AlesTsurko"},j:{title:"J",owner:"Golmote"},java:{title:"Java",require:"clike",owner:"sherblot"},javadoc:{title:"JavaDoc",require:["markup","java","javadoclike"],modify:"java",optional:"scala",owner:"RunDevelopment"},javadoclike:{title:"JavaDoc-like",modify:["java","javascript","php"],owner:"RunDevelopment"},javastacktrace:{title:"Java stack trace",owner:"RunDevelopment"},jexl:{title:"Jexl",owner:"czosel"},jolie:{title:"Jolie",require:"clike",owner:"thesave"},jq:{title:"JQ",owner:"RunDevelopment"},jsdoc:{title:"JSDoc",require:["javascript","javadoclike","typescript"],modify:"javascript",optional:["actionscript","coffeescript"],owner:"RunDevelopment"},"js-extras":{title:"JS Extras",require:"javascript",modify:"javascript",optional:["actionscript","coffeescript","flow","n4js","typescript"],owner:"RunDevelopment"},json:{title:"JSON",alias:"webmanifest",aliasTitles:{webmanifest:"Web App Manifest"},owner:"CupOfTea696"},json5:{title:"JSON5",require:"json",owner:"RunDevelopment"},jsonp:{title:"JSONP",require:"json",owner:"RunDevelopment"},jsstacktrace:{title:"JS stack trace",owner:"sbrl"},"js-templates":{title:"JS Templates",require:"javascript",modify:"javascript",optional:["css","css-extras","graphql","markdown","markup","sql"],owner:"RunDevelopment"},julia:{title:"Julia",owner:"cdagnino"},keepalived:{title:"Keepalived Configure",owner:"dev-itsheng"},keyman:{title:"Keyman",owner:"mcdurdin"},kotlin:{title:"Kotlin",alias:["kt","kts"],aliasTitles:{kts:"Kotlin Script"},require:"clike",owner:"Golmote"},kumir:{title:"KuMir (\u041a\u0443\u041c\u0438\u0440)",alias:"kum",owner:"edukisto"},kusto:{title:"Kusto",owner:"RunDevelopment"},latex:{title:"LaTeX",alias:["tex","context"],aliasTitles:{tex:"TeX",context:"ConTeXt"},owner:"japborst"},latte:{title:"Latte",require:["clike","markup-templating","php"],owner:"nette"},less:{title:"Less",require:"css",optional:"css-extras",owner:"Golmote"},lilypond:{title:"LilyPond",require:"scheme",alias:"ly",owner:"RunDevelopment"},liquid:{title:"Liquid",require:"markup-templating",owner:"cinhtau"},lisp:{title:"Lisp",alias:["emacs","elisp","emacs-lisp"],owner:"JuanCaicedo"},livescript:{title:"LiveScript",owner:"Golmote"},llvm:{title:"LLVM IR",owner:"porglezomp"},log:{title:"Log file",optional:"javastacktrace",owner:"RunDevelopment"},lolcode:{title:"LOLCODE",owner:"Golmote"},lua:{title:"Lua",owner:"Golmote"},magma:{title:"Magma (CAS)",owner:"RunDevelopment"},makefile:{title:"Makefile",owner:"Golmote"},markdown:{title:"Markdown",require:"markup",optional:"yaml",alias:"md",owner:"Golmote"},"markup-templating":{title:"Markup templating",require:"markup",owner:"Golmote"},mata:{title:"Mata",owner:"RunDevelopment"},matlab:{title:"MATLAB",owner:"Golmote"},maxscript:{title:"MAXScript",owner:"RunDevelopment"},mel:{title:"MEL",owner:"Golmote"},mermaid:{title:"Mermaid",owner:"RunDevelopment"},metafont:{title:"METAFONT",owner:"LaeriExNihilo"},mizar:{title:"Mizar",owner:"Golmote"},mongodb:{title:"MongoDB",owner:"airs0urce",require:"javascript"},monkey:{title:"Monkey",owner:"Golmote"},moonscript:{title:"MoonScript",alias:"moon",owner:"RunDevelopment"},n1ql:{title:"N1QL",owner:"TMWilds"},n4js:{title:"N4JS",require:"javascript",optional:"jsdoc",alias:"n4jsd",owner:"bsmith-n4"},"nand2tetris-hdl":{title:"Nand To Tetris HDL",owner:"stephanmax"},naniscript:{title:"Naninovel Script",owner:"Elringus",alias:"nani"},nasm:{title:"NASM",owner:"rbmj"},neon:{title:"NEON",owner:"nette"},nevod:{title:"Nevod",owner:"nezaboodka"},nginx:{title:"nginx",owner:"volado"},nim:{title:"Nim",owner:"Golmote"},nix:{title:"Nix",owner:"Golmote"},nsis:{title:"NSIS",owner:"idleberg"},objectivec:{title:"Objective-C",require:"c",alias:"objc",owner:"uranusjr"},ocaml:{title:"OCaml",owner:"Golmote"},odin:{title:"Odin",owner:"edukisto"},opencl:{title:"OpenCL",require:"c",modify:["c","cpp"],owner:"Milania1"},openqasm:{title:"OpenQasm",alias:"qasm",owner:"RunDevelopment"},oz:{title:"Oz",owner:"Golmote"},parigp:{title:"PARI/GP",owner:"Golmote"},parser:{title:"Parser",require:"markup",owner:"Golmote"},pascal:{title:"Pascal",alias:"objectpascal",aliasTitles:{objectpascal:"Object Pascal"},owner:"Golmote"},pascaligo:{title:"Pascaligo",owner:"DefinitelyNotAGoat"},psl:{title:"PATROL Scripting Language",owner:"bertysentry"},pcaxis:{title:"PC-Axis",alias:"px",owner:"RunDevelopment"},peoplecode:{title:"PeopleCode",alias:"pcode",owner:"RunDevelopment"},perl:{title:"Perl",owner:"Golmote"},php:{title:"PHP",require:"markup-templating",owner:"milesj"},phpdoc:{title:"PHPDoc",require:["php","javadoclike"],modify:"php",owner:"RunDevelopment"},"php-extras":{title:"PHP Extras",require:"php",modify:"php",owner:"milesj"},"plant-uml":{title:"PlantUML",alias:"plantuml",owner:"RunDevelopment"},plsql:{title:"PL/SQL",require:"sql",owner:"Golmote"},powerquery:{title:"PowerQuery",alias:["pq","mscript"],owner:"peterbud"},powershell:{title:"PowerShell",owner:"nauzilus"},processing:{title:"Processing",require:"clike",owner:"Golmote"},prolog:{title:"Prolog",owner:"Golmote"},promql:{title:"PromQL",owner:"arendjr"},properties:{title:".properties",owner:"Golmote"},protobuf:{title:"Protocol Buffers",require:"clike",owner:"just-boris"},pug:{title:"Pug",require:["markup","javascript"],optional:["coffeescript","ejs","handlebars","less","livescript","markdown","scss","stylus","twig"],owner:"Golmote"},puppet:{title:"Puppet",owner:"Golmote"},pure:{title:"Pure",optional:["c","cpp","fortran"],owner:"Golmote"},purebasic:{title:"PureBasic",require:"clike",alias:"pbfasm",owner:"HeX0R101"},purescript:{title:"PureScript",require:"haskell",alias:"purs",owner:"sriharshachilakapati"},python:{title:"Python",alias:"py",owner:"multipetros"},qsharp:{title:"Q#",require:"clike",alias:"qs",owner:"fedonman"},q:{title:"Q (kdb+ database)",owner:"Golmote"},qml:{title:"QML",require:"javascript",owner:"RunDevelopment"},qore:{title:"Qore",require:"clike",owner:"temnroegg"},r:{title:"R",owner:"Golmote"},racket:{title:"Racket",require:"scheme",alias:"rkt",owner:"RunDevelopment"},cshtml:{title:"Razor C#",alias:"razor",require:["markup","csharp"],optional:["css","css-extras","javascript","js-extras"],owner:"RunDevelopment"},jsx:{title:"React JSX",require:["markup","javascript"],optional:["jsdoc","js-extras","js-templates"],owner:"vkbansal"},tsx:{title:"React TSX",require:["jsx","typescript"]},reason:{title:"Reason",require:"clike",owner:"Golmote"},regex:{title:"Regex",owner:"RunDevelopment"},rego:{title:"Rego",owner:"JordanSh"},renpy:{title:"Ren'py",alias:"rpy",owner:"HyuchiaDiego"},rescript:{title:"ReScript",alias:"res",owner:"vmarcosp"},rest:{title:"reST (reStructuredText)",owner:"Golmote"},rip:{title:"Rip",owner:"ravinggenius"},roboconf:{title:"Roboconf",owner:"Golmote"},robotframework:{title:"Robot Framework",alias:"robot",owner:"RunDevelopment"},ruby:{title:"Ruby",require:"clike",alias:"rb",owner:"samflores"},rust:{title:"Rust",owner:"Golmote"},sas:{title:"SAS",optional:["groovy","lua","sql"],owner:"Golmote"},sass:{title:"Sass (Sass)",require:"css",optional:"css-extras",owner:"Golmote"},scss:{title:"Sass (SCSS)",require:"css",optional:"css-extras",owner:"MoOx"},scala:{title:"Scala",require:"java",owner:"jozic"},scheme:{title:"Scheme",owner:"bacchus123"},"shell-session":{title:"Shell session",require:"bash",alias:["sh-session","shellsession"],owner:"RunDevelopment"},smali:{title:"Smali",owner:"RunDevelopment"},smalltalk:{title:"Smalltalk",owner:"Golmote"},smarty:{title:"Smarty",require:"markup-templating",optional:"php",owner:"Golmote"},sml:{title:"SML",alias:"smlnj",aliasTitles:{smlnj:"SML/NJ"},owner:"RunDevelopment"},solidity:{title:"Solidity (Ethereum)",alias:"sol",require:"clike",owner:"glachaud"},"solution-file":{title:"Solution file",alias:"sln",owner:"RunDevelopment"},soy:{title:"Soy (Closure Template)",require:"markup-templating",owner:"Golmote"},sparql:{title:"SPARQL",require:"turtle",owner:"Triply-Dev",alias:"rq"},"splunk-spl":{title:"Splunk SPL",owner:"RunDevelopment"},sqf:{title:"SQF: Status Quo Function (Arma 3)",require:"clike",owner:"RunDevelopment"},sql:{title:"SQL",owner:"multipetros"},squirrel:{title:"Squirrel",require:"clike",owner:"RunDevelopment"},stan:{title:"Stan",owner:"RunDevelopment"},stata:{title:"Stata Ado",require:["mata","java","python"],owner:"RunDevelopment"},iecst:{title:"Structured Text (IEC 61131-3)",owner:"serhioromano"},stylus:{title:"Stylus",owner:"vkbansal"},supercollider:{title:"SuperCollider",alias:"sclang",owner:"RunDevelopment"},swift:{title:"Swift",owner:"chrischares"},systemd:{title:"Systemd configuration file",owner:"RunDevelopment"},"t4-templating":{title:"T4 templating",owner:"RunDevelopment"},"t4-cs":{title:"T4 Text Templates (C#)",require:["t4-templating","csharp"],alias:"t4",owner:"RunDevelopment"},"t4-vb":{title:"T4 Text Templates (VB)",require:["t4-templating","vbnet"],owner:"RunDevelopment"},tap:{title:"TAP",owner:"isaacs",require:"yaml"},tcl:{title:"Tcl",owner:"PeterChaplin"},tt2:{title:"Template Toolkit 2",require:["clike","markup-templating"],owner:"gflohr"},textile:{title:"Textile",require:"markup",optional:"css",owner:"Golmote"},toml:{title:"TOML",owner:"RunDevelopment"},tremor:{title:"Tremor",alias:["trickle","troy"],owner:"darach",aliasTitles:{trickle:"trickle",troy:"troy"}},turtle:{title:"Turtle",alias:"trig",aliasTitles:{trig:"TriG"},owner:"jakubklimek"},twig:{title:"Twig",require:"markup-templating",owner:"brandonkelly"},typescript:{title:"TypeScript",require:"javascript",optional:"js-templates",alias:"ts",owner:"vkbansal"},typoscript:{title:"TypoScript",alias:"tsconfig",aliasTitles:{tsconfig:"TSConfig"},owner:"dkern"},unrealscript:{title:"UnrealScript",alias:["uscript","uc"],owner:"RunDevelopment"},uorazor:{title:"UO Razor Script",owner:"jaseowns"},uri:{title:"URI",alias:"url",aliasTitles:{url:"URL"},owner:"RunDevelopment"},v:{title:"V",require:"clike",owner:"taggon"},vala:{title:"Vala",require:"clike",optional:"regex",owner:"TemplarVolk"},vbnet:{title:"VB.Net",require:"basic",owner:"Bigsby"},velocity:{title:"Velocity",require:"markup",owner:"Golmote"},verilog:{title:"Verilog",owner:"a-rey"},vhdl:{title:"VHDL",owner:"a-rey"},vim:{title:"vim",owner:"westonganger"},"visual-basic":{title:"Visual Basic",alias:["vb","vba"],aliasTitles:{vba:"VBA"},owner:"Golmote"},warpscript:{title:"WarpScript",owner:"RunDevelopment"},wasm:{title:"WebAssembly",owner:"Golmote"},"web-idl":{title:"Web IDL",alias:"webidl",owner:"RunDevelopment"},wgsl:{title:"WGSL",owner:"Dr4gonthree"},wiki:{title:"Wiki markup",require:"markup",owner:"Golmote"},wolfram:{title:"Wolfram language",alias:["mathematica","nb","wl"],aliasTitles:{mathematica:"Mathematica",nb:"Mathematica Notebook"},owner:"msollami"},wren:{title:"Wren",owner:"clsource"},xeora:{title:"Xeora",require:"markup",alias:"xeoracube",aliasTitles:{xeoracube:"XeoraCube"},owner:"freakmaxi"},"xml-doc":{title:"XML doc (.net)",require:"markup",modify:["csharp","fsharp","vbnet"],owner:"RunDevelopment"},xojo:{title:"Xojo (REALbasic)",owner:"Golmote"},xquery:{title:"XQuery",require:"markup",owner:"Golmote"},yaml:{title:"YAML",alias:"yml",owner:"hason"},yang:{title:"YANG",owner:"RunDevelopment"},zig:{title:"Zig",owner:"RunDevelopment"}},plugins:{meta:{path:"plugins/{id}/prism-{id}",link:"plugins/{id}/"},"line-highlight":{title:"Line Highlight",description:"Highlights specific lines and/or line ranges."},"line-numbers":{title:"Line Numbers",description:"Line number at the beginning of code lines.",owner:"kuba-kubula"},"show-invisibles":{title:"Show Invisibles",description:"Show hidden characters such as tabs and line breaks.",optional:["autolinker","data-uri-highlight"]},autolinker:{title:"Autolinker",description:"Converts URLs and emails in code to clickable links. Parses Markdown links in comments."},wpd:{title:"WebPlatform Docs",description:'Makes tokens link to <a href="https://webplatform.github.io/docs/">WebPlatform.org documentation</a>. The links open in a new tab.'},"custom-class":{title:"Custom Class",description:"This plugin allows you to prefix Prism's default classes (<code>.comment</code> can become <code>.namespace--comment</code>) or replace them with your defined ones (like <code>.editor__comment</code>). You can even add new classes.",owner:"dvkndn",noCSS:!0},"file-highlight":{title:"File Highlight",description:"Fetch external files and highlight them with Prism. Used on the Prism website itself.",noCSS:!0},"show-language":{title:"Show Language",description:"Display the highlighted language in code blocks (inline code does not show the label).",owner:"nauzilus",noCSS:!0,require:"toolbar"},"jsonp-highlight":{title:"JSONP Highlight",description:"Fetch content with JSONP and highlight some interesting content (e.g. GitHub/Gists or Bitbucket API).",noCSS:!0,owner:"nauzilus"},"highlight-keywords":{title:"Highlight Keywords",description:"Adds special CSS classes for each keyword for fine-grained highlighting.",owner:"vkbansal",noCSS:!0},"remove-initial-line-feed":{title:"Remove initial line feed",description:"Removes the initial line feed in code blocks.",owner:"Golmote",noCSS:!0},"inline-color":{title:"Inline color",description:"Adds a small inline preview for colors in style sheets.",require:"css-extras",owner:"RunDevelopment"},previewers:{title:"Previewers",description:"Previewers for angles, colors, gradients, easing and time.",require:"css-extras",owner:"Golmote"},autoloader:{title:"Autoloader",description:"Automatically loads the needed languages to highlight the code blocks.",owner:"Golmote",noCSS:!0},"keep-markup":{title:"Keep Markup",description:"Prevents custom markup from being dropped out during highlighting.",owner:"Golmote",optional:"normalize-whitespace",noCSS:!0},"command-line":{title:"Command Line",description:"Display a command line with a prompt and, optionally, the output/response from the commands.",owner:"chriswells0"},"unescaped-markup":{title:"Unescaped Markup",description:"Write markup without having to escape anything."},"normalize-whitespace":{title:"Normalize Whitespace",description:"Supports multiple operations to normalize whitespace in code blocks.",owner:"zeitgeist87",optional:"unescaped-markup",noCSS:!0},"data-uri-highlight":{title:"Data-URI Highlight",description:"Highlights data-URI contents.",owner:"Golmote",noCSS:!0},toolbar:{title:"Toolbar",description:"Attach a toolbar for plugins to easily register buttons on the top of a code block.",owner:"mAAdhaTTah"},"copy-to-clipboard":{title:"Copy to Clipboard Button",description:"Add a button that copies the code block to the clipboard when clicked.",owner:"mAAdhaTTah",require:"toolbar",noCSS:!0},"download-button":{title:"Download Button",description:"A button in the toolbar of a code block adding a convenient way to download a code file.",owner:"Golmote",require:"toolbar",noCSS:!0},"match-braces":{title:"Match braces",description:"Highlights matching braces.",owner:"RunDevelopment"},"diff-highlight":{title:"Diff Highlight",description:"Highlights the code inside diff blocks.",owner:"RunDevelopment",require:"diff"},"filter-highlight-all":{title:"Filter highlightAll",description:"Filters the elements the <code>highlightAll</code> and <code>highlightAllUnder</code> methods actually highlight.",owner:"RunDevelopment",noCSS:!0},treeview:{title:"Treeview",description:"A language with special styles to highlight file system tree structures.",owner:"Golmote"}}})},2885:(e,t,n)=>{const r=n(9901),o=n(9642),a=new Set;function i(e){void 0===e?e=Object.keys(r.languages).filter((e=>"meta"!=e)):Array.isArray(e)||(e=[e]);const t=[...a,...Object.keys(Prism.languages)];o(r,e,t).load((e=>{if(!(e in r.languages))return void(i.silent||console.warn("Language does not exist: "+e));const t="./prism-"+e;delete n.c[n(6500).resolve(t)],delete Prism.languages[e],n(6500)(t),a.add(e)}))}i.silent=!1,e.exports=i},6854:()=>{!function(e){function t(e,t){return"___"+e.toUpperCase()+t+"___"}Object.defineProperties(e.languages["markup-templating"]={},{buildPlaceholders:{value:function(n,r,o,a){if(n.language===r){var i=n.tokenStack=[];n.code=n.code.replace(o,(function(e){if("function"==typeof a&&!a(e))return e;for(var o,l=i.length;-1!==n.code.indexOf(o=t(r,l));)++l;return i[l]=e,o})),n.grammar=e.languages.markup}}},tokenizePlaceholders:{value:function(n,r){if(n.language===r&&n.tokenStack){n.grammar=e.languages[r];var o=0,a=Object.keys(n.tokenStack);!function i(l){for(var s=0;s<l.length&&!(o>=a.length);s++){var c=l[s];if("string"==typeof c||c.content&&"string"==typeof c.content){var u=a[o],d=n.tokenStack[u],p="string"==typeof c?c:c.content,f=t(r,u),m=p.indexOf(f);if(m>-1){++o;var g=p.substring(0,m),h=new e.Token(r,e.tokenize(d,n.grammar),"language-"+r,d),b=p.substring(m+f.length),y=[];g&&y.push.apply(y,i([g])),y.push(h),b&&y.push.apply(y,i([b])),"string"==typeof c?l.splice.apply(l,[s,1].concat(y)):c.content=y}}else c.content&&i(c.content)}return l}(n.tokens)}}}})}(Prism)},6726:(e,t,n)=>{var r={"./":2885};function o(e){var t=a(e);return n(t)}function a(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}o.keys=function(){return Object.keys(r)},o.resolve=a,e.exports=o,o.id=6726},6500:(e,t,n)=>{var r={"./":2885};function o(e){var t=a(e);return n(t)}function a(e){if(!n.o(r,e)){var t=new Error("Cannot find module '"+e+"'");throw t.code="MODULE_NOT_FOUND",t}return r[e]}o.keys=function(){return Object.keys(r)},o.resolve=a,e.exports=o,o.id=6500},9642:e=>{"use strict";var t=function(){var e=function(){};function t(e,t){Array.isArray(e)?e.forEach(t):null!=e&&t(e,0)}function n(e){for(var t={},n=0,r=e.length;n<r;n++)t[e[n]]=!0;return t}function r(e){var n={},r=[];function o(r,a){if(!(r in n)){a.push(r);var i=a.indexOf(r);if(i<a.length-1)throw new Error("Circular dependency: "+a.slice(i).join(" -> "));var l={},s=e[r];if(s){function c(t){if(!(t in e))throw new Error(r+" depends on an unknown component "+t);if(!(t in l))for(var i in o(t,a),l[t]=!0,n[t])l[i]=!0}t(s.require,c),t(s.optional,c),t(s.modify,c)}n[r]=l,a.pop()}}return function(e){var t=n[e];return t||(o(e,r),t=n[e]),t}}function o(e){for(var t in e)return!0;return!1}return function(a,i,l){var s=function(e){var t={};for(var n in e){var r=e[n];for(var o in r)if("meta"!=o){var a=r[o];t[o]="string"==typeof a?{title:a}:a}}return t}(a),c=function(e){var n;return function(r){if(r in e)return r;if(!n)for(var o in n={},e){var a=e[o];t(a&&a.alias,(function(t){if(t in n)throw new Error(t+" cannot be alias for both "+o+" and "+n[t]);if(t in e)throw new Error(t+" cannot be alias of "+o+" because it is a component.");n[t]=o}))}return n[r]||r}}(s);i=i.map(c),l=(l||[]).map(c);var u=n(i),d=n(l);i.forEach((function e(n){var r=s[n];t(r&&r.require,(function(t){t in d||(u[t]=!0,e(t))}))}));for(var p,f=r(s),m=u;o(m);){for(var g in p={},m){var h=s[g];t(h&&h.modify,(function(e){e in d&&(p[e]=!0)}))}for(var b in d)if(!(b in u))for(var y in f(b))if(y in u){p[b]=!0;break}for(var v in m=p)u[v]=!0}var w={getIds:function(){var e=[];return w.load((function(t){e.push(t)})),e},load:function(t,n){return function(t,n,r,o){var a=o?o.series:void 0,i=o?o.parallel:e,l={},s={};function c(e){if(e in l)return l[e];s[e]=!0;var o,u=[];for(var d in t(e))d in n&&u.push(d);if(0===u.length)o=r(e);else{var p=i(u.map((function(e){var t=c(e);return delete s[e],t})));a?o=a(p,(function(){return r(e)})):r(e)}return l[e]=o}for(var u in n)c(u);var d=[];for(var p in s)d.push(l[p]);return i(d)}(f,u,t,n)}};return w}}();e.exports=t},2703:(e,t,n)=>{"use strict";var r=n(414);function o(){}function a(){}a.resetWarningCache=o,e.exports=function(){function e(e,t,n,o,a,i){if(i!==r){var l=new Error("Calling PropTypes validators directly is not supported by the `prop-types` package. Use PropTypes.checkPropTypes() to call them. Read more at http://fb.me/use-check-prop-types");throw l.name="Invariant Violation",l}}function t(){return e}e.isRequired=e;var n={array:e,bigint:e,bool:e,func:e,number:e,object:e,string:e,symbol:e,any:e,arrayOf:t,element:e,elementType:e,instanceOf:t,node:e,objectOf:t,oneOf:t,oneOfType:t,shape:t,exact:t,checkPropTypes:a,resetWarningCache:o};return n.PropTypes=n,n}},5697:(e,t,n)=>{e.exports=n(2703)()},414:e=>{"use strict";e.exports="SECRET_DO_NOT_PASS_THIS_OR_YOU_WILL_BE_FIRED"},4448:(e,t,n)=>{"use strict";var r=n(7294),o=n(3840);function a(e){for(var t="https://reactjs.org/docs/error-decoder.html?invariant="+e,n=1;n<arguments.length;n++)t+="&args[]="+encodeURIComponent(arguments[n]);return"Minified React error #"+e+"; visit "+t+" for the full message or use the non-minified dev environment for full errors and additional helpful warnings."}var i=new Set,l={};function s(e,t){c(e,t),c(e+"Capture",t)}function c(e,t){for(l[e]=t,e=0;e<t.length;e++)i.add(t[e])}var u=!("undefined"==typeof window||void 0===window.document||void 0===window.document.createElement),d=Object.prototype.hasOwnProperty,p=/^[:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD][:A-Z_a-z\u00C0-\u00D6\u00D8-\u00F6\u00F8-\u02FF\u0370-\u037D\u037F-\u1FFF\u200C-\u200D\u2070-\u218F\u2C00-\u2FEF\u3001-\uD7FF\uF900-\uFDCF\uFDF0-\uFFFD\-.0-9\u00B7\u0300-\u036F\u203F-\u2040]*$/,f={},m={};function g(e,t,n,r,o,a,i){this.acceptsBooleans=2===t||3===t||4===t,this.attributeName=r,this.attributeNamespace=o,this.mustUseProperty=n,this.propertyName=e,this.type=t,this.sanitizeURL=a,this.removeEmptyString=i}var h={};"children dangerouslySetInnerHTML defaultValue defaultChecked innerHTML suppressContentEditableWarning suppressHydrationWarning style".split(" ").forEach((function(e){h[e]=new g(e,0,!1,e,null,!1,!1)})),[["acceptCharset","accept-charset"],["className","class"],["htmlFor","for"],["httpEquiv","http-equiv"]].forEach((function(e){var t=e[0];h[t]=new g(t,1,!1,e[1],null,!1,!1)})),["contentEditable","draggable","spellCheck","value"].forEach((function(e){h[e]=new g(e,2,!1,e.toLowerCase(),null,!1,!1)})),["autoReverse","externalResourcesRequired","focusable","preserveAlpha"].forEach((function(e){h[e]=new g(e,2,!1,e,null,!1,!1)})),"allowFullScreen async autoFocus autoPlay controls default defer disabled disablePictureInPicture disableRemotePlayback formNoValidate hidden loop noModule noValidate open playsInline readOnly required reversed scoped seamless itemScope".split(" ").forEach((function(e){h[e]=new g(e,3,!1,e.toLowerCase(),null,!1,!1)})),["checked","multiple","muted","selected"].forEach((function(e){h[e]=new g(e,3,!0,e,null,!1,!1)})),["capture","download"].forEach((function(e){h[e]=new g(e,4,!1,e,null,!1,!1)})),["cols","rows","size","span"].forEach((function(e){h[e]=new g(e,6,!1,e,null,!1,!1)})),["rowSpan","start"].forEach((function(e){h[e]=new g(e,5,!1,e.toLowerCase(),null,!1,!1)}));var b=/[\-:]([a-z])/g;function y(e){return e[1].toUpperCase()}function v(e,t,n,r){var o=h.hasOwnProperty(t)?h[t]:null;(null!==o?0!==o.type:r||!(2<t.length)||"o"!==t[0]&&"O"!==t[0]||"n"!==t[1]&&"N"!==t[1])&&(function(e,t,n,r){if(null==t||function(e,t,n,r){if(null!==n&&0===n.type)return!1;switch(typeof t){case"function":case"symbol":return!0;case"boolean":return!r&&(null!==n?!n.acceptsBooleans:"data-"!==(e=e.toLowerCase().slice(0,5))&&"aria-"!==e);default:return!1}}(e,t,n,r))return!0;if(r)return!1;if(null!==n)switch(n.type){case 3:return!t;case 4:return!1===t;case 5:return isNaN(t);case 6:return isNaN(t)||1>t}return!1}(t,n,o,r)&&(n=null),r||null===o?function(e){return!!d.call(m,e)||!d.call(f,e)&&(p.test(e)?m[e]=!0:(f[e]=!0,!1))}(t)&&(null===n?e.removeAttribute(t):e.setAttribute(t,""+n)):o.mustUseProperty?e[o.propertyName]=null===n?3!==o.type&&"":n:(t=o.attributeName,r=o.attributeNamespace,null===n?e.removeAttribute(t):(n=3===(o=o.type)||4===o&&!0===n?"":""+n,r?e.setAttributeNS(r,t,n):e.setAttribute(t,n))))}"accent-height alignment-baseline arabic-form baseline-shift cap-height clip-path clip-rule color-interpolation color-interpolation-filters color-profile color-rendering dominant-baseline enable-background fill-opacity fill-rule flood-color flood-opacity font-family font-size font-size-adjust font-stretch font-style font-variant font-weight glyph-name glyph-orientation-horizontal glyph-orientation-vertical horiz-adv-x horiz-origin-x image-rendering letter-spacing lighting-color marker-end marker-mid marker-start overline-position overline-thickness paint-order panose-1 pointer-events rendering-intent shape-rendering stop-color stop-opacity strikethrough-position strikethrough-thickness stroke-dasharray stroke-dashoffset stroke-linecap stroke-linejoin stroke-miterlimit stroke-opacity stroke-width text-anchor text-decoration text-rendering underline-position underline-thickness unicode-bidi unicode-range units-per-em v-alphabetic v-hanging v-ideographic v-mathematical vector-effect vert-adv-y vert-origin-x vert-origin-y word-spacing writing-mode xmlns:xlink x-height".split(" ").forEach((function(e){var t=e.replace(b,y);h[t]=new g(t,1,!1,e,null,!1,!1)})),"xlink:actuate xlink:arcrole xlink:role xlink:show xlink:title xlink:type".split(" ").forEach((function(e){var t=e.replace(b,y);h[t]=new g(t,1,!1,e,"http://www.w3.org/1999/xlink",!1,!1)})),["xml:base","xml:lang","xml:space"].forEach((function(e){var t=e.replace(b,y);h[t]=new g(t,1,!1,e,"http://www.w3.org/XML/1998/namespace",!1,!1)})),["tabIndex","crossOrigin"].forEach((function(e){h[e]=new g(e,1,!1,e.toLowerCase(),null,!1,!1)})),h.xlinkHref=new g("xlinkHref",1,!1,"xlink:href","http://www.w3.org/1999/xlink",!0,!1),["src","href","action","formAction"].forEach((function(e){h[e]=new g(e,1,!1,e.toLowerCase(),null,!0,!0)}));var w=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED,k=Symbol.for("react.element"),x=Symbol.for("react.portal"),S=Symbol.for("react.fragment"),E=Symbol.for("react.strict_mode"),_=Symbol.for("react.profiler"),C=Symbol.for("react.provider"),T=Symbol.for("react.context"),j=Symbol.for("react.forward_ref"),A=Symbol.for("react.suspense"),L=Symbol.for("react.suspense_list"),N=Symbol.for("react.memo"),R=Symbol.for("react.lazy");Symbol.for("react.scope"),Symbol.for("react.debug_trace_mode");var P=Symbol.for("react.offscreen");Symbol.for("react.legacy_hidden"),Symbol.for("react.cache"),Symbol.for("react.tracing_marker");var O=Symbol.iterator;function D(e){return null===e||"object"!=typeof e?null:"function"==typeof(e=O&&e[O]||e["@@iterator"])?e:null}var I,F=Object.assign;function M(e){if(void 0===I)try{throw Error()}catch(n){var t=n.stack.trim().match(/\n( *(at )?)/);I=t&&t[1]||""}return"\n"+I+e}var z=!1;function B(e,t){if(!e||z)return"";z=!0;var n=Error.prepareStackTrace;Error.prepareStackTrace=void 0;try{if(t)if(t=function(){throw Error()},Object.defineProperty(t.prototype,"props",{set:function(){throw Error()}}),"object"==typeof Reflect&&Reflect.construct){try{Reflect.construct(t,[])}catch(c){var r=c}Reflect.construct(e,[],t)}else{try{t.call()}catch(c){r=c}e.call(t.prototype)}else{try{throw Error()}catch(c){r=c}e()}}catch(c){if(c&&r&&"string"==typeof c.stack){for(var o=c.stack.split("\n"),a=r.stack.split("\n"),i=o.length-1,l=a.length-1;1<=i&&0<=l&&o[i]!==a[l];)l--;for(;1<=i&&0<=l;i--,l--)if(o[i]!==a[l]){if(1!==i||1!==l)do{if(i--,0>--l||o[i]!==a[l]){var s="\n"+o[i].replace(" at new "," at ");return e.displayName&&s.includes("<anonymous>")&&(s=s.replace("<anonymous>",e.displayName)),s}}while(1<=i&&0<=l);break}}}finally{z=!1,Error.prepareStackTrace=n}return(e=e?e.displayName||e.name:"")?M(e):""}function $(e){switch(e.tag){case 5:return M(e.type);case 16:return M("Lazy");case 13:return M("Suspense");case 19:return M("SuspenseList");case 0:case 2:case 15:return e=B(e.type,!1);case 11:return e=B(e.type.render,!1);case 1:return e=B(e.type,!0);default:return""}}function U(e){if(null==e)return null;if("function"==typeof e)return e.displayName||e.name||null;if("string"==typeof e)return e;switch(e){case S:return"Fragment";case x:return"Portal";case _:return"Profiler";case E:return"StrictMode";case A:return"Suspense";case L:return"SuspenseList"}if("object"==typeof e)switch(e.$$typeof){case T:return(e.displayName||"Context")+".Consumer";case C:return(e._context.displayName||"Context")+".Provider";case j:var t=e.render;return(e=e.displayName)||(e=""!==(e=t.displayName||t.name||"")?"ForwardRef("+e+")":"ForwardRef"),e;case N:return null!==(t=e.displayName||null)?t:U(e.type)||"Memo";case R:t=e._payload,e=e._init;try{return U(e(t))}catch(n){}}return null}function q(e){var t=e.type;switch(e.tag){case 24:return"Cache";case 9:return(t.displayName||"Context")+".Consumer";case 10:return(t._context.displayName||"Context")+".Provider";case 18:return"DehydratedFragment";case 11:return e=(e=t.render).displayName||e.name||"",t.displayName||(""!==e?"ForwardRef("+e+")":"ForwardRef");case 7:return"Fragment";case 5:return t;case 4:return"Portal";case 3:return"Root";case 6:return"Text";case 16:return U(t);case 8:return t===E?"StrictMode":"Mode";case 22:return"Offscreen";case 12:return"Profiler";case 21:return"Scope";case 13:return"Suspense";case 19:return"SuspenseList";case 25:return"TracingMarker";case 1:case 0:case 17:case 2:case 14:case 15:if("function"==typeof t)return t.displayName||t.name||null;if("string"==typeof t)return t}return null}function H(e){switch(typeof e){case"boolean":case"number":case"string":case"undefined":case"object":return e;default:return""}}function Z(e){var t=e.type;return(e=e.nodeName)&&"input"===e.toLowerCase()&&("checkbox"===t||"radio"===t)}function G(e){e._valueTracker||(e._valueTracker=function(e){var t=Z(e)?"checked":"value",n=Object.getOwnPropertyDescriptor(e.constructor.prototype,t),r=""+e[t];if(!e.hasOwnProperty(t)&&void 0!==n&&"function"==typeof n.get&&"function"==typeof n.set){var o=n.get,a=n.set;return Object.defineProperty(e,t,{configurable:!0,get:function(){return o.call(this)},set:function(e){r=""+e,a.call(this,e)}}),Object.defineProperty(e,t,{enumerable:n.enumerable}),{getValue:function(){return r},setValue:function(e){r=""+e},stopTracking:function(){e._valueTracker=null,delete e[t]}}}}(e))}function V(e){if(!e)return!1;var t=e._valueTracker;if(!t)return!0;var n=t.getValue(),r="";return e&&(r=Z(e)?e.checked?"true":"false":e.value),(e=r)!==n&&(t.setValue(e),!0)}function W(e){if(void 0===(e=e||("undefined"!=typeof document?document:void 0)))return null;try{return e.activeElement||e.body}catch(t){return e.body}}function Q(e,t){var n=t.checked;return F({},t,{defaultChecked:void 0,defaultValue:void 0,value:void 0,checked:null!=n?n:e._wrapperState.initialChecked})}function K(e,t){var n=null==t.defaultValue?"":t.defaultValue,r=null!=t.checked?t.checked:t.defaultChecked;n=H(null!=t.value?t.value:n),e._wrapperState={initialChecked:r,initialValue:n,controlled:"checkbox"===t.type||"radio"===t.type?null!=t.checked:null!=t.value}}function Y(e,t){null!=(t=t.checked)&&v(e,"checked",t,!1)}function X(e,t){Y(e,t);var n=H(t.value),r=t.type;if(null!=n)"number"===r?(0===n&&""===e.value||e.value!=n)&&(e.value=""+n):e.value!==""+n&&(e.value=""+n);else if("submit"===r||"reset"===r)return void e.removeAttribute("value");t.hasOwnProperty("value")?ee(e,t.type,n):t.hasOwnProperty("defaultValue")&&ee(e,t.type,H(t.defaultValue)),null==t.checked&&null!=t.defaultChecked&&(e.defaultChecked=!!t.defaultChecked)}function J(e,t,n){if(t.hasOwnProperty("value")||t.hasOwnProperty("defaultValue")){var r=t.type;if(!("submit"!==r&&"reset"!==r||void 0!==t.value&&null!==t.value))return;t=""+e._wrapperState.initialValue,n||t===e.value||(e.value=t),e.defaultValue=t}""!==(n=e.name)&&(e.name=""),e.defaultChecked=!!e._wrapperState.initialChecked,""!==n&&(e.name=n)}function ee(e,t,n){"number"===t&&W(e.ownerDocument)===e||(null==n?e.defaultValue=""+e._wrapperState.initialValue:e.defaultValue!==""+n&&(e.defaultValue=""+n))}var te=Array.isArray;function ne(e,t,n,r){if(e=e.options,t){t={};for(var o=0;o<n.length;o++)t["$"+n[o]]=!0;for(n=0;n<e.length;n++)o=t.hasOwnProperty("$"+e[n].value),e[n].selected!==o&&(e[n].selected=o),o&&r&&(e[n].defaultSelected=!0)}else{for(n=""+H(n),t=null,o=0;o<e.length;o++){if(e[o].value===n)return e[o].selected=!0,void(r&&(e[o].defaultSelected=!0));null!==t||e[o].disabled||(t=e[o])}null!==t&&(t.selected=!0)}}function re(e,t){if(null!=t.dangerouslySetInnerHTML)throw Error(a(91));return F({},t,{value:void 0,defaultValue:void 0,children:""+e._wrapperState.initialValue})}function oe(e,t){var n=t.value;if(null==n){if(n=t.children,t=t.defaultValue,null!=n){if(null!=t)throw Error(a(92));if(te(n)){if(1<n.length)throw Error(a(93));n=n[0]}t=n}null==t&&(t=""),n=t}e._wrapperState={initialValue:H(n)}}function ae(e,t){var n=H(t.value),r=H(t.defaultValue);null!=n&&((n=""+n)!==e.value&&(e.value=n),null==t.defaultValue&&e.defaultValue!==n&&(e.defaultValue=n)),null!=r&&(e.defaultValue=""+r)}function ie(e){var t=e.textContent;t===e._wrapperState.initialValue&&""!==t&&null!==t&&(e.value=t)}function le(e){switch(e){case"svg":return"http://www.w3.org/2000/svg";case"math":return"http://www.w3.org/1998/Math/MathML";default:return"http://www.w3.org/1999/xhtml"}}function se(e,t){return null==e||"http://www.w3.org/1999/xhtml"===e?le(t):"http://www.w3.org/2000/svg"===e&&"foreignObject"===t?"http://www.w3.org/1999/xhtml":e}var ce,ue,de=(ue=function(e,t){if("http://www.w3.org/2000/svg"!==e.namespaceURI||"innerHTML"in e)e.innerHTML=t;else{for((ce=ce||document.createElement("div")).innerHTML="<svg>"+t.valueOf().toString()+"</svg>",t=ce.firstChild;e.firstChild;)e.removeChild(e.firstChild);for(;t.firstChild;)e.appendChild(t.firstChild)}},"undefined"!=typeof MSApp&&MSApp.execUnsafeLocalFunction?function(e,t,n,r){MSApp.execUnsafeLocalFunction((function(){return ue(e,t)}))}:ue);function pe(e,t){if(t){var n=e.firstChild;if(n&&n===e.lastChild&&3===n.nodeType)return void(n.nodeValue=t)}e.textContent=t}var fe={animationIterationCount:!0,aspectRatio:!0,borderImageOutset:!0,borderImageSlice:!0,borderImageWidth:!0,boxFlex:!0,boxFlexGroup:!0,boxOrdinalGroup:!0,columnCount:!0,columns:!0,flex:!0,flexGrow:!0,flexPositive:!0,flexShrink:!0,flexNegative:!0,flexOrder:!0,gridArea:!0,gridRow:!0,gridRowEnd:!0,gridRowSpan:!0,gridRowStart:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnSpan:!0,gridColumnStart:!0,fontWeight:!0,lineClamp:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,tabSize:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeDasharray:!0,strokeDashoffset:!0,strokeMiterlimit:!0,strokeOpacity:!0,strokeWidth:!0},me=["Webkit","ms","Moz","O"];function ge(e,t,n){return null==t||"boolean"==typeof t||""===t?"":n||"number"!=typeof t||0===t||fe.hasOwnProperty(e)&&fe[e]?(""+t).trim():t+"px"}function he(e,t){for(var n in e=e.style,t)if(t.hasOwnProperty(n)){var r=0===n.indexOf("--"),o=ge(n,t[n],r);"float"===n&&(n="cssFloat"),r?e.setProperty(n,o):e[n]=o}}Object.keys(fe).forEach((function(e){me.forEach((function(t){t=t+e.charAt(0).toUpperCase()+e.substring(1),fe[t]=fe[e]}))}));var be=F({menuitem:!0},{area:!0,base:!0,br:!0,col:!0,embed:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0});function ye(e,t){if(t){if(be[e]&&(null!=t.children||null!=t.dangerouslySetInnerHTML))throw Error(a(137,e));if(null!=t.dangerouslySetInnerHTML){if(null!=t.children)throw Error(a(60));if("object"!=typeof t.dangerouslySetInnerHTML||!("__html"in t.dangerouslySetInnerHTML))throw Error(a(61))}if(null!=t.style&&"object"!=typeof t.style)throw Error(a(62))}}function ve(e,t){if(-1===e.indexOf("-"))return"string"==typeof t.is;switch(e){case"annotation-xml":case"color-profile":case"font-face":case"font-face-src":case"font-face-uri":case"font-face-format":case"font-face-name":case"missing-glyph":return!1;default:return!0}}var we=null;function ke(e){return(e=e.target||e.srcElement||window).correspondingUseElement&&(e=e.correspondingUseElement),3===e.nodeType?e.parentNode:e}var xe=null,Se=null,Ee=null;function _e(e){if(e=wo(e)){if("function"!=typeof xe)throw Error(a(280));var t=e.stateNode;t&&(t=xo(t),xe(e.stateNode,e.type,t))}}function Ce(e){Se?Ee?Ee.push(e):Ee=[e]:Se=e}function Te(){if(Se){var e=Se,t=Ee;if(Ee=Se=null,_e(e),t)for(e=0;e<t.length;e++)_e(t[e])}}function je(e,t){return e(t)}function Ae(){}var Le=!1;function Ne(e,t,n){if(Le)return e(t,n);Le=!0;try{return je(e,t,n)}finally{Le=!1,(null!==Se||null!==Ee)&&(Ae(),Te())}}function Re(e,t){var n=e.stateNode;if(null===n)return null;var r=xo(n);if(null===r)return null;n=r[t];e:switch(t){case"onClick":case"onClickCapture":case"onDoubleClick":case"onDoubleClickCapture":case"onMouseDown":case"onMouseDownCapture":case"onMouseMove":case"onMouseMoveCapture":case"onMouseUp":case"onMouseUpCapture":case"onMouseEnter":(r=!r.disabled)||(r=!("button"===(e=e.type)||"input"===e||"select"===e||"textarea"===e)),e=!r;break e;default:e=!1}if(e)return null;if(n&&"function"!=typeof n)throw Error(a(231,t,typeof n));return n}var Pe=!1;if(u)try{var Oe={};Object.defineProperty(Oe,"passive",{get:function(){Pe=!0}}),window.addEventListener("test",Oe,Oe),window.removeEventListener("test",Oe,Oe)}catch(ue){Pe=!1}function De(e,t,n,r,o,a,i,l,s){var c=Array.prototype.slice.call(arguments,3);try{t.apply(n,c)}catch(u){this.onError(u)}}var Ie=!1,Fe=null,Me=!1,ze=null,Be={onError:function(e){Ie=!0,Fe=e}};function $e(e,t,n,r,o,a,i,l,s){Ie=!1,Fe=null,De.apply(Be,arguments)}function Ue(e){var t=e,n=e;if(e.alternate)for(;t.return;)t=t.return;else{e=t;do{0!=(4098&(t=e).flags)&&(n=t.return),e=t.return}while(e)}return 3===t.tag?n:null}function qe(e){if(13===e.tag){var t=e.memoizedState;if(null===t&&(null!==(e=e.alternate)&&(t=e.memoizedState)),null!==t)return t.dehydrated}return null}function He(e){if(Ue(e)!==e)throw Error(a(188))}function Ze(e){return null!==(e=function(e){var t=e.alternate;if(!t){if(null===(t=Ue(e)))throw Error(a(188));return t!==e?null:e}for(var n=e,r=t;;){var o=n.return;if(null===o)break;var i=o.alternate;if(null===i){if(null!==(r=o.return)){n=r;continue}break}if(o.child===i.child){for(i=o.child;i;){if(i===n)return He(o),e;if(i===r)return He(o),t;i=i.sibling}throw Error(a(188))}if(n.return!==r.return)n=o,r=i;else{for(var l=!1,s=o.child;s;){if(s===n){l=!0,n=o,r=i;break}if(s===r){l=!0,r=o,n=i;break}s=s.sibling}if(!l){for(s=i.child;s;){if(s===n){l=!0,n=i,r=o;break}if(s===r){l=!0,r=i,n=o;break}s=s.sibling}if(!l)throw Error(a(189))}}if(n.alternate!==r)throw Error(a(190))}if(3!==n.tag)throw Error(a(188));return n.stateNode.current===n?e:t}(e))?Ge(e):null}function Ge(e){if(5===e.tag||6===e.tag)return e;for(e=e.child;null!==e;){var t=Ge(e);if(null!==t)return t;e=e.sibling}return null}var Ve=o.unstable_scheduleCallback,We=o.unstable_cancelCallback,Qe=o.unstable_shouldYield,Ke=o.unstable_requestPaint,Ye=o.unstable_now,Xe=o.unstable_getCurrentPriorityLevel,Je=o.unstable_ImmediatePriority,et=o.unstable_UserBlockingPriority,tt=o.unstable_NormalPriority,nt=o.unstable_LowPriority,rt=o.unstable_IdlePriority,ot=null,at=null;var it=Math.clz32?Math.clz32:function(e){return e>>>=0,0===e?32:31-(lt(e)/st|0)|0},lt=Math.log,st=Math.LN2;var ct=64,ut=4194304;function dt(e){switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return 4194240&e;case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:return 130023424&e;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 1073741824;default:return e}}function pt(e,t){var n=e.pendingLanes;if(0===n)return 0;var r=0,o=e.suspendedLanes,a=e.pingedLanes,i=268435455&n;if(0!==i){var l=i&~o;0!==l?r=dt(l):0!==(a&=i)&&(r=dt(a))}else 0!==(i=n&~o)?r=dt(i):0!==a&&(r=dt(a));if(0===r)return 0;if(0!==t&&t!==r&&0==(t&o)&&((o=r&-r)>=(a=t&-t)||16===o&&0!=(4194240&a)))return t;if(0!=(4&r)&&(r|=16&n),0!==(t=e.entangledLanes))for(e=e.entanglements,t&=r;0<t;)o=1<<(n=31-it(t)),r|=e[n],t&=~o;return r}function ft(e,t){switch(e){case 1:case 2:case 4:return t+250;case 8:case 16:case 32:case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;default:return-1}}function mt(e){return 0!==(e=-1073741825&e.pendingLanes)?e:1073741824&e?1073741824:0}function gt(){var e=ct;return 0==(4194240&(ct<<=1))&&(ct=64),e}function ht(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function bt(e,t,n){e.pendingLanes|=t,536870912!==t&&(e.suspendedLanes=0,e.pingedLanes=0),(e=e.eventTimes)[t=31-it(t)]=n}function yt(e,t){var n=e.entangledLanes|=t;for(e=e.entanglements;n;){var r=31-it(n),o=1<<r;o&t|e[r]&t&&(e[r]|=t),n&=~o}}var vt=0;function wt(e){return 1<(e&=-e)?4<e?0!=(268435455&e)?16:536870912:4:1}var kt,xt,St,Et,_t,Ct=!1,Tt=[],jt=null,At=null,Lt=null,Nt=new Map,Rt=new Map,Pt=[],Ot="mousedown mouseup touchcancel touchend touchstart auxclick dblclick pointercancel pointerdown pointerup dragend dragstart drop compositionend compositionstart keydown keypress keyup input textInput copy cut paste click change contextmenu reset submit".split(" ");function Dt(e,t){switch(e){case"focusin":case"focusout":jt=null;break;case"dragenter":case"dragleave":At=null;break;case"mouseover":case"mouseout":Lt=null;break;case"pointerover":case"pointerout":Nt.delete(t.pointerId);break;case"gotpointercapture":case"lostpointercapture":Rt.delete(t.pointerId)}}function It(e,t,n,r,o,a){return null===e||e.nativeEvent!==a?(e={blockedOn:t,domEventName:n,eventSystemFlags:r,nativeEvent:a,targetContainers:[o]},null!==t&&(null!==(t=wo(t))&&xt(t)),e):(e.eventSystemFlags|=r,t=e.targetContainers,null!==o&&-1===t.indexOf(o)&&t.push(o),e)}function Ft(e){var t=vo(e.target);if(null!==t){var n=Ue(t);if(null!==n)if(13===(t=n.tag)){if(null!==(t=qe(n)))return e.blockedOn=t,void _t(e.priority,(function(){St(n)}))}else if(3===t&&n.stateNode.current.memoizedState.isDehydrated)return void(e.blockedOn=3===n.tag?n.stateNode.containerInfo:null)}e.blockedOn=null}function Mt(e){if(null!==e.blockedOn)return!1;for(var t=e.targetContainers;0<t.length;){var n=Qt(e.domEventName,e.eventSystemFlags,t[0],e.nativeEvent);if(null!==n)return null!==(t=wo(n))&&xt(t),e.blockedOn=n,!1;var r=new(n=e.nativeEvent).constructor(n.type,n);we=r,n.target.dispatchEvent(r),we=null,t.shift()}return!0}function zt(e,t,n){Mt(e)&&n.delete(t)}function Bt(){Ct=!1,null!==jt&&Mt(jt)&&(jt=null),null!==At&&Mt(At)&&(At=null),null!==Lt&&Mt(Lt)&&(Lt=null),Nt.forEach(zt),Rt.forEach(zt)}function $t(e,t){e.blockedOn===t&&(e.blockedOn=null,Ct||(Ct=!0,o.unstable_scheduleCallback(o.unstable_NormalPriority,Bt)))}function Ut(e){function t(t){return $t(t,e)}if(0<Tt.length){$t(Tt[0],e);for(var n=1;n<Tt.length;n++){var r=Tt[n];r.blockedOn===e&&(r.blockedOn=null)}}for(null!==jt&&$t(jt,e),null!==At&&$t(At,e),null!==Lt&&$t(Lt,e),Nt.forEach(t),Rt.forEach(t),n=0;n<Pt.length;n++)(r=Pt[n]).blockedOn===e&&(r.blockedOn=null);for(;0<Pt.length&&null===(n=Pt[0]).blockedOn;)Ft(n),null===n.blockedOn&&Pt.shift()}var qt=w.ReactCurrentBatchConfig,Ht=!0;function Zt(e,t,n,r){var o=vt,a=qt.transition;qt.transition=null;try{vt=1,Vt(e,t,n,r)}finally{vt=o,qt.transition=a}}function Gt(e,t,n,r){var o=vt,a=qt.transition;qt.transition=null;try{vt=4,Vt(e,t,n,r)}finally{vt=o,qt.transition=a}}function Vt(e,t,n,r){if(Ht){var o=Qt(e,t,n,r);if(null===o)Hr(e,t,r,Wt,n),Dt(e,r);else if(function(e,t,n,r,o){switch(t){case"focusin":return jt=It(jt,e,t,n,r,o),!0;case"dragenter":return At=It(At,e,t,n,r,o),!0;case"mouseover":return Lt=It(Lt,e,t,n,r,o),!0;case"pointerover":var a=o.pointerId;return Nt.set(a,It(Nt.get(a)||null,e,t,n,r,o)),!0;case"gotpointercapture":return a=o.pointerId,Rt.set(a,It(Rt.get(a)||null,e,t,n,r,o)),!0}return!1}(o,e,t,n,r))r.stopPropagation();else if(Dt(e,r),4&t&&-1<Ot.indexOf(e)){for(;null!==o;){var a=wo(o);if(null!==a&&kt(a),null===(a=Qt(e,t,n,r))&&Hr(e,t,r,Wt,n),a===o)break;o=a}null!==o&&r.stopPropagation()}else Hr(e,t,r,null,n)}}var Wt=null;function Qt(e,t,n,r){if(Wt=null,null!==(e=vo(e=ke(r))))if(null===(t=Ue(e)))e=null;else if(13===(n=t.tag)){if(null!==(e=qe(t)))return e;e=null}else if(3===n){if(t.stateNode.current.memoizedState.isDehydrated)return 3===t.tag?t.stateNode.containerInfo:null;e=null}else t!==e&&(e=null);return Wt=e,null}function Kt(e){switch(e){case"cancel":case"click":case"close":case"contextmenu":case"copy":case"cut":case"auxclick":case"dblclick":case"dragend":case"dragstart":case"drop":case"focusin":case"focusout":case"input":case"invalid":case"keydown":case"keypress":case"keyup":case"mousedown":case"mouseup":case"paste":case"pause":case"play":case"pointercancel":case"pointerdown":case"pointerup":case"ratechange":case"reset":case"resize":case"seeked":case"submit":case"touchcancel":case"touchend":case"touchstart":case"volumechange":case"change":case"selectionchange":case"textInput":case"compositionstart":case"compositionend":case"compositionupdate":case"beforeblur":case"afterblur":case"beforeinput":case"blur":case"fullscreenchange":case"focus":case"hashchange":case"popstate":case"select":case"selectstart":return 1;case"drag":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"mousemove":case"mouseout":case"mouseover":case"pointermove":case"pointerout":case"pointerover":case"scroll":case"toggle":case"touchmove":case"wheel":case"mouseenter":case"mouseleave":case"pointerenter":case"pointerleave":return 4;case"message":switch(Xe()){case Je:return 1;case et:return 4;case tt:case nt:return 16;case rt:return 536870912;default:return 16}default:return 16}}var Yt=null,Xt=null,Jt=null;function en(){if(Jt)return Jt;var e,t,n=Xt,r=n.length,o="value"in Yt?Yt.value:Yt.textContent,a=o.length;for(e=0;e<r&&n[e]===o[e];e++);var i=r-e;for(t=1;t<=i&&n[r-t]===o[a-t];t++);return Jt=o.slice(e,1<t?1-t:void 0)}function tn(e){var t=e.keyCode;return"charCode"in e?0===(e=e.charCode)&&13===t&&(e=13):e=t,10===e&&(e=13),32<=e||13===e?e:0}function nn(){return!0}function rn(){return!1}function on(e){function t(t,n,r,o,a){for(var i in this._reactName=t,this._targetInst=r,this.type=n,this.nativeEvent=o,this.target=a,this.currentTarget=null,e)e.hasOwnProperty(i)&&(t=e[i],this[i]=t?t(o):o[i]);return this.isDefaultPrevented=(null!=o.defaultPrevented?o.defaultPrevented:!1===o.returnValue)?nn:rn,this.isPropagationStopped=rn,this}return F(t.prototype,{preventDefault:function(){this.defaultPrevented=!0;var e=this.nativeEvent;e&&(e.preventDefault?e.preventDefault():"unknown"!=typeof e.returnValue&&(e.returnValue=!1),this.isDefaultPrevented=nn)},stopPropagation:function(){var e=this.nativeEvent;e&&(e.stopPropagation?e.stopPropagation():"unknown"!=typeof e.cancelBubble&&(e.cancelBubble=!0),this.isPropagationStopped=nn)},persist:function(){},isPersistent:nn}),t}var an,ln,sn,cn={eventPhase:0,bubbles:0,cancelable:0,timeStamp:function(e){return e.timeStamp||Date.now()},defaultPrevented:0,isTrusted:0},un=on(cn),dn=F({},cn,{view:0,detail:0}),pn=on(dn),fn=F({},dn,{screenX:0,screenY:0,clientX:0,clientY:0,pageX:0,pageY:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,getModifierState:_n,button:0,buttons:0,relatedTarget:function(e){return void 0===e.relatedTarget?e.fromElement===e.srcElement?e.toElement:e.fromElement:e.relatedTarget},movementX:function(e){return"movementX"in e?e.movementX:(e!==sn&&(sn&&"mousemove"===e.type?(an=e.screenX-sn.screenX,ln=e.screenY-sn.screenY):ln=an=0,sn=e),an)},movementY:function(e){return"movementY"in e?e.movementY:ln}}),mn=on(fn),gn=on(F({},fn,{dataTransfer:0})),hn=on(F({},dn,{relatedTarget:0})),bn=on(F({},cn,{animationName:0,elapsedTime:0,pseudoElement:0})),yn=F({},cn,{clipboardData:function(e){return"clipboardData"in e?e.clipboardData:window.clipboardData}}),vn=on(yn),wn=on(F({},cn,{data:0})),kn={Esc:"Escape",Spacebar:" ",Left:"ArrowLeft",Up:"ArrowUp",Right:"ArrowRight",Down:"ArrowDown",Del:"Delete",Win:"OS",Menu:"ContextMenu",Apps:"ContextMenu",Scroll:"ScrollLock",MozPrintableKey:"Unidentified"},xn={8:"Backspace",9:"Tab",12:"Clear",13:"Enter",16:"Shift",17:"Control",18:"Alt",19:"Pause",20:"CapsLock",27:"Escape",32:" ",33:"PageUp",34:"PageDown",35:"End",36:"Home",37:"ArrowLeft",38:"ArrowUp",39:"ArrowRight",40:"ArrowDown",45:"Insert",46:"Delete",112:"F1",113:"F2",114:"F3",115:"F4",116:"F5",117:"F6",118:"F7",119:"F8",120:"F9",121:"F10",122:"F11",123:"F12",144:"NumLock",145:"ScrollLock",224:"Meta"},Sn={Alt:"altKey",Control:"ctrlKey",Meta:"metaKey",Shift:"shiftKey"};function En(e){var t=this.nativeEvent;return t.getModifierState?t.getModifierState(e):!!(e=Sn[e])&&!!t[e]}function _n(){return En}var Cn=F({},dn,{key:function(e){if(e.key){var t=kn[e.key]||e.key;if("Unidentified"!==t)return t}return"keypress"===e.type?13===(e=tn(e))?"Enter":String.fromCharCode(e):"keydown"===e.type||"keyup"===e.type?xn[e.keyCode]||"Unidentified":""},code:0,location:0,ctrlKey:0,shiftKey:0,altKey:0,metaKey:0,repeat:0,locale:0,getModifierState:_n,charCode:function(e){return"keypress"===e.type?tn(e):0},keyCode:function(e){return"keydown"===e.type||"keyup"===e.type?e.keyCode:0},which:function(e){return"keypress"===e.type?tn(e):"keydown"===e.type||"keyup"===e.type?e.keyCode:0}}),Tn=on(Cn),jn=on(F({},fn,{pointerId:0,width:0,height:0,pressure:0,tangentialPressure:0,tiltX:0,tiltY:0,twist:0,pointerType:0,isPrimary:0})),An=on(F({},dn,{touches:0,targetTouches:0,changedTouches:0,altKey:0,metaKey:0,ctrlKey:0,shiftKey:0,getModifierState:_n})),Ln=on(F({},cn,{propertyName:0,elapsedTime:0,pseudoElement:0})),Nn=F({},fn,{deltaX:function(e){return"deltaX"in e?e.deltaX:"wheelDeltaX"in e?-e.wheelDeltaX:0},deltaY:function(e){return"deltaY"in e?e.deltaY:"wheelDeltaY"in e?-e.wheelDeltaY:"wheelDelta"in e?-e.wheelDelta:0},deltaZ:0,deltaMode:0}),Rn=on(Nn),Pn=[9,13,27,32],On=u&&"CompositionEvent"in window,Dn=null;u&&"documentMode"in document&&(Dn=document.documentMode);var In=u&&"TextEvent"in window&&!Dn,Fn=u&&(!On||Dn&&8<Dn&&11>=Dn),Mn=String.fromCharCode(32),zn=!1;function Bn(e,t){switch(e){case"keyup":return-1!==Pn.indexOf(t.keyCode);case"keydown":return 229!==t.keyCode;case"keypress":case"mousedown":case"focusout":return!0;default:return!1}}function $n(e){return"object"==typeof(e=e.detail)&&"data"in e?e.data:null}var Un=!1;var qn={color:!0,date:!0,datetime:!0,"datetime-local":!0,email:!0,month:!0,number:!0,password:!0,range:!0,search:!0,tel:!0,text:!0,time:!0,url:!0,week:!0};function Hn(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return"input"===t?!!qn[e.type]:"textarea"===t}function Zn(e,t,n,r){Ce(r),0<(t=Gr(t,"onChange")).length&&(n=new un("onChange","change",null,n,r),e.push({event:n,listeners:t}))}var Gn=null,Vn=null;function Wn(e){Mr(e,0)}function Qn(e){if(V(ko(e)))return e}function Kn(e,t){if("change"===e)return t}var Yn=!1;if(u){var Xn;if(u){var Jn="oninput"in document;if(!Jn){var er=document.createElement("div");er.setAttribute("oninput","return;"),Jn="function"==typeof er.oninput}Xn=Jn}else Xn=!1;Yn=Xn&&(!document.documentMode||9<document.documentMode)}function tr(){Gn&&(Gn.detachEvent("onpropertychange",nr),Vn=Gn=null)}function nr(e){if("value"===e.propertyName&&Qn(Vn)){var t=[];Zn(t,Vn,e,ke(e)),Ne(Wn,t)}}function rr(e,t,n){"focusin"===e?(tr(),Vn=n,(Gn=t).attachEvent("onpropertychange",nr)):"focusout"===e&&tr()}function or(e){if("selectionchange"===e||"keyup"===e||"keydown"===e)return Qn(Vn)}function ar(e,t){if("click"===e)return Qn(t)}function ir(e,t){if("input"===e||"change"===e)return Qn(t)}var lr="function"==typeof Object.is?Object.is:function(e,t){return e===t&&(0!==e||1/e==1/t)||e!=e&&t!=t};function sr(e,t){if(lr(e,t))return!0;if("object"!=typeof e||null===e||"object"!=typeof t||null===t)return!1;var n=Object.keys(e),r=Object.keys(t);if(n.length!==r.length)return!1;for(r=0;r<n.length;r++){var o=n[r];if(!d.call(t,o)||!lr(e[o],t[o]))return!1}return!0}function cr(e){for(;e&&e.firstChild;)e=e.firstChild;return e}function ur(e,t){var n,r=cr(e);for(e=0;r;){if(3===r.nodeType){if(n=e+r.textContent.length,e<=t&&n>=t)return{node:r,offset:t-e};e=n}e:{for(;r;){if(r.nextSibling){r=r.nextSibling;break e}r=r.parentNode}r=void 0}r=cr(r)}}function dr(e,t){return!(!e||!t)&&(e===t||(!e||3!==e.nodeType)&&(t&&3===t.nodeType?dr(e,t.parentNode):"contains"in e?e.contains(t):!!e.compareDocumentPosition&&!!(16&e.compareDocumentPosition(t))))}function pr(){for(var e=window,t=W();t instanceof e.HTMLIFrameElement;){try{var n="string"==typeof t.contentWindow.location.href}catch(r){n=!1}if(!n)break;t=W((e=t.contentWindow).document)}return t}function fr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&("input"===t&&("text"===e.type||"search"===e.type||"tel"===e.type||"url"===e.type||"password"===e.type)||"textarea"===t||"true"===e.contentEditable)}function mr(e){var t=pr(),n=e.focusedElem,r=e.selectionRange;if(t!==n&&n&&n.ownerDocument&&dr(n.ownerDocument.documentElement,n)){if(null!==r&&fr(n))if(t=r.start,void 0===(e=r.end)&&(e=t),"selectionStart"in n)n.selectionStart=t,n.selectionEnd=Math.min(e,n.value.length);else if((e=(t=n.ownerDocument||document)&&t.defaultView||window).getSelection){e=e.getSelection();var o=n.textContent.length,a=Math.min(r.start,o);r=void 0===r.end?a:Math.min(r.end,o),!e.extend&&a>r&&(o=r,r=a,a=o),o=ur(n,a);var i=ur(n,r);o&&i&&(1!==e.rangeCount||e.anchorNode!==o.node||e.anchorOffset!==o.offset||e.focusNode!==i.node||e.focusOffset!==i.offset)&&((t=t.createRange()).setStart(o.node,o.offset),e.removeAllRanges(),a>r?(e.addRange(t),e.extend(i.node,i.offset)):(t.setEnd(i.node,i.offset),e.addRange(t)))}for(t=[],e=n;e=e.parentNode;)1===e.nodeType&&t.push({element:e,left:e.scrollLeft,top:e.scrollTop});for("function"==typeof n.focus&&n.focus(),n=0;n<t.length;n++)(e=t[n]).element.scrollLeft=e.left,e.element.scrollTop=e.top}}var gr=u&&"documentMode"in document&&11>=document.documentMode,hr=null,br=null,yr=null,vr=!1;function wr(e,t,n){var r=n.window===n?n.document:9===n.nodeType?n:n.ownerDocument;vr||null==hr||hr!==W(r)||("selectionStart"in(r=hr)&&fr(r)?r={start:r.selectionStart,end:r.selectionEnd}:r={anchorNode:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection()).anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset},yr&&sr(yr,r)||(yr=r,0<(r=Gr(br,"onSelect")).length&&(t=new un("onSelect","select",null,t,n),e.push({event:t,listeners:r}),t.target=hr)))}function kr(e,t){var n={};return n[e.toLowerCase()]=t.toLowerCase(),n["Webkit"+e]="webkit"+t,n["Moz"+e]="moz"+t,n}var xr={animationend:kr("Animation","AnimationEnd"),animationiteration:kr("Animation","AnimationIteration"),animationstart:kr("Animation","AnimationStart"),transitionend:kr("Transition","TransitionEnd")},Sr={},Er={};function _r(e){if(Sr[e])return Sr[e];if(!xr[e])return e;var t,n=xr[e];for(t in n)if(n.hasOwnProperty(t)&&t in Er)return Sr[e]=n[t];return e}u&&(Er=document.createElement("div").style,"AnimationEvent"in window||(delete xr.animationend.animation,delete xr.animationiteration.animation,delete xr.animationstart.animation),"TransitionEvent"in window||delete xr.transitionend.transition);var Cr=_r("animationend"),Tr=_r("animationiteration"),jr=_r("animationstart"),Ar=_r("transitionend"),Lr=new Map,Nr="abort auxClick cancel canPlay canPlayThrough click close contextMenu copy cut drag dragEnd dragEnter dragExit dragLeave dragOver dragStart drop durationChange emptied encrypted ended error gotPointerCapture input invalid keyDown keyPress keyUp load loadedData loadedMetadata loadStart lostPointerCapture mouseDown mouseMove mouseOut mouseOver mouseUp paste pause play playing pointerCancel pointerDown pointerMove pointerOut pointerOver pointerUp progress rateChange reset resize seeked seeking stalled submit suspend timeUpdate touchCancel touchEnd touchStart volumeChange scroll toggle touchMove waiting wheel".split(" ");function Rr(e,t){Lr.set(e,t),s(t,[e])}for(var Pr=0;Pr<Nr.length;Pr++){var Or=Nr[Pr];Rr(Or.toLowerCase(),"on"+(Or[0].toUpperCase()+Or.slice(1)))}Rr(Cr,"onAnimationEnd"),Rr(Tr,"onAnimationIteration"),Rr(jr,"onAnimationStart"),Rr("dblclick","onDoubleClick"),Rr("focusin","onFocus"),Rr("focusout","onBlur"),Rr(Ar,"onTransitionEnd"),c("onMouseEnter",["mouseout","mouseover"]),c("onMouseLeave",["mouseout","mouseover"]),c("onPointerEnter",["pointerout","pointerover"]),c("onPointerLeave",["pointerout","pointerover"]),s("onChange","change click focusin focusout input keydown keyup selectionchange".split(" ")),s("onSelect","focusout contextmenu dragend focusin keydown keyup mousedown mouseup selectionchange".split(" ")),s("onBeforeInput",["compositionend","keypress","textInput","paste"]),s("onCompositionEnd","compositionend focusout keydown keypress keyup mousedown".split(" ")),s("onCompositionStart","compositionstart focusout keydown keypress keyup mousedown".split(" ")),s("onCompositionUpdate","compositionupdate focusout keydown keypress keyup mousedown".split(" "));var Dr="abort canplay canplaythrough durationchange emptied encrypted ended error loadeddata loadedmetadata loadstart pause play playing progress ratechange resize seeked seeking stalled suspend timeupdate volumechange waiting".split(" "),Ir=new Set("cancel close invalid load scroll toggle".split(" ").concat(Dr));function Fr(e,t,n){var r=e.type||"unknown-event";e.currentTarget=n,function(e,t,n,r,o,i,l,s,c){if($e.apply(this,arguments),Ie){if(!Ie)throw Error(a(198));var u=Fe;Ie=!1,Fe=null,Me||(Me=!0,ze=u)}}(r,t,void 0,e),e.currentTarget=null}function Mr(e,t){t=0!=(4&t);for(var n=0;n<e.length;n++){var r=e[n],o=r.event;r=r.listeners;e:{var a=void 0;if(t)for(var i=r.length-1;0<=i;i--){var l=r[i],s=l.instance,c=l.currentTarget;if(l=l.listener,s!==a&&o.isPropagationStopped())break e;Fr(o,l,c),a=s}else for(i=0;i<r.length;i++){if(s=(l=r[i]).instance,c=l.currentTarget,l=l.listener,s!==a&&o.isPropagationStopped())break e;Fr(o,l,c),a=s}}}if(Me)throw e=ze,Me=!1,ze=null,e}function zr(e,t){var n=t[ho];void 0===n&&(n=t[ho]=new Set);var r=e+"__bubble";n.has(r)||(qr(t,e,2,!1),n.add(r))}function Br(e,t,n){var r=0;t&&(r|=4),qr(n,e,r,t)}var $r="_reactListening"+Math.random().toString(36).slice(2);function Ur(e){if(!e[$r]){e[$r]=!0,i.forEach((function(t){"selectionchange"!==t&&(Ir.has(t)||Br(t,!1,e),Br(t,!0,e))}));var t=9===e.nodeType?e:e.ownerDocument;null===t||t[$r]||(t[$r]=!0,Br("selectionchange",!1,t))}}function qr(e,t,n,r){switch(Kt(t)){case 1:var o=Zt;break;case 4:o=Gt;break;default:o=Vt}n=o.bind(null,t,n,e),o=void 0,!Pe||"touchstart"!==t&&"touchmove"!==t&&"wheel"!==t||(o=!0),r?void 0!==o?e.addEventListener(t,n,{capture:!0,passive:o}):e.addEventListener(t,n,!0):void 0!==o?e.addEventListener(t,n,{passive:o}):e.addEventListener(t,n,!1)}function Hr(e,t,n,r,o){var a=r;if(0==(1&t)&&0==(2&t)&&null!==r)e:for(;;){if(null===r)return;var i=r.tag;if(3===i||4===i){var l=r.stateNode.containerInfo;if(l===o||8===l.nodeType&&l.parentNode===o)break;if(4===i)for(i=r.return;null!==i;){var s=i.tag;if((3===s||4===s)&&((s=i.stateNode.containerInfo)===o||8===s.nodeType&&s.parentNode===o))return;i=i.return}for(;null!==l;){if(null===(i=vo(l)))return;if(5===(s=i.tag)||6===s){r=a=i;continue e}l=l.parentNode}}r=r.return}Ne((function(){var r=a,o=ke(n),i=[];e:{var l=Lr.get(e);if(void 0!==l){var s=un,c=e;switch(e){case"keypress":if(0===tn(n))break e;case"keydown":case"keyup":s=Tn;break;case"focusin":c="focus",s=hn;break;case"focusout":c="blur",s=hn;break;case"beforeblur":case"afterblur":s=hn;break;case"click":if(2===n.button)break e;case"auxclick":case"dblclick":case"mousedown":case"mousemove":case"mouseup":case"mouseout":case"mouseover":case"contextmenu":s=mn;break;case"drag":case"dragend":case"dragenter":case"dragexit":case"dragleave":case"dragover":case"dragstart":case"drop":s=gn;break;case"touchcancel":case"touchend":case"touchmove":case"touchstart":s=An;break;case Cr:case Tr:case jr:s=bn;break;case Ar:s=Ln;break;case"scroll":s=pn;break;case"wheel":s=Rn;break;case"copy":case"cut":case"paste":s=vn;break;case"gotpointercapture":case"lostpointercapture":case"pointercancel":case"pointerdown":case"pointermove":case"pointerout":case"pointerover":case"pointerup":s=jn}var u=0!=(4&t),d=!u&&"scroll"===e,p=u?null!==l?l+"Capture":null:l;u=[];for(var f,m=r;null!==m;){var g=(f=m).stateNode;if(5===f.tag&&null!==g&&(f=g,null!==p&&(null!=(g=Re(m,p))&&u.push(Zr(m,g,f)))),d)break;m=m.return}0<u.length&&(l=new s(l,c,null,n,o),i.push({event:l,listeners:u}))}}if(0==(7&t)){if(s="mouseout"===e||"pointerout"===e,(!(l="mouseover"===e||"pointerover"===e)||n===we||!(c=n.relatedTarget||n.fromElement)||!vo(c)&&!c[go])&&(s||l)&&(l=o.window===o?o:(l=o.ownerDocument)?l.defaultView||l.parentWindow:window,s?(s=r,null!==(c=(c=n.relatedTarget||n.toElement)?vo(c):null)&&(c!==(d=Ue(c))||5!==c.tag&&6!==c.tag)&&(c=null)):(s=null,c=r),s!==c)){if(u=mn,g="onMouseLeave",p="onMouseEnter",m="mouse","pointerout"!==e&&"pointerover"!==e||(u=jn,g="onPointerLeave",p="onPointerEnter",m="pointer"),d=null==s?l:ko(s),f=null==c?l:ko(c),(l=new u(g,m+"leave",s,n,o)).target=d,l.relatedTarget=f,g=null,vo(o)===r&&((u=new u(p,m+"enter",c,n,o)).target=f,u.relatedTarget=d,g=u),d=g,s&&c)e:{for(p=c,m=0,f=u=s;f;f=Vr(f))m++;for(f=0,g=p;g;g=Vr(g))f++;for(;0<m-f;)u=Vr(u),m--;for(;0<f-m;)p=Vr(p),f--;for(;m--;){if(u===p||null!==p&&u===p.alternate)break e;u=Vr(u),p=Vr(p)}u=null}else u=null;null!==s&&Wr(i,l,s,u,!1),null!==c&&null!==d&&Wr(i,d,c,u,!0)}if("select"===(s=(l=r?ko(r):window).nodeName&&l.nodeName.toLowerCase())||"input"===s&&"file"===l.type)var h=Kn;else if(Hn(l))if(Yn)h=ir;else{h=or;var b=rr}else(s=l.nodeName)&&"input"===s.toLowerCase()&&("checkbox"===l.type||"radio"===l.type)&&(h=ar);switch(h&&(h=h(e,r))?Zn(i,h,n,o):(b&&b(e,l,r),"focusout"===e&&(b=l._wrapperState)&&b.controlled&&"number"===l.type&&ee(l,"number",l.value)),b=r?ko(r):window,e){case"focusin":(Hn(b)||"true"===b.contentEditable)&&(hr=b,br=r,yr=null);break;case"focusout":yr=br=hr=null;break;case"mousedown":vr=!0;break;case"contextmenu":case"mouseup":case"dragend":vr=!1,wr(i,n,o);break;case"selectionchange":if(gr)break;case"keydown":case"keyup":wr(i,n,o)}var y;if(On)e:{switch(e){case"compositionstart":var v="onCompositionStart";break e;case"compositionend":v="onCompositionEnd";break e;case"compositionupdate":v="onCompositionUpdate";break e}v=void 0}else Un?Bn(e,n)&&(v="onCompositionEnd"):"keydown"===e&&229===n.keyCode&&(v="onCompositionStart");v&&(Fn&&"ko"!==n.locale&&(Un||"onCompositionStart"!==v?"onCompositionEnd"===v&&Un&&(y=en()):(Xt="value"in(Yt=o)?Yt.value:Yt.textContent,Un=!0)),0<(b=Gr(r,v)).length&&(v=new wn(v,e,null,n,o),i.push({event:v,listeners:b}),y?v.data=y:null!==(y=$n(n))&&(v.data=y))),(y=In?function(e,t){switch(e){case"compositionend":return $n(t);case"keypress":return 32!==t.which?null:(zn=!0,Mn);case"textInput":return(e=t.data)===Mn&&zn?null:e;default:return null}}(e,n):function(e,t){if(Un)return"compositionend"===e||!On&&Bn(e,t)?(e=en(),Jt=Xt=Yt=null,Un=!1,e):null;switch(e){case"paste":default:return null;case"keypress":if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1<t.char.length)return t.char;if(t.which)return String.fromCharCode(t.which)}return null;case"compositionend":return Fn&&"ko"!==t.locale?null:t.data}}(e,n))&&(0<(r=Gr(r,"onBeforeInput")).length&&(o=new wn("onBeforeInput","beforeinput",null,n,o),i.push({event:o,listeners:r}),o.data=y))}Mr(i,t)}))}function Zr(e,t,n){return{instance:e,listener:t,currentTarget:n}}function Gr(e,t){for(var n=t+"Capture",r=[];null!==e;){var o=e,a=o.stateNode;5===o.tag&&null!==a&&(o=a,null!=(a=Re(e,n))&&r.unshift(Zr(e,a,o)),null!=(a=Re(e,t))&&r.push(Zr(e,a,o))),e=e.return}return r}function Vr(e){if(null===e)return null;do{e=e.return}while(e&&5!==e.tag);return e||null}function Wr(e,t,n,r,o){for(var a=t._reactName,i=[];null!==n&&n!==r;){var l=n,s=l.alternate,c=l.stateNode;if(null!==s&&s===r)break;5===l.tag&&null!==c&&(l=c,o?null!=(s=Re(n,a))&&i.unshift(Zr(n,s,l)):o||null!=(s=Re(n,a))&&i.push(Zr(n,s,l))),n=n.return}0!==i.length&&e.push({event:t,listeners:i})}var Qr=/\r\n?/g,Kr=/\u0000|\uFFFD/g;function Yr(e){return("string"==typeof e?e:""+e).replace(Qr,"\n").replace(Kr,"")}function Xr(e,t,n){if(t=Yr(t),Yr(e)!==t&&n)throw Error(a(425))}function Jr(){}var eo=null,to=null;function no(e,t){return"textarea"===e||"noscript"===e||"string"==typeof t.children||"number"==typeof t.children||"object"==typeof t.dangerouslySetInnerHTML&&null!==t.dangerouslySetInnerHTML&&null!=t.dangerouslySetInnerHTML.__html}var ro="function"==typeof setTimeout?setTimeout:void 0,oo="function"==typeof clearTimeout?clearTimeout:void 0,ao="function"==typeof Promise?Promise:void 0,io="function"==typeof queueMicrotask?queueMicrotask:void 0!==ao?function(e){return ao.resolve(null).then(e).catch(lo)}:ro;function lo(e){setTimeout((function(){throw e}))}function so(e,t){var n=t,r=0;do{var o=n.nextSibling;if(e.removeChild(n),o&&8===o.nodeType)if("/$"===(n=o.data)){if(0===r)return e.removeChild(o),void Ut(t);r--}else"$"!==n&&"$?"!==n&&"$!"!==n||r++;n=o}while(n);Ut(t)}function co(e){for(;null!=e;e=e.nextSibling){var t=e.nodeType;if(1===t||3===t)break;if(8===t){if("$"===(t=e.data)||"$!"===t||"$?"===t)break;if("/$"===t)return null}}return e}function uo(e){e=e.previousSibling;for(var t=0;e;){if(8===e.nodeType){var n=e.data;if("$"===n||"$!"===n||"$?"===n){if(0===t)return e;t--}else"/$"===n&&t++}e=e.previousSibling}return null}var po=Math.random().toString(36).slice(2),fo="__reactFiber$"+po,mo="__reactProps$"+po,go="__reactContainer$"+po,ho="__reactEvents$"+po,bo="__reactListeners$"+po,yo="__reactHandles$"+po;function vo(e){var t=e[fo];if(t)return t;for(var n=e.parentNode;n;){if(t=n[go]||n[fo]){if(n=t.alternate,null!==t.child||null!==n&&null!==n.child)for(e=uo(e);null!==e;){if(n=e[fo])return n;e=uo(e)}return t}n=(e=n).parentNode}return null}function wo(e){return!(e=e[fo]||e[go])||5!==e.tag&&6!==e.tag&&13!==e.tag&&3!==e.tag?null:e}function ko(e){if(5===e.tag||6===e.tag)return e.stateNode;throw Error(a(33))}function xo(e){return e[mo]||null}var So=[],Eo=-1;function _o(e){return{current:e}}function Co(e){0>Eo||(e.current=So[Eo],So[Eo]=null,Eo--)}function To(e,t){Eo++,So[Eo]=e.current,e.current=t}var jo={},Ao=_o(jo),Lo=_o(!1),No=jo;function Ro(e,t){var n=e.type.contextTypes;if(!n)return jo;var r=e.stateNode;if(r&&r.__reactInternalMemoizedUnmaskedChildContext===t)return r.__reactInternalMemoizedMaskedChildContext;var o,a={};for(o in n)a[o]=t[o];return r&&((e=e.stateNode).__reactInternalMemoizedUnmaskedChildContext=t,e.__reactInternalMemoizedMaskedChildContext=a),a}function Po(e){return null!=(e=e.childContextTypes)}function Oo(){Co(Lo),Co(Ao)}function Do(e,t,n){if(Ao.current!==jo)throw Error(a(168));To(Ao,t),To(Lo,n)}function Io(e,t,n){var r=e.stateNode;if(t=t.childContextTypes,"function"!=typeof r.getChildContext)return n;for(var o in r=r.getChildContext())if(!(o in t))throw Error(a(108,q(e)||"Unknown",o));return F({},n,r)}function Fo(e){return e=(e=e.stateNode)&&e.__reactInternalMemoizedMergedChildContext||jo,No=Ao.current,To(Ao,e),To(Lo,Lo.current),!0}function Mo(e,t,n){var r=e.stateNode;if(!r)throw Error(a(169));n?(e=Io(e,t,No),r.__reactInternalMemoizedMergedChildContext=e,Co(Lo),Co(Ao),To(Ao,e)):Co(Lo),To(Lo,n)}var zo=null,Bo=!1,$o=!1;function Uo(e){null===zo?zo=[e]:zo.push(e)}function qo(){if(!$o&&null!==zo){$o=!0;var e=0,t=vt;try{var n=zo;for(vt=1;e<n.length;e++){var r=n[e];do{r=r(!0)}while(null!==r)}zo=null,Bo=!1}catch(o){throw null!==zo&&(zo=zo.slice(e+1)),Ve(Je,qo),o}finally{vt=t,$o=!1}}return null}var Ho=[],Zo=0,Go=null,Vo=0,Wo=[],Qo=0,Ko=null,Yo=1,Xo="";function Jo(e,t){Ho[Zo++]=Vo,Ho[Zo++]=Go,Go=e,Vo=t}function ea(e,t,n){Wo[Qo++]=Yo,Wo[Qo++]=Xo,Wo[Qo++]=Ko,Ko=e;var r=Yo;e=Xo;var o=32-it(r)-1;r&=~(1<<o),n+=1;var a=32-it(t)+o;if(30<a){var i=o-o%5;a=(r&(1<<i)-1).toString(32),r>>=i,o-=i,Yo=1<<32-it(t)+o|n<<o|r,Xo=a+e}else Yo=1<<a|n<<o|r,Xo=e}function ta(e){null!==e.return&&(Jo(e,1),ea(e,1,0))}function na(e){for(;e===Go;)Go=Ho[--Zo],Ho[Zo]=null,Vo=Ho[--Zo],Ho[Zo]=null;for(;e===Ko;)Ko=Wo[--Qo],Wo[Qo]=null,Xo=Wo[--Qo],Wo[Qo]=null,Yo=Wo[--Qo],Wo[Qo]=null}var ra=null,oa=null,aa=!1,ia=null;function la(e,t){var n=Rc(5,null,null,0);n.elementType="DELETED",n.stateNode=t,n.return=e,null===(t=e.deletions)?(e.deletions=[n],e.flags|=16):t.push(n)}function sa(e,t){switch(e.tag){case 5:var n=e.type;return null!==(t=1!==t.nodeType||n.toLowerCase()!==t.nodeName.toLowerCase()?null:t)&&(e.stateNode=t,ra=e,oa=co(t.firstChild),!0);case 6:return null!==(t=""===e.pendingProps||3!==t.nodeType?null:t)&&(e.stateNode=t,ra=e,oa=null,!0);case 13:return null!==(t=8!==t.nodeType?null:t)&&(n=null!==Ko?{id:Yo,overflow:Xo}:null,e.memoizedState={dehydrated:t,treeContext:n,retryLane:1073741824},(n=Rc(18,null,null,0)).stateNode=t,n.return=e,e.child=n,ra=e,oa=null,!0);default:return!1}}function ca(e){return 0!=(1&e.mode)&&0==(128&e.flags)}function ua(e){if(aa){var t=oa;if(t){var n=t;if(!sa(e,t)){if(ca(e))throw Error(a(418));t=co(n.nextSibling);var r=ra;t&&sa(e,t)?la(r,n):(e.flags=-4097&e.flags|2,aa=!1,ra=e)}}else{if(ca(e))throw Error(a(418));e.flags=-4097&e.flags|2,aa=!1,ra=e}}}function da(e){for(e=e.return;null!==e&&5!==e.tag&&3!==e.tag&&13!==e.tag;)e=e.return;ra=e}function pa(e){if(e!==ra)return!1;if(!aa)return da(e),aa=!0,!1;var t;if((t=3!==e.tag)&&!(t=5!==e.tag)&&(t="head"!==(t=e.type)&&"body"!==t&&!no(e.type,e.memoizedProps)),t&&(t=oa)){if(ca(e))throw fa(),Error(a(418));for(;t;)la(e,t),t=co(t.nextSibling)}if(da(e),13===e.tag){if(!(e=null!==(e=e.memoizedState)?e.dehydrated:null))throw Error(a(317));e:{for(e=e.nextSibling,t=0;e;){if(8===e.nodeType){var n=e.data;if("/$"===n){if(0===t){oa=co(e.nextSibling);break e}t--}else"$"!==n&&"$!"!==n&&"$?"!==n||t++}e=e.nextSibling}oa=null}}else oa=ra?co(e.stateNode.nextSibling):null;return!0}function fa(){for(var e=oa;e;)e=co(e.nextSibling)}function ma(){oa=ra=null,aa=!1}function ga(e){null===ia?ia=[e]:ia.push(e)}var ha=w.ReactCurrentBatchConfig;function ba(e,t){if(e&&e.defaultProps){for(var n in t=F({},t),e=e.defaultProps)void 0===t[n]&&(t[n]=e[n]);return t}return t}var ya=_o(null),va=null,wa=null,ka=null;function xa(){ka=wa=va=null}function Sa(e){var t=ya.current;Co(ya),e._currentValue=t}function Ea(e,t,n){for(;null!==e;){var r=e.alternate;if((e.childLanes&t)!==t?(e.childLanes|=t,null!==r&&(r.childLanes|=t)):null!==r&&(r.childLanes&t)!==t&&(r.childLanes|=t),e===n)break;e=e.return}}function _a(e,t){va=e,ka=wa=null,null!==(e=e.dependencies)&&null!==e.firstContext&&(0!=(e.lanes&t)&&(wl=!0),e.firstContext=null)}function Ca(e){var t=e._currentValue;if(ka!==e)if(e={context:e,memoizedValue:t,next:null},null===wa){if(null===va)throw Error(a(308));wa=e,va.dependencies={lanes:0,firstContext:e}}else wa=wa.next=e;return t}var Ta=null;function ja(e){null===Ta?Ta=[e]:Ta.push(e)}function Aa(e,t,n,r){var o=t.interleaved;return null===o?(n.next=n,ja(t)):(n.next=o.next,o.next=n),t.interleaved=n,La(e,r)}function La(e,t){e.lanes|=t;var n=e.alternate;for(null!==n&&(n.lanes|=t),n=e,e=e.return;null!==e;)e.childLanes|=t,null!==(n=e.alternate)&&(n.childLanes|=t),n=e,e=e.return;return 3===n.tag?n.stateNode:null}var Na=!1;function Ra(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,interleaved:null,lanes:0},effects:null}}function Pa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,effects:e.effects})}function Oa(e,t){return{eventTime:e,lane:t,tag:0,payload:null,callback:null,next:null}}function Da(e,t,n){var r=e.updateQueue;if(null===r)return null;if(r=r.shared,0!=(2&As)){var o=r.pending;return null===o?t.next=t:(t.next=o.next,o.next=t),r.pending=t,La(e,n)}return null===(o=r.interleaved)?(t.next=t,ja(r)):(t.next=o.next,o.next=t),r.interleaved=t,La(e,n)}function Ia(e,t,n){if(null!==(t=t.updateQueue)&&(t=t.shared,0!=(4194240&n))){var r=t.lanes;n|=r&=e.pendingLanes,t.lanes=n,yt(e,n)}}function Fa(e,t){var n=e.updateQueue,r=e.alternate;if(null!==r&&n===(r=r.updateQueue)){var o=null,a=null;if(null!==(n=n.firstBaseUpdate)){do{var i={eventTime:n.eventTime,lane:n.lane,tag:n.tag,payload:n.payload,callback:n.callback,next:null};null===a?o=a=i:a=a.next=i,n=n.next}while(null!==n);null===a?o=a=t:a=a.next=t}else o=a=t;return n={baseState:r.baseState,firstBaseUpdate:o,lastBaseUpdate:a,shared:r.shared,effects:r.effects},void(e.updateQueue=n)}null===(e=n.lastBaseUpdate)?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}function Ma(e,t,n,r){var o=e.updateQueue;Na=!1;var a=o.firstBaseUpdate,i=o.lastBaseUpdate,l=o.shared.pending;if(null!==l){o.shared.pending=null;var s=l,c=s.next;s.next=null,null===i?a=c:i.next=c,i=s;var u=e.alternate;null!==u&&((l=(u=u.updateQueue).lastBaseUpdate)!==i&&(null===l?u.firstBaseUpdate=c:l.next=c,u.lastBaseUpdate=s))}if(null!==a){var d=o.baseState;for(i=0,u=c=s=null,l=a;;){var p=l.lane,f=l.eventTime;if((r&p)===p){null!==u&&(u=u.next={eventTime:f,lane:0,tag:l.tag,payload:l.payload,callback:l.callback,next:null});e:{var m=e,g=l;switch(p=t,f=n,g.tag){case 1:if("function"==typeof(m=g.payload)){d=m.call(f,d,p);break e}d=m;break e;case 3:m.flags=-65537&m.flags|128;case 0:if(null==(p="function"==typeof(m=g.payload)?m.call(f,d,p):m))break e;d=F({},d,p);break e;case 2:Na=!0}}null!==l.callback&&0!==l.lane&&(e.flags|=64,null===(p=o.effects)?o.effects=[l]:p.push(l))}else f={eventTime:f,lane:p,tag:l.tag,payload:l.payload,callback:l.callback,next:null},null===u?(c=u=f,s=d):u=u.next=f,i|=p;if(null===(l=l.next)){if(null===(l=o.shared.pending))break;l=(p=l).next,p.next=null,o.lastBaseUpdate=p,o.shared.pending=null}}if(null===u&&(s=d),o.baseState=s,o.firstBaseUpdate=c,o.lastBaseUpdate=u,null!==(t=o.shared.interleaved)){o=t;do{i|=o.lane,o=o.next}while(o!==t)}else null===a&&(o.shared.lanes=0);Fs|=i,e.lanes=i,e.memoizedState=d}}function za(e,t,n){if(e=t.effects,t.effects=null,null!==e)for(t=0;t<e.length;t++){var r=e[t],o=r.callback;if(null!==o){if(r.callback=null,r=n,"function"!=typeof o)throw Error(a(191,o));o.call(r)}}}var Ba=(new r.Component).refs;function $a(e,t,n,r){n=null==(n=n(r,t=e.memoizedState))?t:F({},t,n),e.memoizedState=n,0===e.lanes&&(e.updateQueue.baseState=n)}var Ua={isMounted:function(e){return!!(e=e._reactInternals)&&Ue(e)===e},enqueueSetState:function(e,t,n){e=e._reactInternals;var r=tc(),o=nc(e),a=Oa(r,o);a.payload=t,null!=n&&(a.callback=n),null!==(t=Da(e,a,o))&&(rc(t,e,o,r),Ia(t,e,o))},enqueueReplaceState:function(e,t,n){e=e._reactInternals;var r=tc(),o=nc(e),a=Oa(r,o);a.tag=1,a.payload=t,null!=n&&(a.callback=n),null!==(t=Da(e,a,o))&&(rc(t,e,o,r),Ia(t,e,o))},enqueueForceUpdate:function(e,t){e=e._reactInternals;var n=tc(),r=nc(e),o=Oa(n,r);o.tag=2,null!=t&&(o.callback=t),null!==(t=Da(e,o,r))&&(rc(t,e,r,n),Ia(t,e,r))}};function qa(e,t,n,r,o,a,i){return"function"==typeof(e=e.stateNode).shouldComponentUpdate?e.shouldComponentUpdate(r,a,i):!t.prototype||!t.prototype.isPureReactComponent||(!sr(n,r)||!sr(o,a))}function Ha(e,t,n){var r=!1,o=jo,a=t.contextType;return"object"==typeof a&&null!==a?a=Ca(a):(o=Po(t)?No:Ao.current,a=(r=null!=(r=t.contextTypes))?Ro(e,o):jo),t=new t(n,a),e.memoizedState=null!==t.state&&void 0!==t.state?t.state:null,t.updater=Ua,e.stateNode=t,t._reactInternals=e,r&&((e=e.stateNode).__reactInternalMemoizedUnmaskedChildContext=o,e.__reactInternalMemoizedMaskedChildContext=a),t}function Za(e,t,n,r){e=t.state,"function"==typeof t.componentWillReceiveProps&&t.componentWillReceiveProps(n,r),"function"==typeof t.UNSAFE_componentWillReceiveProps&&t.UNSAFE_componentWillReceiveProps(n,r),t.state!==e&&Ua.enqueueReplaceState(t,t.state,null)}function Ga(e,t,n,r){var o=e.stateNode;o.props=n,o.state=e.memoizedState,o.refs=Ba,Ra(e);var a=t.contextType;"object"==typeof a&&null!==a?o.context=Ca(a):(a=Po(t)?No:Ao.current,o.context=Ro(e,a)),o.state=e.memoizedState,"function"==typeof(a=t.getDerivedStateFromProps)&&($a(e,t,a,n),o.state=e.memoizedState),"function"==typeof t.getDerivedStateFromProps||"function"==typeof o.getSnapshotBeforeUpdate||"function"!=typeof o.UNSAFE_componentWillMount&&"function"!=typeof o.componentWillMount||(t=o.state,"function"==typeof o.componentWillMount&&o.componentWillMount(),"function"==typeof o.UNSAFE_componentWillMount&&o.UNSAFE_componentWillMount(),t!==o.state&&Ua.enqueueReplaceState(o,o.state,null),Ma(e,n,o,r),o.state=e.memoizedState),"function"==typeof o.componentDidMount&&(e.flags|=4194308)}function Va(e,t,n){if(null!==(e=n.ref)&&"function"!=typeof e&&"object"!=typeof e){if(n._owner){if(n=n._owner){if(1!==n.tag)throw Error(a(309));var r=n.stateNode}if(!r)throw Error(a(147,e));var o=r,i=""+e;return null!==t&&null!==t.ref&&"function"==typeof t.ref&&t.ref._stringRef===i?t.ref:(t=function(e){var t=o.refs;t===Ba&&(t=o.refs={}),null===e?delete t[i]:t[i]=e},t._stringRef=i,t)}if("string"!=typeof e)throw Error(a(284));if(!n._owner)throw Error(a(290,e))}return e}function Wa(e,t){throw e=Object.prototype.toString.call(t),Error(a(31,"[object Object]"===e?"object with keys {"+Object.keys(t).join(", ")+"}":e))}function Qa(e){return(0,e._init)(e._payload)}function Ka(e){function t(t,n){if(e){var r=t.deletions;null===r?(t.deletions=[n],t.flags|=16):r.push(n)}}function n(n,r){if(!e)return null;for(;null!==r;)t(n,r),r=r.sibling;return null}function r(e,t){for(e=new Map;null!==t;)null!==t.key?e.set(t.key,t):e.set(t.index,t),t=t.sibling;return e}function o(e,t){return(e=Oc(e,t)).index=0,e.sibling=null,e}function i(t,n,r){return t.index=r,e?null!==(r=t.alternate)?(r=r.index)<n?(t.flags|=2,n):r:(t.flags|=2,n):(t.flags|=1048576,n)}function l(t){return e&&null===t.alternate&&(t.flags|=2),t}function s(e,t,n,r){return null===t||6!==t.tag?((t=Mc(n,e.mode,r)).return=e,t):((t=o(t,n)).return=e,t)}function c(e,t,n,r){var a=n.type;return a===S?d(e,t,n.props.children,r,n.key):null!==t&&(t.elementType===a||"object"==typeof a&&null!==a&&a.$$typeof===R&&Qa(a)===t.type)?((r=o(t,n.props)).ref=Va(e,t,n),r.return=e,r):((r=Dc(n.type,n.key,n.props,null,e.mode,r)).ref=Va(e,t,n),r.return=e,r)}function u(e,t,n,r){return null===t||4!==t.tag||t.stateNode.containerInfo!==n.containerInfo||t.stateNode.implementation!==n.implementation?((t=zc(n,e.mode,r)).return=e,t):((t=o(t,n.children||[])).return=e,t)}function d(e,t,n,r,a){return null===t||7!==t.tag?((t=Ic(n,e.mode,r,a)).return=e,t):((t=o(t,n)).return=e,t)}function p(e,t,n){if("string"==typeof t&&""!==t||"number"==typeof t)return(t=Mc(""+t,e.mode,n)).return=e,t;if("object"==typeof t&&null!==t){switch(t.$$typeof){case k:return(n=Dc(t.type,t.key,t.props,null,e.mode,n)).ref=Va(e,null,t),n.return=e,n;case x:return(t=zc(t,e.mode,n)).return=e,t;case R:return p(e,(0,t._init)(t._payload),n)}if(te(t)||D(t))return(t=Ic(t,e.mode,n,null)).return=e,t;Wa(e,t)}return null}function f(e,t,n,r){var o=null!==t?t.key:null;if("string"==typeof n&&""!==n||"number"==typeof n)return null!==o?null:s(e,t,""+n,r);if("object"==typeof n&&null!==n){switch(n.$$typeof){case k:return n.key===o?c(e,t,n,r):null;case x:return n.key===o?u(e,t,n,r):null;case R:return f(e,t,(o=n._init)(n._payload),r)}if(te(n)||D(n))return null!==o?null:d(e,t,n,r,null);Wa(e,n)}return null}function m(e,t,n,r,o){if("string"==typeof r&&""!==r||"number"==typeof r)return s(t,e=e.get(n)||null,""+r,o);if("object"==typeof r&&null!==r){switch(r.$$typeof){case k:return c(t,e=e.get(null===r.key?n:r.key)||null,r,o);case x:return u(t,e=e.get(null===r.key?n:r.key)||null,r,o);case R:return m(e,t,n,(0,r._init)(r._payload),o)}if(te(r)||D(r))return d(t,e=e.get(n)||null,r,o,null);Wa(t,r)}return null}function g(o,a,l,s){for(var c=null,u=null,d=a,g=a=0,h=null;null!==d&&g<l.length;g++){d.index>g?(h=d,d=null):h=d.sibling;var b=f(o,d,l[g],s);if(null===b){null===d&&(d=h);break}e&&d&&null===b.alternate&&t(o,d),a=i(b,a,g),null===u?c=b:u.sibling=b,u=b,d=h}if(g===l.length)return n(o,d),aa&&Jo(o,g),c;if(null===d){for(;g<l.length;g++)null!==(d=p(o,l[g],s))&&(a=i(d,a,g),null===u?c=d:u.sibling=d,u=d);return aa&&Jo(o,g),c}for(d=r(o,d);g<l.length;g++)null!==(h=m(d,o,g,l[g],s))&&(e&&null!==h.alternate&&d.delete(null===h.key?g:h.key),a=i(h,a,g),null===u?c=h:u.sibling=h,u=h);return e&&d.forEach((function(e){return t(o,e)})),aa&&Jo(o,g),c}function h(o,l,s,c){var u=D(s);if("function"!=typeof u)throw Error(a(150));if(null==(s=u.call(s)))throw Error(a(151));for(var d=u=null,g=l,h=l=0,b=null,y=s.next();null!==g&&!y.done;h++,y=s.next()){g.index>h?(b=g,g=null):b=g.sibling;var v=f(o,g,y.value,c);if(null===v){null===g&&(g=b);break}e&&g&&null===v.alternate&&t(o,g),l=i(v,l,h),null===d?u=v:d.sibling=v,d=v,g=b}if(y.done)return n(o,g),aa&&Jo(o,h),u;if(null===g){for(;!y.done;h++,y=s.next())null!==(y=p(o,y.value,c))&&(l=i(y,l,h),null===d?u=y:d.sibling=y,d=y);return aa&&Jo(o,h),u}for(g=r(o,g);!y.done;h++,y=s.next())null!==(y=m(g,o,h,y.value,c))&&(e&&null!==y.alternate&&g.delete(null===y.key?h:y.key),l=i(y,l,h),null===d?u=y:d.sibling=y,d=y);return e&&g.forEach((function(e){return t(o,e)})),aa&&Jo(o,h),u}return function e(r,a,i,s){if("object"==typeof i&&null!==i&&i.type===S&&null===i.key&&(i=i.props.children),"object"==typeof i&&null!==i){switch(i.$$typeof){case k:e:{for(var c=i.key,u=a;null!==u;){if(u.key===c){if((c=i.type)===S){if(7===u.tag){n(r,u.sibling),(a=o(u,i.props.children)).return=r,r=a;break e}}else if(u.elementType===c||"object"==typeof c&&null!==c&&c.$$typeof===R&&Qa(c)===u.type){n(r,u.sibling),(a=o(u,i.props)).ref=Va(r,u,i),a.return=r,r=a;break e}n(r,u);break}t(r,u),u=u.sibling}i.type===S?((a=Ic(i.props.children,r.mode,s,i.key)).return=r,r=a):((s=Dc(i.type,i.key,i.props,null,r.mode,s)).ref=Va(r,a,i),s.return=r,r=s)}return l(r);case x:e:{for(u=i.key;null!==a;){if(a.key===u){if(4===a.tag&&a.stateNode.containerInfo===i.containerInfo&&a.stateNode.implementation===i.implementation){n(r,a.sibling),(a=o(a,i.children||[])).return=r,r=a;break e}n(r,a);break}t(r,a),a=a.sibling}(a=zc(i,r.mode,s)).return=r,r=a}return l(r);case R:return e(r,a,(u=i._init)(i._payload),s)}if(te(i))return g(r,a,i,s);if(D(i))return h(r,a,i,s);Wa(r,i)}return"string"==typeof i&&""!==i||"number"==typeof i?(i=""+i,null!==a&&6===a.tag?(n(r,a.sibling),(a=o(a,i)).return=r,r=a):(n(r,a),(a=Mc(i,r.mode,s)).return=r,r=a),l(r)):n(r,a)}}var Ya=Ka(!0),Xa=Ka(!1),Ja={},ei=_o(Ja),ti=_o(Ja),ni=_o(Ja);function ri(e){if(e===Ja)throw Error(a(174));return e}function oi(e,t){switch(To(ni,t),To(ti,e),To(ei,Ja),e=t.nodeType){case 9:case 11:t=(t=t.documentElement)?t.namespaceURI:se(null,"");break;default:t=se(t=(e=8===e?t.parentNode:t).namespaceURI||null,e=e.tagName)}Co(ei),To(ei,t)}function ai(){Co(ei),Co(ti),Co(ni)}function ii(e){ri(ni.current);var t=ri(ei.current),n=se(t,e.type);t!==n&&(To(ti,e),To(ei,n))}function li(e){ti.current===e&&(Co(ei),Co(ti))}var si=_o(0);function ci(e){for(var t=e;null!==t;){if(13===t.tag){var n=t.memoizedState;if(null!==n&&(null===(n=n.dehydrated)||"$?"===n.data||"$!"===n.data))return t}else if(19===t.tag&&void 0!==t.memoizedProps.revealOrder){if(0!=(128&t.flags))return t}else if(null!==t.child){t.child.return=t,t=t.child;continue}if(t===e)break;for(;null===t.sibling;){if(null===t.return||t.return===e)return null;t=t.return}t.sibling.return=t.return,t=t.sibling}return null}var ui=[];function di(){for(var e=0;e<ui.length;e++)ui[e]._workInProgressVersionPrimary=null;ui.length=0}var pi=w.ReactCurrentDispatcher,fi=w.ReactCurrentBatchConfig,mi=0,gi=null,hi=null,bi=null,yi=!1,vi=!1,wi=0,ki=0;function xi(){throw Error(a(321))}function Si(e,t){if(null===t)return!1;for(var n=0;n<t.length&&n<e.length;n++)if(!lr(e[n],t[n]))return!1;return!0}function Ei(e,t,n,r,o,i){if(mi=i,gi=t,t.memoizedState=null,t.updateQueue=null,t.lanes=0,pi.current=null===e||null===e.memoizedState?ll:sl,e=n(r,o),vi){i=0;do{if(vi=!1,wi=0,25<=i)throw Error(a(301));i+=1,bi=hi=null,t.updateQueue=null,pi.current=cl,e=n(r,o)}while(vi)}if(pi.current=il,t=null!==hi&&null!==hi.next,mi=0,bi=hi=gi=null,yi=!1,t)throw Error(a(300));return e}function _i(){var e=0!==wi;return wi=0,e}function Ci(){var e={memoizedState:null,baseState:null,baseQueue:null,queue:null,next:null};return null===bi?gi.memoizedState=bi=e:bi=bi.next=e,bi}function Ti(){if(null===hi){var e=gi.alternate;e=null!==e?e.memoizedState:null}else e=hi.next;var t=null===bi?gi.memoizedState:bi.next;if(null!==t)bi=t,hi=e;else{if(null===e)throw Error(a(310));e={memoizedState:(hi=e).memoizedState,baseState:hi.baseState,baseQueue:hi.baseQueue,queue:hi.queue,next:null},null===bi?gi.memoizedState=bi=e:bi=bi.next=e}return bi}function ji(e,t){return"function"==typeof t?t(e):t}function Ai(e){var t=Ti(),n=t.queue;if(null===n)throw Error(a(311));n.lastRenderedReducer=e;var r=hi,o=r.baseQueue,i=n.pending;if(null!==i){if(null!==o){var l=o.next;o.next=i.next,i.next=l}r.baseQueue=o=i,n.pending=null}if(null!==o){i=o.next,r=r.baseState;var s=l=null,c=null,u=i;do{var d=u.lane;if((mi&d)===d)null!==c&&(c=c.next={lane:0,action:u.action,hasEagerState:u.hasEagerState,eagerState:u.eagerState,next:null}),r=u.hasEagerState?u.eagerState:e(r,u.action);else{var p={lane:d,action:u.action,hasEagerState:u.hasEagerState,eagerState:u.eagerState,next:null};null===c?(s=c=p,l=r):c=c.next=p,gi.lanes|=d,Fs|=d}u=u.next}while(null!==u&&u!==i);null===c?l=r:c.next=s,lr(r,t.memoizedState)||(wl=!0),t.memoizedState=r,t.baseState=l,t.baseQueue=c,n.lastRenderedState=r}if(null!==(e=n.interleaved)){o=e;do{i=o.lane,gi.lanes|=i,Fs|=i,o=o.next}while(o!==e)}else null===o&&(n.lanes=0);return[t.memoizedState,n.dispatch]}function Li(e){var t=Ti(),n=t.queue;if(null===n)throw Error(a(311));n.lastRenderedReducer=e;var r=n.dispatch,o=n.pending,i=t.memoizedState;if(null!==o){n.pending=null;var l=o=o.next;do{i=e(i,l.action),l=l.next}while(l!==o);lr(i,t.memoizedState)||(wl=!0),t.memoizedState=i,null===t.baseQueue&&(t.baseState=i),n.lastRenderedState=i}return[i,r]}function Ni(){}function Ri(e,t){var n=gi,r=Ti(),o=t(),i=!lr(r.memoizedState,o);if(i&&(r.memoizedState=o,wl=!0),r=r.queue,Hi(Di.bind(null,n,r,e),[e]),r.getSnapshot!==t||i||null!==bi&&1&bi.memoizedState.tag){if(n.flags|=2048,zi(9,Oi.bind(null,n,r,o,t),void 0,null),null===Ls)throw Error(a(349));0!=(30&mi)||Pi(n,t,o)}return o}function Pi(e,t,n){e.flags|=16384,e={getSnapshot:t,value:n},null===(t=gi.updateQueue)?(t={lastEffect:null,stores:null},gi.updateQueue=t,t.stores=[e]):null===(n=t.stores)?t.stores=[e]:n.push(e)}function Oi(e,t,n,r){t.value=n,t.getSnapshot=r,Ii(t)&&Fi(e)}function Di(e,t,n){return n((function(){Ii(t)&&Fi(e)}))}function Ii(e){var t=e.getSnapshot;e=e.value;try{var n=t();return!lr(e,n)}catch(r){return!0}}function Fi(e){var t=La(e,1);null!==t&&rc(t,e,1,-1)}function Mi(e){var t=Ci();return"function"==typeof e&&(e=e()),t.memoizedState=t.baseState=e,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:ji,lastRenderedState:e},t.queue=e,e=e.dispatch=nl.bind(null,gi,e),[t.memoizedState,e]}function zi(e,t,n,r){return e={tag:e,create:t,destroy:n,deps:r,next:null},null===(t=gi.updateQueue)?(t={lastEffect:null,stores:null},gi.updateQueue=t,t.lastEffect=e.next=e):null===(n=t.lastEffect)?t.lastEffect=e.next=e:(r=n.next,n.next=e,e.next=r,t.lastEffect=e),e}function Bi(){return Ti().memoizedState}function $i(e,t,n,r){var o=Ci();gi.flags|=e,o.memoizedState=zi(1|t,n,void 0,void 0===r?null:r)}function Ui(e,t,n,r){var o=Ti();r=void 0===r?null:r;var a=void 0;if(null!==hi){var i=hi.memoizedState;if(a=i.destroy,null!==r&&Si(r,i.deps))return void(o.memoizedState=zi(t,n,a,r))}gi.flags|=e,o.memoizedState=zi(1|t,n,a,r)}function qi(e,t){return $i(8390656,8,e,t)}function Hi(e,t){return Ui(2048,8,e,t)}function Zi(e,t){return Ui(4,2,e,t)}function Gi(e,t){return Ui(4,4,e,t)}function Vi(e,t){return"function"==typeof t?(e=e(),t(e),function(){t(null)}):null!=t?(e=e(),t.current=e,function(){t.current=null}):void 0}function Wi(e,t,n){return n=null!=n?n.concat([e]):null,Ui(4,4,Vi.bind(null,t,e),n)}function Qi(){}function Ki(e,t){var n=Ti();t=void 0===t?null:t;var r=n.memoizedState;return null!==r&&null!==t&&Si(t,r[1])?r[0]:(n.memoizedState=[e,t],e)}function Yi(e,t){var n=Ti();t=void 0===t?null:t;var r=n.memoizedState;return null!==r&&null!==t&&Si(t,r[1])?r[0]:(e=e(),n.memoizedState=[e,t],e)}function Xi(e,t,n){return 0==(21&mi)?(e.baseState&&(e.baseState=!1,wl=!0),e.memoizedState=n):(lr(n,t)||(n=gt(),gi.lanes|=n,Fs|=n,e.baseState=!0),t)}function Ji(e,t){var n=vt;vt=0!==n&&4>n?n:4,e(!0);var r=fi.transition;fi.transition={};try{e(!1),t()}finally{vt=n,fi.transition=r}}function el(){return Ti().memoizedState}function tl(e,t,n){var r=nc(e);if(n={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null},rl(e))ol(t,n);else if(null!==(n=Aa(e,t,n,r))){rc(n,e,r,tc()),al(n,t,r)}}function nl(e,t,n){var r=nc(e),o={lane:r,action:n,hasEagerState:!1,eagerState:null,next:null};if(rl(e))ol(t,o);else{var a=e.alternate;if(0===e.lanes&&(null===a||0===a.lanes)&&null!==(a=t.lastRenderedReducer))try{var i=t.lastRenderedState,l=a(i,n);if(o.hasEagerState=!0,o.eagerState=l,lr(l,i)){var s=t.interleaved;return null===s?(o.next=o,ja(t)):(o.next=s.next,s.next=o),void(t.interleaved=o)}}catch(c){}null!==(n=Aa(e,t,o,r))&&(rc(n,e,r,o=tc()),al(n,t,r))}}function rl(e){var t=e.alternate;return e===gi||null!==t&&t===gi}function ol(e,t){vi=yi=!0;var n=e.pending;null===n?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function al(e,t,n){if(0!=(4194240&n)){var r=t.lanes;n|=r&=e.pendingLanes,t.lanes=n,yt(e,n)}}var il={readContext:Ca,useCallback:xi,useContext:xi,useEffect:xi,useImperativeHandle:xi,useInsertionEffect:xi,useLayoutEffect:xi,useMemo:xi,useReducer:xi,useRef:xi,useState:xi,useDebugValue:xi,useDeferredValue:xi,useTransition:xi,useMutableSource:xi,useSyncExternalStore:xi,useId:xi,unstable_isNewReconciler:!1},ll={readContext:Ca,useCallback:function(e,t){return Ci().memoizedState=[e,void 0===t?null:t],e},useContext:Ca,useEffect:qi,useImperativeHandle:function(e,t,n){return n=null!=n?n.concat([e]):null,$i(4194308,4,Vi.bind(null,t,e),n)},useLayoutEffect:function(e,t){return $i(4194308,4,e,t)},useInsertionEffect:function(e,t){return $i(4,2,e,t)},useMemo:function(e,t){var n=Ci();return t=void 0===t?null:t,e=e(),n.memoizedState=[e,t],e},useReducer:function(e,t,n){var r=Ci();return t=void 0!==n?n(t):t,r.memoizedState=r.baseState=t,e={pending:null,interleaved:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:t},r.queue=e,e=e.dispatch=tl.bind(null,gi,e),[r.memoizedState,e]},useRef:function(e){return e={current:e},Ci().memoizedState=e},useState:Mi,useDebugValue:Qi,useDeferredValue:function(e){return Ci().memoizedState=e},useTransition:function(){var e=Mi(!1),t=e[0];return e=Ji.bind(null,e[1]),Ci().memoizedState=e,[t,e]},useMutableSource:function(){},useSyncExternalStore:function(e,t,n){var r=gi,o=Ci();if(aa){if(void 0===n)throw Error(a(407));n=n()}else{if(n=t(),null===Ls)throw Error(a(349));0!=(30&mi)||Pi(r,t,n)}o.memoizedState=n;var i={value:n,getSnapshot:t};return o.queue=i,qi(Di.bind(null,r,i,e),[e]),r.flags|=2048,zi(9,Oi.bind(null,r,i,n,t),void 0,null),n},useId:function(){var e=Ci(),t=Ls.identifierPrefix;if(aa){var n=Xo;t=":"+t+"R"+(n=(Yo&~(1<<32-it(Yo)-1)).toString(32)+n),0<(n=wi++)&&(t+="H"+n.toString(32)),t+=":"}else t=":"+t+"r"+(n=ki++).toString(32)+":";return e.memoizedState=t},unstable_isNewReconciler:!1},sl={readContext:Ca,useCallback:Ki,useContext:Ca,useEffect:Hi,useImperativeHandle:Wi,useInsertionEffect:Zi,useLayoutEffect:Gi,useMemo:Yi,useReducer:Ai,useRef:Bi,useState:function(){return Ai(ji)},useDebugValue:Qi,useDeferredValue:function(e){return Xi(Ti(),hi.memoizedState,e)},useTransition:function(){return[Ai(ji)[0],Ti().memoizedState]},useMutableSource:Ni,useSyncExternalStore:Ri,useId:el,unstable_isNewReconciler:!1},cl={readContext:Ca,useCallback:Ki,useContext:Ca,useEffect:Hi,useImperativeHandle:Wi,useInsertionEffect:Zi,useLayoutEffect:Gi,useMemo:Yi,useReducer:Li,useRef:Bi,useState:function(){return Li(ji)},useDebugValue:Qi,useDeferredValue:function(e){var t=Ti();return null===hi?t.memoizedState=e:Xi(t,hi.memoizedState,e)},useTransition:function(){return[Li(ji)[0],Ti().memoizedState]},useMutableSource:Ni,useSyncExternalStore:Ri,useId:el,unstable_isNewReconciler:!1};function ul(e,t){try{var n="",r=t;do{n+=$(r),r=r.return}while(r);var o=n}catch(a){o="\nError generating stack: "+a.message+"\n"+a.stack}return{value:e,source:t,stack:o,digest:null}}function dl(e,t,n){return{value:e,source:null,stack:null!=n?n:null,digest:null!=t?t:null}}function pl(e,t){try{console.error(t.value)}catch(n){setTimeout((function(){throw n}))}}var fl="function"==typeof WeakMap?WeakMap:Map;function ml(e,t,n){(n=Oa(-1,n)).tag=3,n.payload={element:null};var r=t.value;return n.callback=function(){Zs||(Zs=!0,Gs=r),pl(0,t)},n}function gl(e,t,n){(n=Oa(-1,n)).tag=3;var r=e.type.getDerivedStateFromError;if("function"==typeof r){var o=t.value;n.payload=function(){return r(o)},n.callback=function(){pl(0,t)}}var a=e.stateNode;return null!==a&&"function"==typeof a.componentDidCatch&&(n.callback=function(){pl(0,t),"function"!=typeof r&&(null===Vs?Vs=new Set([this]):Vs.add(this));var e=t.stack;this.componentDidCatch(t.value,{componentStack:null!==e?e:""})}),n}function hl(e,t,n){var r=e.pingCache;if(null===r){r=e.pingCache=new fl;var o=new Set;r.set(t,o)}else void 0===(o=r.get(t))&&(o=new Set,r.set(t,o));o.has(n)||(o.add(n),e=Cc.bind(null,e,t,n),t.then(e,e))}function bl(e){do{var t;if((t=13===e.tag)&&(t=null===(t=e.memoizedState)||null!==t.dehydrated),t)return e;e=e.return}while(null!==e);return null}function yl(e,t,n,r,o){return 0==(1&e.mode)?(e===t?e.flags|=65536:(e.flags|=128,n.flags|=131072,n.flags&=-52805,1===n.tag&&(null===n.alternate?n.tag=17:((t=Oa(-1,1)).tag=2,Da(n,t,1))),n.lanes|=1),e):(e.flags|=65536,e.lanes=o,e)}var vl=w.ReactCurrentOwner,wl=!1;function kl(e,t,n,r){t.child=null===e?Xa(t,null,n,r):Ya(t,e.child,n,r)}function xl(e,t,n,r,o){n=n.render;var a=t.ref;return _a(t,o),r=Ei(e,t,n,r,a,o),n=_i(),null===e||wl?(aa&&n&&ta(t),t.flags|=1,kl(e,t,r,o),t.child):(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~o,Zl(e,t,o))}function Sl(e,t,n,r,o){if(null===e){var a=n.type;return"function"!=typeof a||Pc(a)||void 0!==a.defaultProps||null!==n.compare||void 0!==n.defaultProps?((e=Dc(n.type,null,r,t,t.mode,o)).ref=t.ref,e.return=t,t.child=e):(t.tag=15,t.type=a,El(e,t,a,r,o))}if(a=e.child,0==(e.lanes&o)){var i=a.memoizedProps;if((n=null!==(n=n.compare)?n:sr)(i,r)&&e.ref===t.ref)return Zl(e,t,o)}return t.flags|=1,(e=Oc(a,r)).ref=t.ref,e.return=t,t.child=e}function El(e,t,n,r,o){if(null!==e){var a=e.memoizedProps;if(sr(a,r)&&e.ref===t.ref){if(wl=!1,t.pendingProps=r=a,0==(e.lanes&o))return t.lanes=e.lanes,Zl(e,t,o);0!=(131072&e.flags)&&(wl=!0)}}return Tl(e,t,n,r,o)}function _l(e,t,n){var r=t.pendingProps,o=r.children,a=null!==e?e.memoizedState:null;if("hidden"===r.mode)if(0==(1&t.mode))t.memoizedState={baseLanes:0,cachePool:null,transitions:null},To(Os,Ps),Ps|=n;else{if(0==(1073741824&n))return e=null!==a?a.baseLanes|n:n,t.lanes=t.childLanes=1073741824,t.memoizedState={baseLanes:e,cachePool:null,transitions:null},t.updateQueue=null,To(Os,Ps),Ps|=e,null;t.memoizedState={baseLanes:0,cachePool:null,transitions:null},r=null!==a?a.baseLanes:n,To(Os,Ps),Ps|=r}else null!==a?(r=a.baseLanes|n,t.memoizedState=null):r=n,To(Os,Ps),Ps|=r;return kl(e,t,o,n),t.child}function Cl(e,t){var n=t.ref;(null===e&&null!==n||null!==e&&e.ref!==n)&&(t.flags|=512,t.flags|=2097152)}function Tl(e,t,n,r,o){var a=Po(n)?No:Ao.current;return a=Ro(t,a),_a(t,o),n=Ei(e,t,n,r,a,o),r=_i(),null===e||wl?(aa&&r&&ta(t),t.flags|=1,kl(e,t,n,o),t.child):(t.updateQueue=e.updateQueue,t.flags&=-2053,e.lanes&=~o,Zl(e,t,o))}function jl(e,t,n,r,o){if(Po(n)){var a=!0;Fo(t)}else a=!1;if(_a(t,o),null===t.stateNode)Hl(e,t),Ha(t,n,r),Ga(t,n,r,o),r=!0;else if(null===e){var i=t.stateNode,l=t.memoizedProps;i.props=l;var s=i.context,c=n.contextType;"object"==typeof c&&null!==c?c=Ca(c):c=Ro(t,c=Po(n)?No:Ao.current);var u=n.getDerivedStateFromProps,d="function"==typeof u||"function"==typeof i.getSnapshotBeforeUpdate;d||"function"!=typeof i.UNSAFE_componentWillReceiveProps&&"function"!=typeof i.componentWillReceiveProps||(l!==r||s!==c)&&Za(t,i,r,c),Na=!1;var p=t.memoizedState;i.state=p,Ma(t,r,i,o),s=t.memoizedState,l!==r||p!==s||Lo.current||Na?("function"==typeof u&&($a(t,n,u,r),s=t.memoizedState),(l=Na||qa(t,n,l,r,p,s,c))?(d||"function"!=typeof i.UNSAFE_componentWillMount&&"function"!=typeof i.componentWillMount||("function"==typeof i.componentWillMount&&i.componentWillMount(),"function"==typeof i.UNSAFE_componentWillMount&&i.UNSAFE_componentWillMount()),"function"==typeof i.componentDidMount&&(t.flags|=4194308)):("function"==typeof i.componentDidMount&&(t.flags|=4194308),t.memoizedProps=r,t.memoizedState=s),i.props=r,i.state=s,i.context=c,r=l):("function"==typeof i.componentDidMount&&(t.flags|=4194308),r=!1)}else{i=t.stateNode,Pa(e,t),l=t.memoizedProps,c=t.type===t.elementType?l:ba(t.type,l),i.props=c,d=t.pendingProps,p=i.context,"object"==typeof(s=n.contextType)&&null!==s?s=Ca(s):s=Ro(t,s=Po(n)?No:Ao.current);var f=n.getDerivedStateFromProps;(u="function"==typeof f||"function"==typeof i.getSnapshotBeforeUpdate)||"function"!=typeof i.UNSAFE_componentWillReceiveProps&&"function"!=typeof i.componentWillReceiveProps||(l!==d||p!==s)&&Za(t,i,r,s),Na=!1,p=t.memoizedState,i.state=p,Ma(t,r,i,o);var m=t.memoizedState;l!==d||p!==m||Lo.current||Na?("function"==typeof f&&($a(t,n,f,r),m=t.memoizedState),(c=Na||qa(t,n,c,r,p,m,s)||!1)?(u||"function"!=typeof i.UNSAFE_componentWillUpdate&&"function"!=typeof i.componentWillUpdate||("function"==typeof i.componentWillUpdate&&i.componentWillUpdate(r,m,s),"function"==typeof i.UNSAFE_componentWillUpdate&&i.UNSAFE_componentWillUpdate(r,m,s)),"function"==typeof i.componentDidUpdate&&(t.flags|=4),"function"==typeof i.getSnapshotBeforeUpdate&&(t.flags|=1024)):("function"!=typeof i.componentDidUpdate||l===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),"function"!=typeof i.getSnapshotBeforeUpdate||l===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),t.memoizedProps=r,t.memoizedState=m),i.props=r,i.state=m,i.context=s,r=c):("function"!=typeof i.componentDidUpdate||l===e.memoizedProps&&p===e.memoizedState||(t.flags|=4),"function"!=typeof i.getSnapshotBeforeUpdate||l===e.memoizedProps&&p===e.memoizedState||(t.flags|=1024),r=!1)}return Al(e,t,n,r,a,o)}function Al(e,t,n,r,o,a){Cl(e,t);var i=0!=(128&t.flags);if(!r&&!i)return o&&Mo(t,n,!1),Zl(e,t,a);r=t.stateNode,vl.current=t;var l=i&&"function"!=typeof n.getDerivedStateFromError?null:r.render();return t.flags|=1,null!==e&&i?(t.child=Ya(t,e.child,null,a),t.child=Ya(t,null,l,a)):kl(e,t,l,a),t.memoizedState=r.state,o&&Mo(t,n,!0),t.child}function Ll(e){var t=e.stateNode;t.pendingContext?Do(0,t.pendingContext,t.pendingContext!==t.context):t.context&&Do(0,t.context,!1),oi(e,t.containerInfo)}function Nl(e,t,n,r,o){return ma(),ga(o),t.flags|=256,kl(e,t,n,r),t.child}var Rl,Pl,Ol,Dl,Il={dehydrated:null,treeContext:null,retryLane:0};function Fl(e){return{baseLanes:e,cachePool:null,transitions:null}}function Ml(e,t,n){var r,o=t.pendingProps,i=si.current,l=!1,s=0!=(128&t.flags);if((r=s)||(r=(null===e||null!==e.memoizedState)&&0!=(2&i)),r?(l=!0,t.flags&=-129):null!==e&&null===e.memoizedState||(i|=1),To(si,1&i),null===e)return ua(t),null!==(e=t.memoizedState)&&null!==(e=e.dehydrated)?(0==(1&t.mode)?t.lanes=1:"$!"===e.data?t.lanes=8:t.lanes=1073741824,null):(s=o.children,e=o.fallback,l?(o=t.mode,l=t.child,s={mode:"hidden",children:s},0==(1&o)&&null!==l?(l.childLanes=0,l.pendingProps=s):l=Fc(s,o,0,null),e=Ic(e,o,n,null),l.return=t,e.return=t,l.sibling=e,t.child=l,t.child.memoizedState=Fl(n),t.memoizedState=Il,e):zl(t,s));if(null!==(i=e.memoizedState)&&null!==(r=i.dehydrated))return function(e,t,n,r,o,i,l){if(n)return 256&t.flags?(t.flags&=-257,Bl(e,t,l,r=dl(Error(a(422))))):null!==t.memoizedState?(t.child=e.child,t.flags|=128,null):(i=r.fallback,o=t.mode,r=Fc({mode:"visible",children:r.children},o,0,null),(i=Ic(i,o,l,null)).flags|=2,r.return=t,i.return=t,r.sibling=i,t.child=r,0!=(1&t.mode)&&Ya(t,e.child,null,l),t.child.memoizedState=Fl(l),t.memoizedState=Il,i);if(0==(1&t.mode))return Bl(e,t,l,null);if("$!"===o.data){if(r=o.nextSibling&&o.nextSibling.dataset)var s=r.dgst;return r=s,Bl(e,t,l,r=dl(i=Error(a(419)),r,void 0))}if(s=0!=(l&e.childLanes),wl||s){if(null!==(r=Ls)){switch(l&-l){case 4:o=2;break;case 16:o=8;break;case 64:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:case 4194304:case 8388608:case 16777216:case 33554432:case 67108864:o=32;break;case 536870912:o=268435456;break;default:o=0}0!==(o=0!=(o&(r.suspendedLanes|l))?0:o)&&o!==i.retryLane&&(i.retryLane=o,La(e,o),rc(r,e,o,-1))}return hc(),Bl(e,t,l,r=dl(Error(a(421))))}return"$?"===o.data?(t.flags|=128,t.child=e.child,t=jc.bind(null,e),o._reactRetry=t,null):(e=i.treeContext,oa=co(o.nextSibling),ra=t,aa=!0,ia=null,null!==e&&(Wo[Qo++]=Yo,Wo[Qo++]=Xo,Wo[Qo++]=Ko,Yo=e.id,Xo=e.overflow,Ko=t),t=zl(t,r.children),t.flags|=4096,t)}(e,t,s,o,r,i,n);if(l){l=o.fallback,s=t.mode,r=(i=e.child).sibling;var c={mode:"hidden",children:o.children};return 0==(1&s)&&t.child!==i?((o=t.child).childLanes=0,o.pendingProps=c,t.deletions=null):(o=Oc(i,c)).subtreeFlags=14680064&i.subtreeFlags,null!==r?l=Oc(r,l):(l=Ic(l,s,n,null)).flags|=2,l.return=t,o.return=t,o.sibling=l,t.child=o,o=l,l=t.child,s=null===(s=e.child.memoizedState)?Fl(n):{baseLanes:s.baseLanes|n,cachePool:null,transitions:s.transitions},l.memoizedState=s,l.childLanes=e.childLanes&~n,t.memoizedState=Il,o}return e=(l=e.child).sibling,o=Oc(l,{mode:"visible",children:o.children}),0==(1&t.mode)&&(o.lanes=n),o.return=t,o.sibling=null,null!==e&&(null===(n=t.deletions)?(t.deletions=[e],t.flags|=16):n.push(e)),t.child=o,t.memoizedState=null,o}function zl(e,t){return(t=Fc({mode:"visible",children:t},e.mode,0,null)).return=e,e.child=t}function Bl(e,t,n,r){return null!==r&&ga(r),Ya(t,e.child,null,n),(e=zl(t,t.pendingProps.children)).flags|=2,t.memoizedState=null,e}function $l(e,t,n){e.lanes|=t;var r=e.alternate;null!==r&&(r.lanes|=t),Ea(e.return,t,n)}function Ul(e,t,n,r,o){var a=e.memoizedState;null===a?e.memoizedState={isBackwards:t,rendering:null,renderingStartTime:0,last:r,tail:n,tailMode:o}:(a.isBackwards=t,a.rendering=null,a.renderingStartTime=0,a.last=r,a.tail=n,a.tailMode=o)}function ql(e,t,n){var r=t.pendingProps,o=r.revealOrder,a=r.tail;if(kl(e,t,r.children,n),0!=(2&(r=si.current)))r=1&r|2,t.flags|=128;else{if(null!==e&&0!=(128&e.flags))e:for(e=t.child;null!==e;){if(13===e.tag)null!==e.memoizedState&&$l(e,n,t);else if(19===e.tag)$l(e,n,t);else if(null!==e.child){e.child.return=e,e=e.child;continue}if(e===t)break e;for(;null===e.sibling;){if(null===e.return||e.return===t)break e;e=e.return}e.sibling.return=e.return,e=e.sibling}r&=1}if(To(si,r),0==(1&t.mode))t.memoizedState=null;else switch(o){case"forwards":for(n=t.child,o=null;null!==n;)null!==(e=n.alternate)&&null===ci(e)&&(o=n),n=n.sibling;null===(n=o)?(o=t.child,t.child=null):(o=n.sibling,n.sibling=null),Ul(t,!1,o,n,a);break;case"backwards":for(n=null,o=t.child,t.child=null;null!==o;){if(null!==(e=o.alternate)&&null===ci(e)){t.child=o;break}e=o.sibling,o.sibling=n,n=o,o=e}Ul(t,!0,n,null,a);break;case"together":Ul(t,!1,null,null,void 0);break;default:t.memoizedState=null}return t.child}function Hl(e,t){0==(1&t.mode)&&null!==e&&(e.alternate=null,t.alternate=null,t.flags|=2)}function Zl(e,t,n){if(null!==e&&(t.dependencies=e.dependencies),Fs|=t.lanes,0==(n&t.childLanes))return null;if(null!==e&&t.child!==e.child)throw Error(a(153));if(null!==t.child){for(n=Oc(e=t.child,e.pendingProps),t.child=n,n.return=t;null!==e.sibling;)e=e.sibling,(n=n.sibling=Oc(e,e.pendingProps)).return=t;n.sibling=null}return t.child}function Gl(e,t){if(!aa)switch(e.tailMode){case"hidden":t=e.tail;for(var n=null;null!==t;)null!==t.alternate&&(n=t),t=t.sibling;null===n?e.tail=null:n.sibling=null;break;case"collapsed":n=e.tail;for(var r=null;null!==n;)null!==n.alternate&&(r=n),n=n.sibling;null===r?t||null===e.tail?e.tail=null:e.tail.sibling=null:r.sibling=null}}function Vl(e){var t=null!==e.alternate&&e.alternate.child===e.child,n=0,r=0;if(t)for(var o=e.child;null!==o;)n|=o.lanes|o.childLanes,r|=14680064&o.subtreeFlags,r|=14680064&o.flags,o.return=e,o=o.sibling;else for(o=e.child;null!==o;)n|=o.lanes|o.childLanes,r|=o.subtreeFlags,r|=o.flags,o.return=e,o=o.sibling;return e.subtreeFlags|=r,e.childLanes=n,t}function Wl(e,t,n){var r=t.pendingProps;switch(na(t),t.tag){case 2:case 16:case 15:case 0:case 11:case 7:case 8:case 12:case 9:case 14:return Vl(t),null;case 1:case 17:return Po(t.type)&&Oo(),Vl(t),null;case 3:return r=t.stateNode,ai(),Co(Lo),Co(Ao),di(),r.pendingContext&&(r.context=r.pendingContext,r.pendingContext=null),null!==e&&null!==e.child||(pa(t)?t.flags|=4:null===e||e.memoizedState.isDehydrated&&0==(256&t.flags)||(t.flags|=1024,null!==ia&&(lc(ia),ia=null))),Pl(e,t),Vl(t),null;case 5:li(t);var o=ri(ni.current);if(n=t.type,null!==e&&null!=t.stateNode)Ol(e,t,n,r,o),e.ref!==t.ref&&(t.flags|=512,t.flags|=2097152);else{if(!r){if(null===t.stateNode)throw Error(a(166));return Vl(t),null}if(e=ri(ei.current),pa(t)){r=t.stateNode,n=t.type;var i=t.memoizedProps;switch(r[fo]=t,r[mo]=i,e=0!=(1&t.mode),n){case"dialog":zr("cancel",r),zr("close",r);break;case"iframe":case"object":case"embed":zr("load",r);break;case"video":case"audio":for(o=0;o<Dr.length;o++)zr(Dr[o],r);break;case"source":zr("error",r);break;case"img":case"image":case"link":zr("error",r),zr("load",r);break;case"details":zr("toggle",r);break;case"input":K(r,i),zr("invalid",r);break;case"select":r._wrapperState={wasMultiple:!!i.multiple},zr("invalid",r);break;case"textarea":oe(r,i),zr("invalid",r)}for(var s in ye(n,i),o=null,i)if(i.hasOwnProperty(s)){var c=i[s];"children"===s?"string"==typeof c?r.textContent!==c&&(!0!==i.suppressHydrationWarning&&Xr(r.textContent,c,e),o=["children",c]):"number"==typeof c&&r.textContent!==""+c&&(!0!==i.suppressHydrationWarning&&Xr(r.textContent,c,e),o=["children",""+c]):l.hasOwnProperty(s)&&null!=c&&"onScroll"===s&&zr("scroll",r)}switch(n){case"input":G(r),J(r,i,!0);break;case"textarea":G(r),ie(r);break;case"select":case"option":break;default:"function"==typeof i.onClick&&(r.onclick=Jr)}r=o,t.updateQueue=r,null!==r&&(t.flags|=4)}else{s=9===o.nodeType?o:o.ownerDocument,"http://www.w3.org/1999/xhtml"===e&&(e=le(n)),"http://www.w3.org/1999/xhtml"===e?"script"===n?((e=s.createElement("div")).innerHTML="<script><\/script>",e=e.removeChild(e.firstChild)):"string"==typeof r.is?e=s.createElement(n,{is:r.is}):(e=s.createElement(n),"select"===n&&(s=e,r.multiple?s.multiple=!0:r.size&&(s.size=r.size))):e=s.createElementNS(e,n),e[fo]=t,e[mo]=r,Rl(e,t,!1,!1),t.stateNode=e;e:{switch(s=ve(n,r),n){case"dialog":zr("cancel",e),zr("close",e),o=r;break;case"iframe":case"object":case"embed":zr("load",e),o=r;break;case"video":case"audio":for(o=0;o<Dr.length;o++)zr(Dr[o],e);o=r;break;case"source":zr("error",e),o=r;break;case"img":case"image":case"link":zr("error",e),zr("load",e),o=r;break;case"details":zr("toggle",e),o=r;break;case"input":K(e,r),o=Q(e,r),zr("invalid",e);break;case"option":default:o=r;break;case"select":e._wrapperState={wasMultiple:!!r.multiple},o=F({},r,{value:void 0}),zr("invalid",e);break;case"textarea":oe(e,r),o=re(e,r),zr("invalid",e)}for(i in ye(n,o),c=o)if(c.hasOwnProperty(i)){var u=c[i];"style"===i?he(e,u):"dangerouslySetInnerHTML"===i?null!=(u=u?u.__html:void 0)&&de(e,u):"children"===i?"string"==typeof u?("textarea"!==n||""!==u)&&pe(e,u):"number"==typeof u&&pe(e,""+u):"suppressContentEditableWarning"!==i&&"suppressHydrationWarning"!==i&&"autoFocus"!==i&&(l.hasOwnProperty(i)?null!=u&&"onScroll"===i&&zr("scroll",e):null!=u&&v(e,i,u,s))}switch(n){case"input":G(e),J(e,r,!1);break;case"textarea":G(e),ie(e);break;case"option":null!=r.value&&e.setAttribute("value",""+H(r.value));break;case"select":e.multiple=!!r.multiple,null!=(i=r.value)?ne(e,!!r.multiple,i,!1):null!=r.defaultValue&&ne(e,!!r.multiple,r.defaultValue,!0);break;default:"function"==typeof o.onClick&&(e.onclick=Jr)}switch(n){case"button":case"input":case"select":case"textarea":r=!!r.autoFocus;break e;case"img":r=!0;break e;default:r=!1}}r&&(t.flags|=4)}null!==t.ref&&(t.flags|=512,t.flags|=2097152)}return Vl(t),null;case 6:if(e&&null!=t.stateNode)Dl(e,t,e.memoizedProps,r);else{if("string"!=typeof r&&null===t.stateNode)throw Error(a(166));if(n=ri(ni.current),ri(ei.current),pa(t)){if(r=t.stateNode,n=t.memoizedProps,r[fo]=t,(i=r.nodeValue!==n)&&null!==(e=ra))switch(e.tag){case 3:Xr(r.nodeValue,n,0!=(1&e.mode));break;case 5:!0!==e.memoizedProps.suppressHydrationWarning&&Xr(r.nodeValue,n,0!=(1&e.mode))}i&&(t.flags|=4)}else(r=(9===n.nodeType?n:n.ownerDocument).createTextNode(r))[fo]=t,t.stateNode=r}return Vl(t),null;case 13:if(Co(si),r=t.memoizedState,null===e||null!==e.memoizedState&&null!==e.memoizedState.dehydrated){if(aa&&null!==oa&&0!=(1&t.mode)&&0==(128&t.flags))fa(),ma(),t.flags|=98560,i=!1;else if(i=pa(t),null!==r&&null!==r.dehydrated){if(null===e){if(!i)throw Error(a(318));if(!(i=null!==(i=t.memoizedState)?i.dehydrated:null))throw Error(a(317));i[fo]=t}else ma(),0==(128&t.flags)&&(t.memoizedState=null),t.flags|=4;Vl(t),i=!1}else null!==ia&&(lc(ia),ia=null),i=!0;if(!i)return 65536&t.flags?t:null}return 0!=(128&t.flags)?(t.lanes=n,t):((r=null!==r)!==(null!==e&&null!==e.memoizedState)&&r&&(t.child.flags|=8192,0!=(1&t.mode)&&(null===e||0!=(1&si.current)?0===Ds&&(Ds=3):hc())),null!==t.updateQueue&&(t.flags|=4),Vl(t),null);case 4:return ai(),Pl(e,t),null===e&&Ur(t.stateNode.containerInfo),Vl(t),null;case 10:return Sa(t.type._context),Vl(t),null;case 19:if(Co(si),null===(i=t.memoizedState))return Vl(t),null;if(r=0!=(128&t.flags),null===(s=i.rendering))if(r)Gl(i,!1);else{if(0!==Ds||null!==e&&0!=(128&e.flags))for(e=t.child;null!==e;){if(null!==(s=ci(e))){for(t.flags|=128,Gl(i,!1),null!==(r=s.updateQueue)&&(t.updateQueue=r,t.flags|=4),t.subtreeFlags=0,r=n,n=t.child;null!==n;)e=r,(i=n).flags&=14680066,null===(s=i.alternate)?(i.childLanes=0,i.lanes=e,i.child=null,i.subtreeFlags=0,i.memoizedProps=null,i.memoizedState=null,i.updateQueue=null,i.dependencies=null,i.stateNode=null):(i.childLanes=s.childLanes,i.lanes=s.lanes,i.child=s.child,i.subtreeFlags=0,i.deletions=null,i.memoizedProps=s.memoizedProps,i.memoizedState=s.memoizedState,i.updateQueue=s.updateQueue,i.type=s.type,e=s.dependencies,i.dependencies=null===e?null:{lanes:e.lanes,firstContext:e.firstContext}),n=n.sibling;return To(si,1&si.current|2),t.child}e=e.sibling}null!==i.tail&&Ye()>qs&&(t.flags|=128,r=!0,Gl(i,!1),t.lanes=4194304)}else{if(!r)if(null!==(e=ci(s))){if(t.flags|=128,r=!0,null!==(n=e.updateQueue)&&(t.updateQueue=n,t.flags|=4),Gl(i,!0),null===i.tail&&"hidden"===i.tailMode&&!s.alternate&&!aa)return Vl(t),null}else 2*Ye()-i.renderingStartTime>qs&&1073741824!==n&&(t.flags|=128,r=!0,Gl(i,!1),t.lanes=4194304);i.isBackwards?(s.sibling=t.child,t.child=s):(null!==(n=i.last)?n.sibling=s:t.child=s,i.last=s)}return null!==i.tail?(t=i.tail,i.rendering=t,i.tail=t.sibling,i.renderingStartTime=Ye(),t.sibling=null,n=si.current,To(si,r?1&n|2:1&n),t):(Vl(t),null);case 22:case 23:return pc(),r=null!==t.memoizedState,null!==e&&null!==e.memoizedState!==r&&(t.flags|=8192),r&&0!=(1&t.mode)?0!=(1073741824&Ps)&&(Vl(t),6&t.subtreeFlags&&(t.flags|=8192)):Vl(t),null;case 24:case 25:return null}throw Error(a(156,t.tag))}function Ql(e,t){switch(na(t),t.tag){case 1:return Po(t.type)&&Oo(),65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 3:return ai(),Co(Lo),Co(Ao),di(),0!=(65536&(e=t.flags))&&0==(128&e)?(t.flags=-65537&e|128,t):null;case 5:return li(t),null;case 13:if(Co(si),null!==(e=t.memoizedState)&&null!==e.dehydrated){if(null===t.alternate)throw Error(a(340));ma()}return 65536&(e=t.flags)?(t.flags=-65537&e|128,t):null;case 19:return Co(si),null;case 4:return ai(),null;case 10:return Sa(t.type._context),null;case 22:case 23:return pc(),null;default:return null}}Rl=function(e,t){for(var n=t.child;null!==n;){if(5===n.tag||6===n.tag)e.appendChild(n.stateNode);else if(4!==n.tag&&null!==n.child){n.child.return=n,n=n.child;continue}if(n===t)break;for(;null===n.sibling;){if(null===n.return||n.return===t)return;n=n.return}n.sibling.return=n.return,n=n.sibling}},Pl=function(){},Ol=function(e,t,n,r){var o=e.memoizedProps;if(o!==r){e=t.stateNode,ri(ei.current);var a,i=null;switch(n){case"input":o=Q(e,o),r=Q(e,r),i=[];break;case"select":o=F({},o,{value:void 0}),r=F({},r,{value:void 0}),i=[];break;case"textarea":o=re(e,o),r=re(e,r),i=[];break;default:"function"!=typeof o.onClick&&"function"==typeof r.onClick&&(e.onclick=Jr)}for(u in ye(n,r),n=null,o)if(!r.hasOwnProperty(u)&&o.hasOwnProperty(u)&&null!=o[u])if("style"===u){var s=o[u];for(a in s)s.hasOwnProperty(a)&&(n||(n={}),n[a]="")}else"dangerouslySetInnerHTML"!==u&&"children"!==u&&"suppressContentEditableWarning"!==u&&"suppressHydrationWarning"!==u&&"autoFocus"!==u&&(l.hasOwnProperty(u)?i||(i=[]):(i=i||[]).push(u,null));for(u in r){var c=r[u];if(s=null!=o?o[u]:void 0,r.hasOwnProperty(u)&&c!==s&&(null!=c||null!=s))if("style"===u)if(s){for(a in s)!s.hasOwnProperty(a)||c&&c.hasOwnProperty(a)||(n||(n={}),n[a]="");for(a in c)c.hasOwnProperty(a)&&s[a]!==c[a]&&(n||(n={}),n[a]=c[a])}else n||(i||(i=[]),i.push(u,n)),n=c;else"dangerouslySetInnerHTML"===u?(c=c?c.__html:void 0,s=s?s.__html:void 0,null!=c&&s!==c&&(i=i||[]).push(u,c)):"children"===u?"string"!=typeof c&&"number"!=typeof c||(i=i||[]).push(u,""+c):"suppressContentEditableWarning"!==u&&"suppressHydrationWarning"!==u&&(l.hasOwnProperty(u)?(null!=c&&"onScroll"===u&&zr("scroll",e),i||s===c||(i=[])):(i=i||[]).push(u,c))}n&&(i=i||[]).push("style",n);var u=i;(t.updateQueue=u)&&(t.flags|=4)}},Dl=function(e,t,n,r){n!==r&&(t.flags|=4)};var Kl=!1,Yl=!1,Xl="function"==typeof WeakSet?WeakSet:Set,Jl=null;function es(e,t){var n=e.ref;if(null!==n)if("function"==typeof n)try{n(null)}catch(r){_c(e,t,r)}else n.current=null}function ts(e,t,n){try{n()}catch(r){_c(e,t,r)}}var ns=!1;function rs(e,t,n){var r=t.updateQueue;if(null!==(r=null!==r?r.lastEffect:null)){var o=r=r.next;do{if((o.tag&e)===e){var a=o.destroy;o.destroy=void 0,void 0!==a&&ts(t,n,a)}o=o.next}while(o!==r)}}function os(e,t){if(null!==(t=null!==(t=t.updateQueue)?t.lastEffect:null)){var n=t=t.next;do{if((n.tag&e)===e){var r=n.create;n.destroy=r()}n=n.next}while(n!==t)}}function as(e){var t=e.ref;if(null!==t){var n=e.stateNode;e.tag,e=n,"function"==typeof t?t(e):t.current=e}}function is(e){var t=e.alternate;null!==t&&(e.alternate=null,is(t)),e.child=null,e.deletions=null,e.sibling=null,5===e.tag&&(null!==(t=e.stateNode)&&(delete t[fo],delete t[mo],delete t[ho],delete t[bo],delete t[yo])),e.stateNode=null,e.return=null,e.dependencies=null,e.memoizedProps=null,e.memoizedState=null,e.pendingProps=null,e.stateNode=null,e.updateQueue=null}function ls(e){return 5===e.tag||3===e.tag||4===e.tag}function ss(e){e:for(;;){for(;null===e.sibling;){if(null===e.return||ls(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;5!==e.tag&&6!==e.tag&&18!==e.tag;){if(2&e.flags)continue e;if(null===e.child||4===e.tag)continue e;e.child.return=e,e=e.child}if(!(2&e.flags))return e.stateNode}}function cs(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?8===n.nodeType?n.parentNode.insertBefore(e,t):n.insertBefore(e,t):(8===n.nodeType?(t=n.parentNode).insertBefore(e,n):(t=n).appendChild(e),null!=(n=n._reactRootContainer)||null!==t.onclick||(t.onclick=Jr));else if(4!==r&&null!==(e=e.child))for(cs(e,t,n),e=e.sibling;null!==e;)cs(e,t,n),e=e.sibling}function us(e,t,n){var r=e.tag;if(5===r||6===r)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(4!==r&&null!==(e=e.child))for(us(e,t,n),e=e.sibling;null!==e;)us(e,t,n),e=e.sibling}var ds=null,ps=!1;function fs(e,t,n){for(n=n.child;null!==n;)ms(e,t,n),n=n.sibling}function ms(e,t,n){if(at&&"function"==typeof at.onCommitFiberUnmount)try{at.onCommitFiberUnmount(ot,n)}catch(l){}switch(n.tag){case 5:Yl||es(n,t);case 6:var r=ds,o=ps;ds=null,fs(e,t,n),ps=o,null!==(ds=r)&&(ps?(e=ds,n=n.stateNode,8===e.nodeType?e.parentNode.removeChild(n):e.removeChild(n)):ds.removeChild(n.stateNode));break;case 18:null!==ds&&(ps?(e=ds,n=n.stateNode,8===e.nodeType?so(e.parentNode,n):1===e.nodeType&&so(e,n),Ut(e)):so(ds,n.stateNode));break;case 4:r=ds,o=ps,ds=n.stateNode.containerInfo,ps=!0,fs(e,t,n),ds=r,ps=o;break;case 0:case 11:case 14:case 15:if(!Yl&&(null!==(r=n.updateQueue)&&null!==(r=r.lastEffect))){o=r=r.next;do{var a=o,i=a.destroy;a=a.tag,void 0!==i&&(0!=(2&a)||0!=(4&a))&&ts(n,t,i),o=o.next}while(o!==r)}fs(e,t,n);break;case 1:if(!Yl&&(es(n,t),"function"==typeof(r=n.stateNode).componentWillUnmount))try{r.props=n.memoizedProps,r.state=n.memoizedState,r.componentWillUnmount()}catch(l){_c(n,t,l)}fs(e,t,n);break;case 21:fs(e,t,n);break;case 22:1&n.mode?(Yl=(r=Yl)||null!==n.memoizedState,fs(e,t,n),Yl=r):fs(e,t,n);break;default:fs(e,t,n)}}function gs(e){var t=e.updateQueue;if(null!==t){e.updateQueue=null;var n=e.stateNode;null===n&&(n=e.stateNode=new Xl),t.forEach((function(t){var r=Ac.bind(null,e,t);n.has(t)||(n.add(t),t.then(r,r))}))}}function hs(e,t){var n=t.deletions;if(null!==n)for(var r=0;r<n.length;r++){var o=n[r];try{var i=e,l=t,s=l;e:for(;null!==s;){switch(s.tag){case 5:ds=s.stateNode,ps=!1;break e;case 3:case 4:ds=s.stateNode.containerInfo,ps=!0;break e}s=s.return}if(null===ds)throw Error(a(160));ms(i,l,o),ds=null,ps=!1;var c=o.alternate;null!==c&&(c.return=null),o.return=null}catch(u){_c(o,t,u)}}if(12854&t.subtreeFlags)for(t=t.child;null!==t;)bs(t,e),t=t.sibling}function bs(e,t){var n=e.alternate,r=e.flags;switch(e.tag){case 0:case 11:case 14:case 15:if(hs(t,e),ys(e),4&r){try{rs(3,e,e.return),os(3,e)}catch(h){_c(e,e.return,h)}try{rs(5,e,e.return)}catch(h){_c(e,e.return,h)}}break;case 1:hs(t,e),ys(e),512&r&&null!==n&&es(n,n.return);break;case 5:if(hs(t,e),ys(e),512&r&&null!==n&&es(n,n.return),32&e.flags){var o=e.stateNode;try{pe(o,"")}catch(h){_c(e,e.return,h)}}if(4&r&&null!=(o=e.stateNode)){var i=e.memoizedProps,l=null!==n?n.memoizedProps:i,s=e.type,c=e.updateQueue;if(e.updateQueue=null,null!==c)try{"input"===s&&"radio"===i.type&&null!=i.name&&Y(o,i),ve(s,l);var u=ve(s,i);for(l=0;l<c.length;l+=2){var d=c[l],p=c[l+1];"style"===d?he(o,p):"dangerouslySetInnerHTML"===d?de(o,p):"children"===d?pe(o,p):v(o,d,p,u)}switch(s){case"input":X(o,i);break;case"textarea":ae(o,i);break;case"select":var f=o._wrapperState.wasMultiple;o._wrapperState.wasMultiple=!!i.multiple;var m=i.value;null!=m?ne(o,!!i.multiple,m,!1):f!==!!i.multiple&&(null!=i.defaultValue?ne(o,!!i.multiple,i.defaultValue,!0):ne(o,!!i.multiple,i.multiple?[]:"",!1))}o[mo]=i}catch(h){_c(e,e.return,h)}}break;case 6:if(hs(t,e),ys(e),4&r){if(null===e.stateNode)throw Error(a(162));o=e.stateNode,i=e.memoizedProps;try{o.nodeValue=i}catch(h){_c(e,e.return,h)}}break;case 3:if(hs(t,e),ys(e),4&r&&null!==n&&n.memoizedState.isDehydrated)try{Ut(t.containerInfo)}catch(h){_c(e,e.return,h)}break;case 4:default:hs(t,e),ys(e);break;case 13:hs(t,e),ys(e),8192&(o=e.child).flags&&(i=null!==o.memoizedState,o.stateNode.isHidden=i,!i||null!==o.alternate&&null!==o.alternate.memoizedState||(Us=Ye())),4&r&&gs(e);break;case 22:if(d=null!==n&&null!==n.memoizedState,1&e.mode?(Yl=(u=Yl)||d,hs(t,e),Yl=u):hs(t,e),ys(e),8192&r){if(u=null!==e.memoizedState,(e.stateNode.isHidden=u)&&!d&&0!=(1&e.mode))for(Jl=e,d=e.child;null!==d;){for(p=Jl=d;null!==Jl;){switch(m=(f=Jl).child,f.tag){case 0:case 11:case 14:case 15:rs(4,f,f.return);break;case 1:es(f,f.return);var g=f.stateNode;if("function"==typeof g.componentWillUnmount){r=f,n=f.return;try{t=r,g.props=t.memoizedProps,g.state=t.memoizedState,g.componentWillUnmount()}catch(h){_c(r,n,h)}}break;case 5:es(f,f.return);break;case 22:if(null!==f.memoizedState){xs(p);continue}}null!==m?(m.return=f,Jl=m):xs(p)}d=d.sibling}e:for(d=null,p=e;;){if(5===p.tag){if(null===d){d=p;try{o=p.stateNode,u?"function"==typeof(i=o.style).setProperty?i.setProperty("display","none","important"):i.display="none":(s=p.stateNode,l=null!=(c=p.memoizedProps.style)&&c.hasOwnProperty("display")?c.display:null,s.style.display=ge("display",l))}catch(h){_c(e,e.return,h)}}}else if(6===p.tag){if(null===d)try{p.stateNode.nodeValue=u?"":p.memoizedProps}catch(h){_c(e,e.return,h)}}else if((22!==p.tag&&23!==p.tag||null===p.memoizedState||p===e)&&null!==p.child){p.child.return=p,p=p.child;continue}if(p===e)break e;for(;null===p.sibling;){if(null===p.return||p.return===e)break e;d===p&&(d=null),p=p.return}d===p&&(d=null),p.sibling.return=p.return,p=p.sibling}}break;case 19:hs(t,e),ys(e),4&r&&gs(e);case 21:}}function ys(e){var t=e.flags;if(2&t){try{e:{for(var n=e.return;null!==n;){if(ls(n)){var r=n;break e}n=n.return}throw Error(a(160))}switch(r.tag){case 5:var o=r.stateNode;32&r.flags&&(pe(o,""),r.flags&=-33),us(e,ss(e),o);break;case 3:case 4:var i=r.stateNode.containerInfo;cs(e,ss(e),i);break;default:throw Error(a(161))}}catch(l){_c(e,e.return,l)}e.flags&=-3}4096&t&&(e.flags&=-4097)}function vs(e,t,n){Jl=e,ws(e,t,n)}function ws(e,t,n){for(var r=0!=(1&e.mode);null!==Jl;){var o=Jl,a=o.child;if(22===o.tag&&r){var i=null!==o.memoizedState||Kl;if(!i){var l=o.alternate,s=null!==l&&null!==l.memoizedState||Yl;l=Kl;var c=Yl;if(Kl=i,(Yl=s)&&!c)for(Jl=o;null!==Jl;)s=(i=Jl).child,22===i.tag&&null!==i.memoizedState?Ss(o):null!==s?(s.return=i,Jl=s):Ss(o);for(;null!==a;)Jl=a,ws(a,t,n),a=a.sibling;Jl=o,Kl=l,Yl=c}ks(e)}else 0!=(8772&o.subtreeFlags)&&null!==a?(a.return=o,Jl=a):ks(e)}}function ks(e){for(;null!==Jl;){var t=Jl;if(0!=(8772&t.flags)){var n=t.alternate;try{if(0!=(8772&t.flags))switch(t.tag){case 0:case 11:case 15:Yl||os(5,t);break;case 1:var r=t.stateNode;if(4&t.flags&&!Yl)if(null===n)r.componentDidMount();else{var o=t.elementType===t.type?n.memoizedProps:ba(t.type,n.memoizedProps);r.componentDidUpdate(o,n.memoizedState,r.__reactInternalSnapshotBeforeUpdate)}var i=t.updateQueue;null!==i&&za(t,i,r);break;case 3:var l=t.updateQueue;if(null!==l){if(n=null,null!==t.child)switch(t.child.tag){case 5:case 1:n=t.child.stateNode}za(t,l,n)}break;case 5:var s=t.stateNode;if(null===n&&4&t.flags){n=s;var c=t.memoizedProps;switch(t.type){case"button":case"input":case"select":case"textarea":c.autoFocus&&n.focus();break;case"img":c.src&&(n.src=c.src)}}break;case 6:case 4:case 12:case 19:case 17:case 21:case 22:case 23:case 25:break;case 13:if(null===t.memoizedState){var u=t.alternate;if(null!==u){var d=u.memoizedState;if(null!==d){var p=d.dehydrated;null!==p&&Ut(p)}}}break;default:throw Error(a(163))}Yl||512&t.flags&&as(t)}catch(f){_c(t,t.return,f)}}if(t===e){Jl=null;break}if(null!==(n=t.sibling)){n.return=t.return,Jl=n;break}Jl=t.return}}function xs(e){for(;null!==Jl;){var t=Jl;if(t===e){Jl=null;break}var n=t.sibling;if(null!==n){n.return=t.return,Jl=n;break}Jl=t.return}}function Ss(e){for(;null!==Jl;){var t=Jl;try{switch(t.tag){case 0:case 11:case 15:var n=t.return;try{os(4,t)}catch(s){_c(t,n,s)}break;case 1:var r=t.stateNode;if("function"==typeof r.componentDidMount){var o=t.return;try{r.componentDidMount()}catch(s){_c(t,o,s)}}var a=t.return;try{as(t)}catch(s){_c(t,a,s)}break;case 5:var i=t.return;try{as(t)}catch(s){_c(t,i,s)}}}catch(s){_c(t,t.return,s)}if(t===e){Jl=null;break}var l=t.sibling;if(null!==l){l.return=t.return,Jl=l;break}Jl=t.return}}var Es,_s=Math.ceil,Cs=w.ReactCurrentDispatcher,Ts=w.ReactCurrentOwner,js=w.ReactCurrentBatchConfig,As=0,Ls=null,Ns=null,Rs=0,Ps=0,Os=_o(0),Ds=0,Is=null,Fs=0,Ms=0,zs=0,Bs=null,$s=null,Us=0,qs=1/0,Hs=null,Zs=!1,Gs=null,Vs=null,Ws=!1,Qs=null,Ks=0,Ys=0,Xs=null,Js=-1,ec=0;function tc(){return 0!=(6&As)?Ye():-1!==Js?Js:Js=Ye()}function nc(e){return 0==(1&e.mode)?1:0!=(2&As)&&0!==Rs?Rs&-Rs:null!==ha.transition?(0===ec&&(ec=gt()),ec):0!==(e=vt)?e:e=void 0===(e=window.event)?16:Kt(e.type)}function rc(e,t,n,r){if(50<Ys)throw Ys=0,Xs=null,Error(a(185));bt(e,n,r),0!=(2&As)&&e===Ls||(e===Ls&&(0==(2&As)&&(Ms|=n),4===Ds&&sc(e,Rs)),oc(e,r),1===n&&0===As&&0==(1&t.mode)&&(qs=Ye()+500,Bo&&qo()))}function oc(e,t){var n=e.callbackNode;!function(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,o=e.expirationTimes,a=e.pendingLanes;0<a;){var i=31-it(a),l=1<<i,s=o[i];-1===s?0!=(l&n)&&0==(l&r)||(o[i]=ft(l,t)):s<=t&&(e.expiredLanes|=l),a&=~l}}(e,t);var r=pt(e,e===Ls?Rs:0);if(0===r)null!==n&&We(n),e.callbackNode=null,e.callbackPriority=0;else if(t=r&-r,e.callbackPriority!==t){if(null!=n&&We(n),1===t)0===e.tag?function(e){Bo=!0,Uo(e)}(cc.bind(null,e)):Uo(cc.bind(null,e)),io((function(){0==(6&As)&&qo()})),n=null;else{switch(wt(r)){case 1:n=Je;break;case 4:n=et;break;case 16:default:n=tt;break;case 536870912:n=rt}n=Lc(n,ac.bind(null,e))}e.callbackPriority=t,e.callbackNode=n}}function ac(e,t){if(Js=-1,ec=0,0!=(6&As))throw Error(a(327));var n=e.callbackNode;if(Sc()&&e.callbackNode!==n)return null;var r=pt(e,e===Ls?Rs:0);if(0===r)return null;if(0!=(30&r)||0!=(r&e.expiredLanes)||t)t=bc(e,r);else{t=r;var o=As;As|=2;var i=gc();for(Ls===e&&Rs===t||(Hs=null,qs=Ye()+500,fc(e,t));;)try{vc();break}catch(s){mc(e,s)}xa(),Cs.current=i,As=o,null!==Ns?t=0:(Ls=null,Rs=0,t=Ds)}if(0!==t){if(2===t&&(0!==(o=mt(e))&&(r=o,t=ic(e,o))),1===t)throw n=Is,fc(e,0),sc(e,r),oc(e,Ye()),n;if(6===t)sc(e,r);else{if(o=e.current.alternate,0==(30&r)&&!function(e){for(var t=e;;){if(16384&t.flags){var n=t.updateQueue;if(null!==n&&null!==(n=n.stores))for(var r=0;r<n.length;r++){var o=n[r],a=o.getSnapshot;o=o.value;try{if(!lr(a(),o))return!1}catch(l){return!1}}}if(n=t.child,16384&t.subtreeFlags&&null!==n)n.return=t,t=n;else{if(t===e)break;for(;null===t.sibling;){if(null===t.return||t.return===e)return!0;t=t.return}t.sibling.return=t.return,t=t.sibling}}return!0}(o)&&(2===(t=bc(e,r))&&(0!==(i=mt(e))&&(r=i,t=ic(e,i))),1===t))throw n=Is,fc(e,0),sc(e,r),oc(e,Ye()),n;switch(e.finishedWork=o,e.finishedLanes=r,t){case 0:case 1:throw Error(a(345));case 2:case 5:xc(e,$s,Hs);break;case 3:if(sc(e,r),(130023424&r)===r&&10<(t=Us+500-Ye())){if(0!==pt(e,0))break;if(((o=e.suspendedLanes)&r)!==r){tc(),e.pingedLanes|=e.suspendedLanes&o;break}e.timeoutHandle=ro(xc.bind(null,e,$s,Hs),t);break}xc(e,$s,Hs);break;case 4:if(sc(e,r),(4194240&r)===r)break;for(t=e.eventTimes,o=-1;0<r;){var l=31-it(r);i=1<<l,(l=t[l])>o&&(o=l),r&=~i}if(r=o,10<(r=(120>(r=Ye()-r)?120:480>r?480:1080>r?1080:1920>r?1920:3e3>r?3e3:4320>r?4320:1960*_s(r/1960))-r)){e.timeoutHandle=ro(xc.bind(null,e,$s,Hs),r);break}xc(e,$s,Hs);break;default:throw Error(a(329))}}}return oc(e,Ye()),e.callbackNode===n?ac.bind(null,e):null}function ic(e,t){var n=Bs;return e.current.memoizedState.isDehydrated&&(fc(e,t).flags|=256),2!==(e=bc(e,t))&&(t=$s,$s=n,null!==t&&lc(t)),e}function lc(e){null===$s?$s=e:$s.push.apply($s,e)}function sc(e,t){for(t&=~zs,t&=~Ms,e.suspendedLanes|=t,e.pingedLanes&=~t,e=e.expirationTimes;0<t;){var n=31-it(t),r=1<<n;e[n]=-1,t&=~r}}function cc(e){if(0!=(6&As))throw Error(a(327));Sc();var t=pt(e,0);if(0==(1&t))return oc(e,Ye()),null;var n=bc(e,t);if(0!==e.tag&&2===n){var r=mt(e);0!==r&&(t=r,n=ic(e,r))}if(1===n)throw n=Is,fc(e,0),sc(e,t),oc(e,Ye()),n;if(6===n)throw Error(a(345));return e.finishedWork=e.current.alternate,e.finishedLanes=t,xc(e,$s,Hs),oc(e,Ye()),null}function uc(e,t){var n=As;As|=1;try{return e(t)}finally{0===(As=n)&&(qs=Ye()+500,Bo&&qo())}}function dc(e){null!==Qs&&0===Qs.tag&&0==(6&As)&&Sc();var t=As;As|=1;var n=js.transition,r=vt;try{if(js.transition=null,vt=1,e)return e()}finally{vt=r,js.transition=n,0==(6&(As=t))&&qo()}}function pc(){Ps=Os.current,Co(Os)}function fc(e,t){e.finishedWork=null,e.finishedLanes=0;var n=e.timeoutHandle;if(-1!==n&&(e.timeoutHandle=-1,oo(n)),null!==Ns)for(n=Ns.return;null!==n;){var r=n;switch(na(r),r.tag){case 1:null!=(r=r.type.childContextTypes)&&Oo();break;case 3:ai(),Co(Lo),Co(Ao),di();break;case 5:li(r);break;case 4:ai();break;case 13:case 19:Co(si);break;case 10:Sa(r.type._context);break;case 22:case 23:pc()}n=n.return}if(Ls=e,Ns=e=Oc(e.current,null),Rs=Ps=t,Ds=0,Is=null,zs=Ms=Fs=0,$s=Bs=null,null!==Ta){for(t=0;t<Ta.length;t++)if(null!==(r=(n=Ta[t]).interleaved)){n.interleaved=null;var o=r.next,a=n.pending;if(null!==a){var i=a.next;a.next=o,r.next=i}n.pending=r}Ta=null}return e}function mc(e,t){for(;;){var n=Ns;try{if(xa(),pi.current=il,yi){for(var r=gi.memoizedState;null!==r;){var o=r.queue;null!==o&&(o.pending=null),r=r.next}yi=!1}if(mi=0,bi=hi=gi=null,vi=!1,wi=0,Ts.current=null,null===n||null===n.return){Ds=1,Is=t,Ns=null;break}e:{var i=e,l=n.return,s=n,c=t;if(t=Rs,s.flags|=32768,null!==c&&"object"==typeof c&&"function"==typeof c.then){var u=c,d=s,p=d.tag;if(0==(1&d.mode)&&(0===p||11===p||15===p)){var f=d.alternate;f?(d.updateQueue=f.updateQueue,d.memoizedState=f.memoizedState,d.lanes=f.lanes):(d.updateQueue=null,d.memoizedState=null)}var m=bl(l);if(null!==m){m.flags&=-257,yl(m,l,s,0,t),1&m.mode&&hl(i,u,t),c=u;var g=(t=m).updateQueue;if(null===g){var h=new Set;h.add(c),t.updateQueue=h}else g.add(c);break e}if(0==(1&t)){hl(i,u,t),hc();break e}c=Error(a(426))}else if(aa&&1&s.mode){var b=bl(l);if(null!==b){0==(65536&b.flags)&&(b.flags|=256),yl(b,l,s,0,t),ga(ul(c,s));break e}}i=c=ul(c,s),4!==Ds&&(Ds=2),null===Bs?Bs=[i]:Bs.push(i),i=l;do{switch(i.tag){case 3:i.flags|=65536,t&=-t,i.lanes|=t,Fa(i,ml(0,c,t));break e;case 1:s=c;var y=i.type,v=i.stateNode;if(0==(128&i.flags)&&("function"==typeof y.getDerivedStateFromError||null!==v&&"function"==typeof v.componentDidCatch&&(null===Vs||!Vs.has(v)))){i.flags|=65536,t&=-t,i.lanes|=t,Fa(i,gl(i,s,t));break e}}i=i.return}while(null!==i)}kc(n)}catch(w){t=w,Ns===n&&null!==n&&(Ns=n=n.return);continue}break}}function gc(){var e=Cs.current;return Cs.current=il,null===e?il:e}function hc(){0!==Ds&&3!==Ds&&2!==Ds||(Ds=4),null===Ls||0==(268435455&Fs)&&0==(268435455&Ms)||sc(Ls,Rs)}function bc(e,t){var n=As;As|=2;var r=gc();for(Ls===e&&Rs===t||(Hs=null,fc(e,t));;)try{yc();break}catch(o){mc(e,o)}if(xa(),As=n,Cs.current=r,null!==Ns)throw Error(a(261));return Ls=null,Rs=0,Ds}function yc(){for(;null!==Ns;)wc(Ns)}function vc(){for(;null!==Ns&&!Qe();)wc(Ns)}function wc(e){var t=Es(e.alternate,e,Ps);e.memoizedProps=e.pendingProps,null===t?kc(e):Ns=t,Ts.current=null}function kc(e){var t=e;do{var n=t.alternate;if(e=t.return,0==(32768&t.flags)){if(null!==(n=Wl(n,t,Ps)))return void(Ns=n)}else{if(null!==(n=Ql(n,t)))return n.flags&=32767,void(Ns=n);if(null===e)return Ds=6,void(Ns=null);e.flags|=32768,e.subtreeFlags=0,e.deletions=null}if(null!==(t=t.sibling))return void(Ns=t);Ns=t=e}while(null!==t);0===Ds&&(Ds=5)}function xc(e,t,n){var r=vt,o=js.transition;try{js.transition=null,vt=1,function(e,t,n,r){do{Sc()}while(null!==Qs);if(0!=(6&As))throw Error(a(327));n=e.finishedWork;var o=e.finishedLanes;if(null===n)return null;if(e.finishedWork=null,e.finishedLanes=0,n===e.current)throw Error(a(177));e.callbackNode=null,e.callbackPriority=0;var i=n.lanes|n.childLanes;if(function(e,t){var n=e.pendingLanes&~t;e.pendingLanes=t,e.suspendedLanes=0,e.pingedLanes=0,e.expiredLanes&=t,e.mutableReadLanes&=t,e.entangledLanes&=t,t=e.entanglements;var r=e.eventTimes;for(e=e.expirationTimes;0<n;){var o=31-it(n),a=1<<o;t[o]=0,r[o]=-1,e[o]=-1,n&=~a}}(e,i),e===Ls&&(Ns=Ls=null,Rs=0),0==(2064&n.subtreeFlags)&&0==(2064&n.flags)||Ws||(Ws=!0,Lc(tt,(function(){return Sc(),null}))),i=0!=(15990&n.flags),0!=(15990&n.subtreeFlags)||i){i=js.transition,js.transition=null;var l=vt;vt=1;var s=As;As|=4,Ts.current=null,function(e,t){if(eo=Ht,fr(e=pr())){if("selectionStart"in e)var n={start:e.selectionStart,end:e.selectionEnd};else e:{var r=(n=(n=e.ownerDocument)&&n.defaultView||window).getSelection&&n.getSelection();if(r&&0!==r.rangeCount){n=r.anchorNode;var o=r.anchorOffset,i=r.focusNode;r=r.focusOffset;try{n.nodeType,i.nodeType}catch(k){n=null;break e}var l=0,s=-1,c=-1,u=0,d=0,p=e,f=null;t:for(;;){for(var m;p!==n||0!==o&&3!==p.nodeType||(s=l+o),p!==i||0!==r&&3!==p.nodeType||(c=l+r),3===p.nodeType&&(l+=p.nodeValue.length),null!==(m=p.firstChild);)f=p,p=m;for(;;){if(p===e)break t;if(f===n&&++u===o&&(s=l),f===i&&++d===r&&(c=l),null!==(m=p.nextSibling))break;f=(p=f).parentNode}p=m}n=-1===s||-1===c?null:{start:s,end:c}}else n=null}n=n||{start:0,end:0}}else n=null;for(to={focusedElem:e,selectionRange:n},Ht=!1,Jl=t;null!==Jl;)if(e=(t=Jl).child,0!=(1028&t.subtreeFlags)&&null!==e)e.return=t,Jl=e;else for(;null!==Jl;){t=Jl;try{var g=t.alternate;if(0!=(1024&t.flags))switch(t.tag){case 0:case 11:case 15:case 5:case 6:case 4:case 17:break;case 1:if(null!==g){var h=g.memoizedProps,b=g.memoizedState,y=t.stateNode,v=y.getSnapshotBeforeUpdate(t.elementType===t.type?h:ba(t.type,h),b);y.__reactInternalSnapshotBeforeUpdate=v}break;case 3:var w=t.stateNode.containerInfo;1===w.nodeType?w.textContent="":9===w.nodeType&&w.documentElement&&w.removeChild(w.documentElement);break;default:throw Error(a(163))}}catch(k){_c(t,t.return,k)}if(null!==(e=t.sibling)){e.return=t.return,Jl=e;break}Jl=t.return}g=ns,ns=!1}(e,n),bs(n,e),mr(to),Ht=!!eo,to=eo=null,e.current=n,vs(n,e,o),Ke(),As=s,vt=l,js.transition=i}else e.current=n;if(Ws&&(Ws=!1,Qs=e,Ks=o),i=e.pendingLanes,0===i&&(Vs=null),function(e){if(at&&"function"==typeof at.onCommitFiberRoot)try{at.onCommitFiberRoot(ot,e,void 0,128==(128&e.current.flags))}catch(t){}}(n.stateNode),oc(e,Ye()),null!==t)for(r=e.onRecoverableError,n=0;n<t.length;n++)o=t[n],r(o.value,{componentStack:o.stack,digest:o.digest});if(Zs)throw Zs=!1,e=Gs,Gs=null,e;0!=(1&Ks)&&0!==e.tag&&Sc(),i=e.pendingLanes,0!=(1&i)?e===Xs?Ys++:(Ys=0,Xs=e):Ys=0,qo()}(e,t,n,r)}finally{js.transition=o,vt=r}return null}function Sc(){if(null!==Qs){var e=wt(Ks),t=js.transition,n=vt;try{if(js.transition=null,vt=16>e?16:e,null===Qs)var r=!1;else{if(e=Qs,Qs=null,Ks=0,0!=(6&As))throw Error(a(331));var o=As;for(As|=4,Jl=e.current;null!==Jl;){var i=Jl,l=i.child;if(0!=(16&Jl.flags)){var s=i.deletions;if(null!==s){for(var c=0;c<s.length;c++){var u=s[c];for(Jl=u;null!==Jl;){var d=Jl;switch(d.tag){case 0:case 11:case 15:rs(8,d,i)}var p=d.child;if(null!==p)p.return=d,Jl=p;else for(;null!==Jl;){var f=(d=Jl).sibling,m=d.return;if(is(d),d===u){Jl=null;break}if(null!==f){f.return=m,Jl=f;break}Jl=m}}}var g=i.alternate;if(null!==g){var h=g.child;if(null!==h){g.child=null;do{var b=h.sibling;h.sibling=null,h=b}while(null!==h)}}Jl=i}}if(0!=(2064&i.subtreeFlags)&&null!==l)l.return=i,Jl=l;else e:for(;null!==Jl;){if(0!=(2048&(i=Jl).flags))switch(i.tag){case 0:case 11:case 15:rs(9,i,i.return)}var y=i.sibling;if(null!==y){y.return=i.return,Jl=y;break e}Jl=i.return}}var v=e.current;for(Jl=v;null!==Jl;){var w=(l=Jl).child;if(0!=(2064&l.subtreeFlags)&&null!==w)w.return=l,Jl=w;else e:for(l=v;null!==Jl;){if(0!=(2048&(s=Jl).flags))try{switch(s.tag){case 0:case 11:case 15:os(9,s)}}catch(x){_c(s,s.return,x)}if(s===l){Jl=null;break e}var k=s.sibling;if(null!==k){k.return=s.return,Jl=k;break e}Jl=s.return}}if(As=o,qo(),at&&"function"==typeof at.onPostCommitFiberRoot)try{at.onPostCommitFiberRoot(ot,e)}catch(x){}r=!0}return r}finally{vt=n,js.transition=t}}return!1}function Ec(e,t,n){e=Da(e,t=ml(0,t=ul(n,t),1),1),t=tc(),null!==e&&(bt(e,1,t),oc(e,t))}function _c(e,t,n){if(3===e.tag)Ec(e,e,n);else for(;null!==t;){if(3===t.tag){Ec(t,e,n);break}if(1===t.tag){var r=t.stateNode;if("function"==typeof t.type.getDerivedStateFromError||"function"==typeof r.componentDidCatch&&(null===Vs||!Vs.has(r))){t=Da(t,e=gl(t,e=ul(n,e),1),1),e=tc(),null!==t&&(bt(t,1,e),oc(t,e));break}}t=t.return}}function Cc(e,t,n){var r=e.pingCache;null!==r&&r.delete(t),t=tc(),e.pingedLanes|=e.suspendedLanes&n,Ls===e&&(Rs&n)===n&&(4===Ds||3===Ds&&(130023424&Rs)===Rs&&500>Ye()-Us?fc(e,0):zs|=n),oc(e,t)}function Tc(e,t){0===t&&(0==(1&e.mode)?t=1:(t=ut,0==(130023424&(ut<<=1))&&(ut=4194304)));var n=tc();null!==(e=La(e,t))&&(bt(e,t,n),oc(e,n))}function jc(e){var t=e.memoizedState,n=0;null!==t&&(n=t.retryLane),Tc(e,n)}function Ac(e,t){var n=0;switch(e.tag){case 13:var r=e.stateNode,o=e.memoizedState;null!==o&&(n=o.retryLane);break;case 19:r=e.stateNode;break;default:throw Error(a(314))}null!==r&&r.delete(t),Tc(e,n)}function Lc(e,t){return Ve(e,t)}function Nc(e,t,n,r){this.tag=e,this.key=n,this.sibling=this.child=this.return=this.stateNode=this.type=this.elementType=null,this.index=0,this.ref=null,this.pendingProps=t,this.dependencies=this.memoizedState=this.updateQueue=this.memoizedProps=null,this.mode=r,this.subtreeFlags=this.flags=0,this.deletions=null,this.childLanes=this.lanes=0,this.alternate=null}function Rc(e,t,n,r){return new Nc(e,t,n,r)}function Pc(e){return!(!(e=e.prototype)||!e.isReactComponent)}function Oc(e,t){var n=e.alternate;return null===n?((n=Rc(e.tag,t,e.key,e.mode)).elementType=e.elementType,n.type=e.type,n.stateNode=e.stateNode,n.alternate=e,e.alternate=n):(n.pendingProps=t,n.type=e.type,n.flags=0,n.subtreeFlags=0,n.deletions=null),n.flags=14680064&e.flags,n.childLanes=e.childLanes,n.lanes=e.lanes,n.child=e.child,n.memoizedProps=e.memoizedProps,n.memoizedState=e.memoizedState,n.updateQueue=e.updateQueue,t=e.dependencies,n.dependencies=null===t?null:{lanes:t.lanes,firstContext:t.firstContext},n.sibling=e.sibling,n.index=e.index,n.ref=e.ref,n}function Dc(e,t,n,r,o,i){var l=2;if(r=e,"function"==typeof e)Pc(e)&&(l=1);else if("string"==typeof e)l=5;else e:switch(e){case S:return Ic(n.children,o,i,t);case E:l=8,o|=8;break;case _:return(e=Rc(12,n,t,2|o)).elementType=_,e.lanes=i,e;case A:return(e=Rc(13,n,t,o)).elementType=A,e.lanes=i,e;case L:return(e=Rc(19,n,t,o)).elementType=L,e.lanes=i,e;case P:return Fc(n,o,i,t);default:if("object"==typeof e&&null!==e)switch(e.$$typeof){case C:l=10;break e;case T:l=9;break e;case j:l=11;break e;case N:l=14;break e;case R:l=16,r=null;break e}throw Error(a(130,null==e?e:typeof e,""))}return(t=Rc(l,n,t,o)).elementType=e,t.type=r,t.lanes=i,t}function Ic(e,t,n,r){return(e=Rc(7,e,r,t)).lanes=n,e}function Fc(e,t,n,r){return(e=Rc(22,e,r,t)).elementType=P,e.lanes=n,e.stateNode={isHidden:!1},e}function Mc(e,t,n){return(e=Rc(6,e,null,t)).lanes=n,e}function zc(e,t,n){return(t=Rc(4,null!==e.children?e.children:[],e.key,t)).lanes=n,t.stateNode={containerInfo:e.containerInfo,pendingChildren:null,implementation:e.implementation},t}function Bc(e,t,n,r,o){this.tag=t,this.containerInfo=e,this.finishedWork=this.pingCache=this.current=this.pendingChildren=null,this.timeoutHandle=-1,this.callbackNode=this.pendingContext=this.context=null,this.callbackPriority=0,this.eventTimes=ht(0),this.expirationTimes=ht(-1),this.entangledLanes=this.finishedLanes=this.mutableReadLanes=this.expiredLanes=this.pingedLanes=this.suspendedLanes=this.pendingLanes=0,this.entanglements=ht(0),this.identifierPrefix=r,this.onRecoverableError=o,this.mutableSourceEagerHydrationData=null}function $c(e,t,n,r,o,a,i,l,s){return e=new Bc(e,t,n,l,s),1===t?(t=1,!0===a&&(t|=8)):t=0,a=Rc(3,null,null,t),e.current=a,a.stateNode=e,a.memoizedState={element:r,isDehydrated:n,cache:null,transitions:null,pendingSuspenseBoundaries:null},Ra(a),e}function Uc(e){if(!e)return jo;e:{if(Ue(e=e._reactInternals)!==e||1!==e.tag)throw Error(a(170));var t=e;do{switch(t.tag){case 3:t=t.stateNode.context;break e;case 1:if(Po(t.type)){t=t.stateNode.__reactInternalMemoizedMergedChildContext;break e}}t=t.return}while(null!==t);throw Error(a(171))}if(1===e.tag){var n=e.type;if(Po(n))return Io(e,n,t)}return t}function qc(e,t,n,r,o,a,i,l,s){return(e=$c(n,r,!0,e,0,a,0,l,s)).context=Uc(null),n=e.current,(a=Oa(r=tc(),o=nc(n))).callback=null!=t?t:null,Da(n,a,o),e.current.lanes=o,bt(e,o,r),oc(e,r),e}function Hc(e,t,n,r){var o=t.current,a=tc(),i=nc(o);return n=Uc(n),null===t.context?t.context=n:t.pendingContext=n,(t=Oa(a,i)).payload={element:e},null!==(r=void 0===r?null:r)&&(t.callback=r),null!==(e=Da(o,t,i))&&(rc(e,o,i,a),Ia(e,o,i)),i}function Zc(e){return(e=e.current).child?(e.child.tag,e.child.stateNode):null}function Gc(e,t){if(null!==(e=e.memoizedState)&&null!==e.dehydrated){var n=e.retryLane;e.retryLane=0!==n&&n<t?n:t}}function Vc(e,t){Gc(e,t),(e=e.alternate)&&Gc(e,t)}Es=function(e,t,n){if(null!==e)if(e.memoizedProps!==t.pendingProps||Lo.current)wl=!0;else{if(0==(e.lanes&n)&&0==(128&t.flags))return wl=!1,function(e,t,n){switch(t.tag){case 3:Ll(t),ma();break;case 5:ii(t);break;case 1:Po(t.type)&&Fo(t);break;case 4:oi(t,t.stateNode.containerInfo);break;case 10:var r=t.type._context,o=t.memoizedProps.value;To(ya,r._currentValue),r._currentValue=o;break;case 13:if(null!==(r=t.memoizedState))return null!==r.dehydrated?(To(si,1&si.current),t.flags|=128,null):0!=(n&t.child.childLanes)?Ml(e,t,n):(To(si,1&si.current),null!==(e=Zl(e,t,n))?e.sibling:null);To(si,1&si.current);break;case 19:if(r=0!=(n&t.childLanes),0!=(128&e.flags)){if(r)return ql(e,t,n);t.flags|=128}if(null!==(o=t.memoizedState)&&(o.rendering=null,o.tail=null,o.lastEffect=null),To(si,si.current),r)break;return null;case 22:case 23:return t.lanes=0,_l(e,t,n)}return Zl(e,t,n)}(e,t,n);wl=0!=(131072&e.flags)}else wl=!1,aa&&0!=(1048576&t.flags)&&ea(t,Vo,t.index);switch(t.lanes=0,t.tag){case 2:var r=t.type;Hl(e,t),e=t.pendingProps;var o=Ro(t,Ao.current);_a(t,n),o=Ei(null,t,r,e,o,n);var i=_i();return t.flags|=1,"object"==typeof o&&null!==o&&"function"==typeof o.render&&void 0===o.$$typeof?(t.tag=1,t.memoizedState=null,t.updateQueue=null,Po(r)?(i=!0,Fo(t)):i=!1,t.memoizedState=null!==o.state&&void 0!==o.state?o.state:null,Ra(t),o.updater=Ua,t.stateNode=o,o._reactInternals=t,Ga(t,r,e,n),t=Al(null,t,r,!0,i,n)):(t.tag=0,aa&&i&&ta(t),kl(null,t,o,n),t=t.child),t;case 16:r=t.elementType;e:{switch(Hl(e,t),e=t.pendingProps,r=(o=r._init)(r._payload),t.type=r,o=t.tag=function(e){if("function"==typeof e)return Pc(e)?1:0;if(null!=e){if((e=e.$$typeof)===j)return 11;if(e===N)return 14}return 2}(r),e=ba(r,e),o){case 0:t=Tl(null,t,r,e,n);break e;case 1:t=jl(null,t,r,e,n);break e;case 11:t=xl(null,t,r,e,n);break e;case 14:t=Sl(null,t,r,ba(r.type,e),n);break e}throw Error(a(306,r,""))}return t;case 0:return r=t.type,o=t.pendingProps,Tl(e,t,r,o=t.elementType===r?o:ba(r,o),n);case 1:return r=t.type,o=t.pendingProps,jl(e,t,r,o=t.elementType===r?o:ba(r,o),n);case 3:e:{if(Ll(t),null===e)throw Error(a(387));r=t.pendingProps,o=(i=t.memoizedState).element,Pa(e,t),Ma(t,r,null,n);var l=t.memoizedState;if(r=l.element,i.isDehydrated){if(i={element:r,isDehydrated:!1,cache:l.cache,pendingSuspenseBoundaries:l.pendingSuspenseBoundaries,transitions:l.transitions},t.updateQueue.baseState=i,t.memoizedState=i,256&t.flags){t=Nl(e,t,r,n,o=ul(Error(a(423)),t));break e}if(r!==o){t=Nl(e,t,r,n,o=ul(Error(a(424)),t));break e}for(oa=co(t.stateNode.containerInfo.firstChild),ra=t,aa=!0,ia=null,n=Xa(t,null,r,n),t.child=n;n;)n.flags=-3&n.flags|4096,n=n.sibling}else{if(ma(),r===o){t=Zl(e,t,n);break e}kl(e,t,r,n)}t=t.child}return t;case 5:return ii(t),null===e&&ua(t),r=t.type,o=t.pendingProps,i=null!==e?e.memoizedProps:null,l=o.children,no(r,o)?l=null:null!==i&&no(r,i)&&(t.flags|=32),Cl(e,t),kl(e,t,l,n),t.child;case 6:return null===e&&ua(t),null;case 13:return Ml(e,t,n);case 4:return oi(t,t.stateNode.containerInfo),r=t.pendingProps,null===e?t.child=Ya(t,null,r,n):kl(e,t,r,n),t.child;case 11:return r=t.type,o=t.pendingProps,xl(e,t,r,o=t.elementType===r?o:ba(r,o),n);case 7:return kl(e,t,t.pendingProps,n),t.child;case 8:case 12:return kl(e,t,t.pendingProps.children,n),t.child;case 10:e:{if(r=t.type._context,o=t.pendingProps,i=t.memoizedProps,l=o.value,To(ya,r._currentValue),r._currentValue=l,null!==i)if(lr(i.value,l)){if(i.children===o.children&&!Lo.current){t=Zl(e,t,n);break e}}else for(null!==(i=t.child)&&(i.return=t);null!==i;){var s=i.dependencies;if(null!==s){l=i.child;for(var c=s.firstContext;null!==c;){if(c.context===r){if(1===i.tag){(c=Oa(-1,n&-n)).tag=2;var u=i.updateQueue;if(null!==u){var d=(u=u.shared).pending;null===d?c.next=c:(c.next=d.next,d.next=c),u.pending=c}}i.lanes|=n,null!==(c=i.alternate)&&(c.lanes|=n),Ea(i.return,n,t),s.lanes|=n;break}c=c.next}}else if(10===i.tag)l=i.type===t.type?null:i.child;else if(18===i.tag){if(null===(l=i.return))throw Error(a(341));l.lanes|=n,null!==(s=l.alternate)&&(s.lanes|=n),Ea(l,n,t),l=i.sibling}else l=i.child;if(null!==l)l.return=i;else for(l=i;null!==l;){if(l===t){l=null;break}if(null!==(i=l.sibling)){i.return=l.return,l=i;break}l=l.return}i=l}kl(e,t,o.children,n),t=t.child}return t;case 9:return o=t.type,r=t.pendingProps.children,_a(t,n),r=r(o=Ca(o)),t.flags|=1,kl(e,t,r,n),t.child;case 14:return o=ba(r=t.type,t.pendingProps),Sl(e,t,r,o=ba(r.type,o),n);case 15:return El(e,t,t.type,t.pendingProps,n);case 17:return r=t.type,o=t.pendingProps,o=t.elementType===r?o:ba(r,o),Hl(e,t),t.tag=1,Po(r)?(e=!0,Fo(t)):e=!1,_a(t,n),Ha(t,r,o),Ga(t,r,o,n),Al(null,t,r,!0,e,n);case 19:return ql(e,t,n);case 22:return _l(e,t,n)}throw Error(a(156,t.tag))};var Wc="function"==typeof reportError?reportError:function(e){console.error(e)};function Qc(e){this._internalRoot=e}function Kc(e){this._internalRoot=e}function Yc(e){return!(!e||1!==e.nodeType&&9!==e.nodeType&&11!==e.nodeType)}function Xc(e){return!(!e||1!==e.nodeType&&9!==e.nodeType&&11!==e.nodeType&&(8!==e.nodeType||" react-mount-point-unstable "!==e.nodeValue))}function Jc(){}function eu(e,t,n,r,o){var a=n._reactRootContainer;if(a){var i=a;if("function"==typeof o){var l=o;o=function(){var e=Zc(i);l.call(e)}}Hc(t,i,e,o)}else i=function(e,t,n,r,o){if(o){if("function"==typeof r){var a=r;r=function(){var e=Zc(i);a.call(e)}}var i=qc(t,r,e,0,null,!1,0,"",Jc);return e._reactRootContainer=i,e[go]=i.current,Ur(8===e.nodeType?e.parentNode:e),dc(),i}for(;o=e.lastChild;)e.removeChild(o);if("function"==typeof r){var l=r;r=function(){var e=Zc(s);l.call(e)}}var s=$c(e,0,!1,null,0,!1,0,"",Jc);return e._reactRootContainer=s,e[go]=s.current,Ur(8===e.nodeType?e.parentNode:e),dc((function(){Hc(t,s,n,r)})),s}(n,t,e,o,r);return Zc(i)}Kc.prototype.render=Qc.prototype.render=function(e){var t=this._internalRoot;if(null===t)throw Error(a(409));Hc(e,t,null,null)},Kc.prototype.unmount=Qc.prototype.unmount=function(){var e=this._internalRoot;if(null!==e){this._internalRoot=null;var t=e.containerInfo;dc((function(){Hc(null,e,null,null)})),t[go]=null}},Kc.prototype.unstable_scheduleHydration=function(e){if(e){var t=Et();e={blockedOn:null,target:e,priority:t};for(var n=0;n<Pt.length&&0!==t&&t<Pt[n].priority;n++);Pt.splice(n,0,e),0===n&&Ft(e)}},kt=function(e){switch(e.tag){case 3:var t=e.stateNode;if(t.current.memoizedState.isDehydrated){var n=dt(t.pendingLanes);0!==n&&(yt(t,1|n),oc(t,Ye()),0==(6&As)&&(qs=Ye()+500,qo()))}break;case 13:dc((function(){var t=La(e,1);if(null!==t){var n=tc();rc(t,e,1,n)}})),Vc(e,1)}},xt=function(e){if(13===e.tag){var t=La(e,134217728);if(null!==t)rc(t,e,134217728,tc());Vc(e,134217728)}},St=function(e){if(13===e.tag){var t=nc(e),n=La(e,t);if(null!==n)rc(n,e,t,tc());Vc(e,t)}},Et=function(){return vt},_t=function(e,t){var n=vt;try{return vt=e,t()}finally{vt=n}},xe=function(e,t,n){switch(t){case"input":if(X(e,n),t=n.name,"radio"===n.type&&null!=t){for(n=e;n.parentNode;)n=n.parentNode;for(n=n.querySelectorAll("input[name="+JSON.stringify(""+t)+'][type="radio"]'),t=0;t<n.length;t++){var r=n[t];if(r!==e&&r.form===e.form){var o=xo(r);if(!o)throw Error(a(90));V(r),X(r,o)}}}break;case"textarea":ae(e,n);break;case"select":null!=(t=n.value)&&ne(e,!!n.multiple,t,!1)}},je=uc,Ae=dc;var tu={usingClientEntryPoint:!1,Events:[wo,ko,xo,Ce,Te,uc]},nu={findFiberByHostInstance:vo,bundleType:0,version:"18.2.0",rendererPackageName:"react-dom"},ru={bundleType:nu.bundleType,version:nu.version,rendererPackageName:nu.rendererPackageName,rendererConfig:nu.rendererConfig,overrideHookState:null,overrideHookStateDeletePath:null,overrideHookStateRenamePath:null,overrideProps:null,overridePropsDeletePath:null,overridePropsRenamePath:null,setErrorHandler:null,setSuspenseHandler:null,scheduleUpdate:null,currentDispatcherRef:w.ReactCurrentDispatcher,findHostInstanceByFiber:function(e){return null===(e=Ze(e))?null:e.stateNode},findFiberByHostInstance:nu.findFiberByHostInstance||function(){return null},findHostInstancesForRefresh:null,scheduleRefresh:null,scheduleRoot:null,setRefreshHandler:null,getCurrentFiber:null,reconcilerVersion:"18.2.0-next-9e3b772b8-20220608"};if("undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__){var ou=__REACT_DEVTOOLS_GLOBAL_HOOK__;if(!ou.isDisabled&&ou.supportsFiber)try{ot=ou.inject(ru),at=ou}catch(ue){}}t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=tu,t.createPortal=function(e,t){var n=2<arguments.length&&void 0!==arguments[2]?arguments[2]:null;if(!Yc(t))throw Error(a(200));return function(e,t,n){var r=3<arguments.length&&void 0!==arguments[3]?arguments[3]:null;return{$$typeof:x,key:null==r?null:""+r,children:e,containerInfo:t,implementation:n}}(e,t,null,n)},t.createRoot=function(e,t){if(!Yc(e))throw Error(a(299));var n=!1,r="",o=Wc;return null!=t&&(!0===t.unstable_strictMode&&(n=!0),void 0!==t.identifierPrefix&&(r=t.identifierPrefix),void 0!==t.onRecoverableError&&(o=t.onRecoverableError)),t=$c(e,1,!1,null,0,n,0,r,o),e[go]=t.current,Ur(8===e.nodeType?e.parentNode:e),new Qc(t)},t.findDOMNode=function(e){if(null==e)return null;if(1===e.nodeType)return e;var t=e._reactInternals;if(void 0===t){if("function"==typeof e.render)throw Error(a(188));throw e=Object.keys(e).join(","),Error(a(268,e))}return e=null===(e=Ze(t))?null:e.stateNode},t.flushSync=function(e){return dc(e)},t.hydrate=function(e,t,n){if(!Xc(t))throw Error(a(200));return eu(null,e,t,!0,n)},t.hydrateRoot=function(e,t,n){if(!Yc(e))throw Error(a(405));var r=null!=n&&n.hydratedSources||null,o=!1,i="",l=Wc;if(null!=n&&(!0===n.unstable_strictMode&&(o=!0),void 0!==n.identifierPrefix&&(i=n.identifierPrefix),void 0!==n.onRecoverableError&&(l=n.onRecoverableError)),t=qc(t,null,e,1,null!=n?n:null,o,0,i,l),e[go]=t.current,Ur(e),r)for(e=0;e<r.length;e++)o=(o=(n=r[e])._getVersion)(n._source),null==t.mutableSourceEagerHydrationData?t.mutableSourceEagerHydrationData=[n,o]:t.mutableSourceEagerHydrationData.push(n,o);return new Kc(t)},t.render=function(e,t,n){if(!Xc(t))throw Error(a(200));return eu(null,e,t,!1,n)},t.unmountComponentAtNode=function(e){if(!Xc(e))throw Error(a(40));return!!e._reactRootContainer&&(dc((function(){eu(null,null,e,!1,(function(){e._reactRootContainer=null,e[go]=null}))})),!0)},t.unstable_batchedUpdates=uc,t.unstable_renderSubtreeIntoContainer=function(e,t,n,r){if(!Xc(n))throw Error(a(200));if(null==e||void 0===e._reactInternals)throw Error(a(38));return eu(e,t,n,!1,r)},t.version="18.2.0-next-9e3b772b8-20220608"},745:(e,t,n)=>{"use strict";var r=n(3935);t.createRoot=r.createRoot,t.hydrateRoot=r.hydrateRoot},3935:(e,t,n)=>{"use strict";!function e(){if("undefined"!=typeof __REACT_DEVTOOLS_GLOBAL_HOOK__&&"function"==typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(e)}catch(t){console.error(t)}}(),e.exports=n(4448)},9590:e=>{var t="undefined"!=typeof Element,n="function"==typeof Map,r="function"==typeof Set,o="function"==typeof ArrayBuffer&&!!ArrayBuffer.isView;function a(e,i){if(e===i)return!0;if(e&&i&&"object"==typeof e&&"object"==typeof i){if(e.constructor!==i.constructor)return!1;var l,s,c,u;if(Array.isArray(e)){if((l=e.length)!=i.length)return!1;for(s=l;0!=s--;)if(!a(e[s],i[s]))return!1;return!0}if(n&&e instanceof Map&&i instanceof Map){if(e.size!==i.size)return!1;for(u=e.entries();!(s=u.next()).done;)if(!i.has(s.value[0]))return!1;for(u=e.entries();!(s=u.next()).done;)if(!a(s.value[1],i.get(s.value[0])))return!1;return!0}if(r&&e instanceof Set&&i instanceof Set){if(e.size!==i.size)return!1;for(u=e.entries();!(s=u.next()).done;)if(!i.has(s.value[0]))return!1;return!0}if(o&&ArrayBuffer.isView(e)&&ArrayBuffer.isView(i)){if((l=e.length)!=i.length)return!1;for(s=l;0!=s--;)if(e[s]!==i[s])return!1;return!0}if(e.constructor===RegExp)return e.source===i.source&&e.flags===i.flags;if(e.valueOf!==Object.prototype.valueOf&&"function"==typeof e.valueOf&&"function"==typeof i.valueOf)return e.valueOf()===i.valueOf();if(e.toString!==Object.prototype.toString&&"function"==typeof e.toString&&"function"==typeof i.toString)return e.toString()===i.toString();if((l=(c=Object.keys(e)).length)!==Object.keys(i).length)return!1;for(s=l;0!=s--;)if(!Object.prototype.hasOwnProperty.call(i,c[s]))return!1;if(t&&e instanceof Element)return!1;for(s=l;0!=s--;)if(("_owner"!==c[s]&&"__v"!==c[s]&&"__o"!==c[s]||!e.$$typeof)&&!a(e[c[s]],i[c[s]]))return!1;return!0}return e!=e&&i!=i}e.exports=function(e,t){try{return a(e,t)}catch(n){if((n.message||"").match(/stack|recursion/i))return console.warn("react-fast-compare cannot handle circular refs"),!1;throw n}}},405:(e,t,n)=>{"use strict";n.d(t,{B6:()=>Z,ql:()=>J});var r=n(7294),o=n(5697),a=n.n(o),i=n(9590),l=n.n(i),s=n(1143),c=n.n(s),u=n(6774),d=n.n(u);function p(){return p=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},p.apply(this,arguments)}function f(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,m(e,t)}function m(e,t){return m=Object.setPrototypeOf||function(e,t){return e.__proto__=t,e},m(e,t)}function g(e,t){if(null==e)return{};var n,r,o={},a=Object.keys(e);for(r=0;r<a.length;r++)t.indexOf(n=a[r])>=0||(o[n]=e[n]);return o}var h={BASE:"base",BODY:"body",HEAD:"head",HTML:"html",LINK:"link",META:"meta",NOSCRIPT:"noscript",SCRIPT:"script",STYLE:"style",TITLE:"title",FRAGMENT:"Symbol(react.fragment)"},b={rel:["amphtml","canonical","alternate"]},y={type:["application/ld+json"]},v={charset:"",name:["robots","description"],property:["og:type","og:title","og:url","og:image","og:image:alt","og:description","twitter:url","twitter:title","twitter:description","twitter:image","twitter:image:alt","twitter:card","twitter:site"]},w=Object.keys(h).map((function(e){return h[e]})),k={accesskey:"accessKey",charset:"charSet",class:"className",contenteditable:"contentEditable",contextmenu:"contextMenu","http-equiv":"httpEquiv",itemprop:"itemProp",tabindex:"tabIndex"},x=Object.keys(k).reduce((function(e,t){return e[k[t]]=t,e}),{}),S=function(e,t){for(var n=e.length-1;n>=0;n-=1){var r=e[n];if(Object.prototype.hasOwnProperty.call(r,t))return r[t]}return null},E=function(e){var t=S(e,h.TITLE),n=S(e,"titleTemplate");if(Array.isArray(t)&&(t=t.join("")),n&&t)return n.replace(/%s/g,(function(){return t}));var r=S(e,"defaultTitle");return t||r||void 0},_=function(e){return S(e,"onChangeClientState")||function(){}},C=function(e,t){return t.filter((function(t){return void 0!==t[e]})).map((function(t){return t[e]})).reduce((function(e,t){return p({},e,t)}),{})},T=function(e,t){return t.filter((function(e){return void 0!==e[h.BASE]})).map((function(e){return e[h.BASE]})).reverse().reduce((function(t,n){if(!t.length)for(var r=Object.keys(n),o=0;o<r.length;o+=1){var a=r[o].toLowerCase();if(-1!==e.indexOf(a)&&n[a])return t.concat(n)}return t}),[])},j=function(e,t,n){var r={};return n.filter((function(t){return!!Array.isArray(t[e])||(void 0!==t[e]&&console&&"function"==typeof console.warn&&console.warn("Helmet: "+e+' should be of type "Array". Instead found type "'+typeof t[e]+'"'),!1)})).map((function(t){return t[e]})).reverse().reduce((function(e,n){var o={};n.filter((function(e){for(var n,a=Object.keys(e),i=0;i<a.length;i+=1){var l=a[i],s=l.toLowerCase();-1===t.indexOf(s)||"rel"===n&&"canonical"===e[n].toLowerCase()||"rel"===s&&"stylesheet"===e[s].toLowerCase()||(n=s),-1===t.indexOf(l)||"innerHTML"!==l&&"cssText"!==l&&"itemprop"!==l||(n=l)}if(!n||!e[n])return!1;var c=e[n].toLowerCase();return r[n]||(r[n]={}),o[n]||(o[n]={}),!r[n][c]&&(o[n][c]=!0,!0)})).reverse().forEach((function(t){return e.push(t)}));for(var a=Object.keys(o),i=0;i<a.length;i+=1){var l=a[i],s=p({},r[l],o[l]);r[l]=s}return e}),[]).reverse()},A=function(e,t){if(Array.isArray(e)&&e.length)for(var n=0;n<e.length;n+=1)if(e[n][t])return!0;return!1},L=function(e){return Array.isArray(e)?e.join(""):e},N=function(e,t){return Array.isArray(e)?e.reduce((function(e,n){return function(e,t){for(var n=Object.keys(e),r=0;r<n.length;r+=1)if(t[n[r]]&&t[n[r]].includes(e[n[r]]))return!0;return!1}(n,t)?e.priority.push(n):e.default.push(n),e}),{priority:[],default:[]}):{default:e}},R=function(e,t){var n;return p({},e,((n={})[t]=void 0,n))},P=[h.NOSCRIPT,h.SCRIPT,h.STYLE],O=function(e,t){return void 0===t&&(t=!0),!1===t?String(e):String(e).replace(/&/g,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""").replace(/'/g,"'")},D=function(e){return Object.keys(e).reduce((function(t,n){var r=void 0!==e[n]?n+'="'+e[n]+'"':""+n;return t?t+" "+r:r}),"")},I=function(e,t){return void 0===t&&(t={}),Object.keys(e).reduce((function(t,n){return t[k[n]||n]=e[n],t}),t)},F=function(e,t){return t.map((function(t,n){var o,a=((o={key:n})["data-rh"]=!0,o);return Object.keys(t).forEach((function(e){var n=k[e]||e;"innerHTML"===n||"cssText"===n?a.dangerouslySetInnerHTML={__html:t.innerHTML||t.cssText}:a[n]=t[e]})),r.createElement(e,a)}))},M=function(e,t,n){switch(e){case h.TITLE:return{toComponent:function(){return n=t.titleAttributes,(o={key:e=t.title})["data-rh"]=!0,a=I(n,o),[r.createElement(h.TITLE,a,e)];var e,n,o,a},toString:function(){return function(e,t,n,r){var o=D(n),a=L(t);return o?"<"+e+' data-rh="true" '+o+">"+O(a,r)+"</"+e+">":"<"+e+' data-rh="true">'+O(a,r)+"</"+e+">"}(e,t.title,t.titleAttributes,n)}};case"bodyAttributes":case"htmlAttributes":return{toComponent:function(){return I(t)},toString:function(){return D(t)}};default:return{toComponent:function(){return F(e,t)},toString:function(){return function(e,t,n){return t.reduce((function(t,r){var o=Object.keys(r).filter((function(e){return!("innerHTML"===e||"cssText"===e)})).reduce((function(e,t){var o=void 0===r[t]?t:t+'="'+O(r[t],n)+'"';return e?e+" "+o:o}),""),a=r.innerHTML||r.cssText||"",i=-1===P.indexOf(e);return t+"<"+e+' data-rh="true" '+o+(i?"/>":">"+a+"</"+e+">")}),"")}(e,t,n)}}}},z=function(e){var t=e.baseTag,n=e.bodyAttributes,r=e.encode,o=e.htmlAttributes,a=e.noscriptTags,i=e.styleTags,l=e.title,s=void 0===l?"":l,c=e.titleAttributes,u=e.linkTags,d=e.metaTags,p=e.scriptTags,f={toComponent:function(){},toString:function(){return""}};if(e.prioritizeSeoTags){var m=function(e){var t=e.linkTags,n=e.scriptTags,r=e.encode,o=N(e.metaTags,v),a=N(t,b),i=N(n,y);return{priorityMethods:{toComponent:function(){return[].concat(F(h.META,o.priority),F(h.LINK,a.priority),F(h.SCRIPT,i.priority))},toString:function(){return M(h.META,o.priority,r)+" "+M(h.LINK,a.priority,r)+" "+M(h.SCRIPT,i.priority,r)}},metaTags:o.default,linkTags:a.default,scriptTags:i.default}}(e);f=m.priorityMethods,u=m.linkTags,d=m.metaTags,p=m.scriptTags}return{priority:f,base:M(h.BASE,t,r),bodyAttributes:M("bodyAttributes",n,r),htmlAttributes:M("htmlAttributes",o,r),link:M(h.LINK,u,r),meta:M(h.META,d,r),noscript:M(h.NOSCRIPT,a,r),script:M(h.SCRIPT,p,r),style:M(h.STYLE,i,r),title:M(h.TITLE,{title:s,titleAttributes:c},r)}},B=[],$=function(e,t){var n=this;void 0===t&&(t="undefined"!=typeof document),this.instances=[],this.value={setHelmet:function(e){n.context.helmet=e},helmetInstances:{get:function(){return n.canUseDOM?B:n.instances},add:function(e){(n.canUseDOM?B:n.instances).push(e)},remove:function(e){var t=(n.canUseDOM?B:n.instances).indexOf(e);(n.canUseDOM?B:n.instances).splice(t,1)}}},this.context=e,this.canUseDOM=t,t||(e.helmet=z({baseTag:[],bodyAttributes:{},encodeSpecialCharacters:!0,htmlAttributes:{},linkTags:[],metaTags:[],noscriptTags:[],scriptTags:[],styleTags:[],title:"",titleAttributes:{}}))},U=r.createContext({}),q=a().shape({setHelmet:a().func,helmetInstances:a().shape({get:a().func,add:a().func,remove:a().func})}),H="undefined"!=typeof document,Z=function(e){function t(n){var r;return(r=e.call(this,n)||this).helmetData=new $(r.props.context,t.canUseDOM),r}return f(t,e),t.prototype.render=function(){return r.createElement(U.Provider,{value:this.helmetData.value},this.props.children)},t}(r.Component);Z.canUseDOM=H,Z.propTypes={context:a().shape({helmet:a().shape()}),children:a().node.isRequired},Z.defaultProps={context:{}},Z.displayName="HelmetProvider";var G=function(e,t){var n,r=document.head||document.querySelector(h.HEAD),o=r.querySelectorAll(e+"[data-rh]"),a=[].slice.call(o),i=[];return t&&t.length&&t.forEach((function(t){var r=document.createElement(e);for(var o in t)Object.prototype.hasOwnProperty.call(t,o)&&("innerHTML"===o?r.innerHTML=t.innerHTML:"cssText"===o?r.styleSheet?r.styleSheet.cssText=t.cssText:r.appendChild(document.createTextNode(t.cssText)):r.setAttribute(o,void 0===t[o]?"":t[o]));r.setAttribute("data-rh","true"),a.some((function(e,t){return n=t,r.isEqualNode(e)}))?a.splice(n,1):i.push(r)})),a.forEach((function(e){return e.parentNode.removeChild(e)})),i.forEach((function(e){return r.appendChild(e)})),{oldTags:a,newTags:i}},V=function(e,t){var n=document.getElementsByTagName(e)[0];if(n){for(var r=n.getAttribute("data-rh"),o=r?r.split(","):[],a=[].concat(o),i=Object.keys(t),l=0;l<i.length;l+=1){var s=i[l],c=t[s]||"";n.getAttribute(s)!==c&&n.setAttribute(s,c),-1===o.indexOf(s)&&o.push(s);var u=a.indexOf(s);-1!==u&&a.splice(u,1)}for(var d=a.length-1;d>=0;d-=1)n.removeAttribute(a[d]);o.length===a.length?n.removeAttribute("data-rh"):n.getAttribute("data-rh")!==i.join(",")&&n.setAttribute("data-rh",i.join(","))}},W=function(e,t){var n=e.baseTag,r=e.htmlAttributes,o=e.linkTags,a=e.metaTags,i=e.noscriptTags,l=e.onChangeClientState,s=e.scriptTags,c=e.styleTags,u=e.title,d=e.titleAttributes;V(h.BODY,e.bodyAttributes),V(h.HTML,r),function(e,t){void 0!==e&&document.title!==e&&(document.title=L(e)),V(h.TITLE,t)}(u,d);var p={baseTag:G(h.BASE,n),linkTags:G(h.LINK,o),metaTags:G(h.META,a),noscriptTags:G(h.NOSCRIPT,i),scriptTags:G(h.SCRIPT,s),styleTags:G(h.STYLE,c)},f={},m={};Object.keys(p).forEach((function(e){var t=p[e],n=t.newTags,r=t.oldTags;n.length&&(f[e]=n),r.length&&(m[e]=p[e].oldTags)})),t&&t(),l(e,f,m)},Q=null,K=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),o=0;o<n;o++)r[o]=arguments[o];return(t=e.call.apply(e,[this].concat(r))||this).rendered=!1,t}f(t,e);var n=t.prototype;return n.shouldComponentUpdate=function(e){return!d()(e,this.props)},n.componentDidUpdate=function(){this.emitChange()},n.componentWillUnmount=function(){this.props.context.helmetInstances.remove(this),this.emitChange()},n.emitChange=function(){var e,t,n=this.props.context,r=n.setHelmet,o=null,a=(e=n.helmetInstances.get().map((function(e){var t=p({},e.props);return delete t.context,t})),{baseTag:T(["href"],e),bodyAttributes:C("bodyAttributes",e),defer:S(e,"defer"),encode:S(e,"encodeSpecialCharacters"),htmlAttributes:C("htmlAttributes",e),linkTags:j(h.LINK,["rel","href"],e),metaTags:j(h.META,["name","charset","http-equiv","property","itemprop"],e),noscriptTags:j(h.NOSCRIPT,["innerHTML"],e),onChangeClientState:_(e),scriptTags:j(h.SCRIPT,["src","innerHTML"],e),styleTags:j(h.STYLE,["cssText"],e),title:E(e),titleAttributes:C("titleAttributes",e),prioritizeSeoTags:A(e,"prioritizeSeoTags")});Z.canUseDOM?(t=a,Q&&cancelAnimationFrame(Q),t.defer?Q=requestAnimationFrame((function(){W(t,(function(){Q=null}))})):(W(t),Q=null)):z&&(o=z(a)),r(o)},n.init=function(){this.rendered||(this.rendered=!0,this.props.context.helmetInstances.add(this),this.emitChange())},n.render=function(){return this.init(),null},t}(r.Component);K.propTypes={context:q.isRequired},K.displayName="HelmetDispatcher";var Y=["children"],X=["children"],J=function(e){function t(){return e.apply(this,arguments)||this}f(t,e);var n=t.prototype;return n.shouldComponentUpdate=function(e){return!l()(R(this.props,"helmetData"),R(e,"helmetData"))},n.mapNestedChildrenToProps=function(e,t){if(!t)return null;switch(e.type){case h.SCRIPT:case h.NOSCRIPT:return{innerHTML:t};case h.STYLE:return{cssText:t};default:throw new Error("<"+e.type+" /> elements are self-closing and can not contain children. Refer to our API for more information.")}},n.flattenArrayTypeChildren=function(e){var t,n=e.child,r=e.arrayTypeChildren;return p({},r,((t={})[n.type]=[].concat(r[n.type]||[],[p({},e.newChildProps,this.mapNestedChildrenToProps(n,e.nestedChildren))]),t))},n.mapObjectTypeChildren=function(e){var t,n,r=e.child,o=e.newProps,a=e.newChildProps,i=e.nestedChildren;switch(r.type){case h.TITLE:return p({},o,((t={})[r.type]=i,t.titleAttributes=p({},a),t));case h.BODY:return p({},o,{bodyAttributes:p({},a)});case h.HTML:return p({},o,{htmlAttributes:p({},a)});default:return p({},o,((n={})[r.type]=p({},a),n))}},n.mapArrayTypeChildrenToProps=function(e,t){var n=p({},t);return Object.keys(e).forEach((function(t){var r;n=p({},n,((r={})[t]=e[t],r))})),n},n.warnOnInvalidChildren=function(e,t){return c()(w.some((function(t){return e.type===t})),"function"==typeof e.type?"You may be attempting to nest <Helmet> components within each other, which is not allowed. Refer to our API for more information.":"Only elements types "+w.join(", ")+" are allowed. Helmet does not support rendering <"+e.type+"> elements. Refer to our API for more information."),c()(!t||"string"==typeof t||Array.isArray(t)&&!t.some((function(e){return"string"!=typeof e})),"Helmet expects a string as a child of <"+e.type+">. Did you forget to wrap your children in braces? ( <"+e.type+">{``}</"+e.type+"> ) Refer to our API for more information."),!0},n.mapChildrenToProps=function(e,t){var n=this,o={};return r.Children.forEach(e,(function(e){if(e&&e.props){var r=e.props,a=r.children,i=g(r,Y),l=Object.keys(i).reduce((function(e,t){return e[x[t]||t]=i[t],e}),{}),s=e.type;switch("symbol"==typeof s?s=s.toString():n.warnOnInvalidChildren(e,a),s){case h.FRAGMENT:t=n.mapChildrenToProps(a,t);break;case h.LINK:case h.META:case h.NOSCRIPT:case h.SCRIPT:case h.STYLE:o=n.flattenArrayTypeChildren({child:e,arrayTypeChildren:o,newChildProps:l,nestedChildren:a});break;default:t=n.mapObjectTypeChildren({child:e,newProps:t,newChildProps:l,nestedChildren:a})}}})),this.mapArrayTypeChildrenToProps(o,t)},n.render=function(){var e=this.props,t=e.children,n=g(e,X),o=p({},n),a=n.helmetData;return t&&(o=this.mapChildrenToProps(t,o)),!a||a instanceof $||(a=new $(a.context,a.instances)),a?r.createElement(K,p({},o,{context:a.value,helmetData:void 0})):r.createElement(U.Consumer,null,(function(e){return r.createElement(K,p({},o,{context:e}))}))},t}(r.Component);J.propTypes={base:a().object,bodyAttributes:a().object,children:a().oneOfType([a().arrayOf(a().node),a().node]),defaultTitle:a().string,defer:a().bool,encodeSpecialCharacters:a().bool,htmlAttributes:a().object,link:a().arrayOf(a().object),meta:a().arrayOf(a().object),noscript:a().arrayOf(a().object),onChangeClientState:a().func,script:a().arrayOf(a().object),style:a().arrayOf(a().object),title:a().string,titleAttributes:a().object,titleTemplate:a().string,prioritizeSeoTags:a().bool,helmetData:a().object},J.defaultProps={defer:!0,encodeSpecialCharacters:!0,prioritizeSeoTags:!1},J.displayName="Helmet"},9921:(e,t)=>{"use strict";var n="function"==typeof Symbol&&Symbol.for,r=n?Symbol.for("react.element"):60103,o=n?Symbol.for("react.portal"):60106,a=n?Symbol.for("react.fragment"):60107,i=n?Symbol.for("react.strict_mode"):60108,l=n?Symbol.for("react.profiler"):60114,s=n?Symbol.for("react.provider"):60109,c=n?Symbol.for("react.context"):60110,u=n?Symbol.for("react.async_mode"):60111,d=n?Symbol.for("react.concurrent_mode"):60111,p=n?Symbol.for("react.forward_ref"):60112,f=n?Symbol.for("react.suspense"):60113,m=n?Symbol.for("react.suspense_list"):60120,g=n?Symbol.for("react.memo"):60115,h=n?Symbol.for("react.lazy"):60116,b=n?Symbol.for("react.block"):60121,y=n?Symbol.for("react.fundamental"):60117,v=n?Symbol.for("react.responder"):60118,w=n?Symbol.for("react.scope"):60119;function k(e){if("object"==typeof e&&null!==e){var t=e.$$typeof;switch(t){case r:switch(e=e.type){case u:case d:case a:case l:case i:case f:return e;default:switch(e=e&&e.$$typeof){case c:case p:case h:case g:case s:return e;default:return t}}case o:return t}}}function x(e){return k(e)===d}t.AsyncMode=u,t.ConcurrentMode=d,t.ContextConsumer=c,t.ContextProvider=s,t.Element=r,t.ForwardRef=p,t.Fragment=a,t.Lazy=h,t.Memo=g,t.Portal=o,t.Profiler=l,t.StrictMode=i,t.Suspense=f,t.isAsyncMode=function(e){return x(e)||k(e)===u},t.isConcurrentMode=x,t.isContextConsumer=function(e){return k(e)===c},t.isContextProvider=function(e){return k(e)===s},t.isElement=function(e){return"object"==typeof e&&null!==e&&e.$$typeof===r},t.isForwardRef=function(e){return k(e)===p},t.isFragment=function(e){return k(e)===a},t.isLazy=function(e){return k(e)===h},t.isMemo=function(e){return k(e)===g},t.isPortal=function(e){return k(e)===o},t.isProfiler=function(e){return k(e)===l},t.isStrictMode=function(e){return k(e)===i},t.isSuspense=function(e){return k(e)===f},t.isValidElementType=function(e){return"string"==typeof e||"function"==typeof e||e===a||e===d||e===l||e===i||e===f||e===m||"object"==typeof e&&null!==e&&(e.$$typeof===h||e.$$typeof===g||e.$$typeof===s||e.$$typeof===c||e.$$typeof===p||e.$$typeof===y||e.$$typeof===v||e.$$typeof===w||e.$$typeof===b)},t.typeOf=k},9864:(e,t,n)=>{"use strict";e.exports=n(9921)},8356:(e,t,n)=>{"use strict";function r(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,e.__proto__=t}function o(e){if(void 0===e)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return e}function a(e,t,n){return t in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function i(){return i=Object.assign||function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},i.apply(this,arguments)}var l=n(7294),s=n(5697),c=[],u=[];function d(e){var t=e(),n={loading:!0,loaded:null,error:null};return n.promise=t.then((function(e){return n.loading=!1,n.loaded=e,e})).catch((function(e){throw n.loading=!1,n.error=e,e})),n}function p(e){var t={loading:!1,loaded:{},error:null},n=[];try{Object.keys(e).forEach((function(r){var o=d(e[r]);o.loading?t.loading=!0:(t.loaded[r]=o.loaded,t.error=o.error),n.push(o.promise),o.promise.then((function(e){t.loaded[r]=e})).catch((function(e){t.error=e}))}))}catch(r){t.error=r}return t.promise=Promise.all(n).then((function(e){return t.loading=!1,e})).catch((function(e){throw t.loading=!1,e})),t}function f(e,t){return l.createElement((n=e)&&n.__esModule?n.default:n,t);var n}function m(e,t){var d,p;if(!t.loading)throw new Error("react-loadable requires a `loading` component");var m=i({loader:null,loading:null,delay:200,timeout:null,render:f,webpack:null,modules:null},t),g=null;function h(){return g||(g=e(m.loader)),g.promise}return c.push(h),"function"==typeof m.webpack&&u.push((function(){if((0,m.webpack)().every((function(e){return void 0!==e&&void 0!==n.m[e]})))return h()})),p=d=function(t){function n(n){var r;return a(o(o(r=t.call(this,n)||this)),"retry",(function(){r.setState({error:null,loading:!0,timedOut:!1}),g=e(m.loader),r._loadModule()})),h(),r.state={error:g.error,pastDelay:!1,timedOut:!1,loading:g.loading,loaded:g.loaded},r}r(n,t),n.preload=function(){return h()};var i=n.prototype;return i.UNSAFE_componentWillMount=function(){this._loadModule()},i.componentDidMount=function(){this._mounted=!0},i._loadModule=function(){var e=this;if(this.context.loadable&&Array.isArray(m.modules)&&m.modules.forEach((function(t){e.context.loadable.report(t)})),g.loading){var t=function(t){e._mounted&&e.setState(t)};"number"==typeof m.delay&&(0===m.delay?this.setState({pastDelay:!0}):this._delay=setTimeout((function(){t({pastDelay:!0})}),m.delay)),"number"==typeof m.timeout&&(this._timeout=setTimeout((function(){t({timedOut:!0})}),m.timeout));var n=function(){t({error:g.error,loaded:g.loaded,loading:g.loading}),e._clearTimeouts()};g.promise.then((function(){return n(),null})).catch((function(e){return n(),null}))}},i.componentWillUnmount=function(){this._mounted=!1,this._clearTimeouts()},i._clearTimeouts=function(){clearTimeout(this._delay),clearTimeout(this._timeout)},i.render=function(){return this.state.loading||this.state.error?l.createElement(m.loading,{isLoading:this.state.loading,pastDelay:this.state.pastDelay,timedOut:this.state.timedOut,error:this.state.error,retry:this.retry}):this.state.loaded?m.render(this.state.loaded,this.props):null},n}(l.Component),a(d,"contextTypes",{loadable:s.shape({report:s.func.isRequired})}),p}function g(e){return m(d,e)}g.Map=function(e){if("function"!=typeof e.render)throw new Error("LoadableMap requires a `render(loaded, props)` function");return m(p,e)};var h=function(e){function t(){return e.apply(this,arguments)||this}r(t,e);var n=t.prototype;return n.getChildContext=function(){return{loadable:{report:this.props.report}}},n.render=function(){return l.Children.only(this.props.children)},t}(l.Component);function b(e){for(var t=[];e.length;){var n=e.pop();t.push(n())}return Promise.all(t).then((function(){if(e.length)return b(e)}))}a(h,"propTypes",{report:s.func.isRequired}),a(h,"childContextTypes",{loadable:s.shape({report:s.func.isRequired}).isRequired}),g.Capture=h,g.preloadAll=function(){return new Promise((function(e,t){b(c).then(e,t)}))},g.preloadReady=function(){return new Promise((function(e,t){b(u).then(e,e)}))},e.exports=g},8790:(e,t,n)=>{"use strict";n.d(t,{H:()=>l,f:()=>i});var r=n(6550),o=n(7462),a=n(7294);function i(e,t,n){return void 0===n&&(n=[]),e.some((function(e){var o=e.path?(0,r.LX)(t,e):n.length?n[n.length-1].match:r.F0.computeRootMatch(t);return o&&(n.push({route:e,match:o}),e.routes&&i(e.routes,t,n)),o})),n}function l(e,t,n){return void 0===t&&(t={}),void 0===n&&(n={}),e?a.createElement(r.rs,n,e.map((function(e,n){return a.createElement(r.AW,{key:e.key||n,path:e.path,exact:e.exact,strict:e.strict,render:function(n){return e.render?e.render((0,o.Z)({},n,{},t,{route:e})):a.createElement(e.component,(0,o.Z)({},n,t,{route:e}))}})}))):null}},3727:(e,t,n)=>{"use strict";n.d(t,{OL:()=>v,VK:()=>u,rU:()=>h});var r=n(6550),o=n(5068),a=n(7294),i=n(9318),l=n(7462),s=n(3366),c=n(8776),u=function(e){function t(){for(var t,n=arguments.length,r=new Array(n),o=0;o<n;o++)r[o]=arguments[o];return(t=e.call.apply(e,[this].concat(r))||this).history=(0,i.lX)(t.props),t}return(0,o.Z)(t,e),t.prototype.render=function(){return a.createElement(r.F0,{history:this.history,children:this.props.children})},t}(a.Component);a.Component;var d=function(e,t){return"function"==typeof e?e(t):e},p=function(e,t){return"string"==typeof e?(0,i.ob)(e,null,null,t):e},f=function(e){return e},m=a.forwardRef;void 0===m&&(m=f);var g=m((function(e,t){var n=e.innerRef,r=e.navigate,o=e.onClick,i=(0,s.Z)(e,["innerRef","navigate","onClick"]),c=i.target,u=(0,l.Z)({},i,{onClick:function(e){try{o&&o(e)}catch(t){throw e.preventDefault(),t}e.defaultPrevented||0!==e.button||c&&"_self"!==c||function(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}(e)||(e.preventDefault(),r())}});return u.ref=f!==m&&t||n,a.createElement("a",u)}));var h=m((function(e,t){var n=e.component,o=void 0===n?g:n,u=e.replace,h=e.to,b=e.innerRef,y=(0,s.Z)(e,["component","replace","to","innerRef"]);return a.createElement(r.s6.Consumer,null,(function(e){e||(0,c.Z)(!1);var n=e.history,r=p(d(h,e.location),e.location),s=r?n.createHref(r):"",g=(0,l.Z)({},y,{href:s,navigate:function(){var t=d(h,e.location),r=(0,i.Ep)(e.location)===(0,i.Ep)(p(t));(u||r?n.replace:n.push)(t)}});return f!==m?g.ref=t||b:g.innerRef=b,a.createElement(o,g)}))})),b=function(e){return e},y=a.forwardRef;void 0===y&&(y=b);var v=y((function(e,t){var n=e["aria-current"],o=void 0===n?"page":n,i=e.activeClassName,u=void 0===i?"active":i,f=e.activeStyle,m=e.className,g=e.exact,v=e.isActive,w=e.location,k=e.sensitive,x=e.strict,S=e.style,E=e.to,_=e.innerRef,C=(0,s.Z)(e,["aria-current","activeClassName","activeStyle","className","exact","isActive","location","sensitive","strict","style","to","innerRef"]);return a.createElement(r.s6.Consumer,null,(function(e){e||(0,c.Z)(!1);var n=w||e.location,i=p(d(E,n),n),s=i.pathname,T=s&&s.replace(/([.+*?=^!:${}()[\]|/\\])/g,"\\$1"),j=T?(0,r.LX)(n.pathname,{path:T,exact:g,sensitive:k,strict:x}):null,A=!!(v?v(j,n):j),L="function"==typeof m?m(A):m,N="function"==typeof S?S(A):S;A&&(L=function(){for(var e=arguments.length,t=new Array(e),n=0;n<e;n++)t[n]=arguments[n];return t.filter((function(e){return e})).join(" ")}(L,u),N=(0,l.Z)({},N,f));var R=(0,l.Z)({"aria-current":A&&o||null,className:L,style:N,to:i},C);return b!==y?R.ref=t||_:R.innerRef=_,a.createElement(h,R)}))}))},6550:(e,t,n)=>{"use strict";n.d(t,{AW:()=>E,F0:()=>v,LX:()=>S,TH:()=>P,k6:()=>R,rs:()=>L,s6:()=>y});var r=n(5068),o=n(7294),a=n(5697),i=n.n(a),l=n(9318),s=n(8776),c=n(7462),u=n(4779),d=n.n(u),p=(n(9864),n(3366)),f=(n(8679),1073741823),m="undefined"!=typeof globalThis?globalThis:"undefined"!=typeof window?window:void 0!==n.g?n.g:{};var g=o.createContext||function(e,t){var n,a,l="__create-react-context-"+function(){var e="__global_unique_id__";return m[e]=(m[e]||0)+1}()+"__",s=function(e){function n(){for(var t,n,r,o=arguments.length,a=new Array(o),i=0;i<o;i++)a[i]=arguments[i];return(t=e.call.apply(e,[this].concat(a))||this).emitter=(n=t.props.value,r=[],{on:function(e){r.push(e)},off:function(e){r=r.filter((function(t){return t!==e}))},get:function(){return n},set:function(e,t){n=e,r.forEach((function(e){return e(n,t)}))}}),t}(0,r.Z)(n,e);var o=n.prototype;return o.getChildContext=function(){var e;return(e={})[l]=this.emitter,e},o.componentWillReceiveProps=function(e){if(this.props.value!==e.value){var n,r=this.props.value,o=e.value;((a=r)===(i=o)?0!==a||1/a==1/i:a!=a&&i!=i)?n=0:(n="function"==typeof t?t(r,o):f,0!==(n|=0)&&this.emitter.set(e.value,n))}var a,i},o.render=function(){return this.props.children},n}(o.Component);s.childContextTypes=((n={})[l]=i().object.isRequired,n);var c=function(t){function n(){for(var e,n=arguments.length,r=new Array(n),o=0;o<n;o++)r[o]=arguments[o];return(e=t.call.apply(t,[this].concat(r))||this).observedBits=void 0,e.state={value:e.getValue()},e.onUpdate=function(t,n){0!=((0|e.observedBits)&n)&&e.setState({value:e.getValue()})},e}(0,r.Z)(n,t);var o=n.prototype;return o.componentWillReceiveProps=function(e){var t=e.observedBits;this.observedBits=null==t?f:t},o.componentDidMount=function(){this.context[l]&&this.context[l].on(this.onUpdate);var e=this.props.observedBits;this.observedBits=null==e?f:e},o.componentWillUnmount=function(){this.context[l]&&this.context[l].off(this.onUpdate)},o.getValue=function(){return this.context[l]?this.context[l].get():e},o.render=function(){return(e=this.props.children,Array.isArray(e)?e[0]:e)(this.state.value);var e},n}(o.Component);return c.contextTypes=((a={})[l]=i().object,a),{Provider:s,Consumer:c}},h=function(e){var t=g();return t.displayName=e,t},b=h("Router-History"),y=h("Router"),v=function(e){function t(t){var n;return(n=e.call(this,t)||this).state={location:t.history.location},n._isMounted=!1,n._pendingLocation=null,t.staticContext||(n.unlisten=t.history.listen((function(e){n._pendingLocation=e}))),n}(0,r.Z)(t,e),t.computeRootMatch=function(e){return{path:"/",url:"/",params:{},isExact:"/"===e}};var n=t.prototype;return n.componentDidMount=function(){var e=this;this._isMounted=!0,this.unlisten&&this.unlisten(),this.props.staticContext||(this.unlisten=this.props.history.listen((function(t){e._isMounted&&e.setState({location:t})}))),this._pendingLocation&&this.setState({location:this._pendingLocation})},n.componentWillUnmount=function(){this.unlisten&&(this.unlisten(),this._isMounted=!1,this._pendingLocation=null)},n.render=function(){return o.createElement(y.Provider,{value:{history:this.props.history,location:this.state.location,match:t.computeRootMatch(this.state.location.pathname),staticContext:this.props.staticContext}},o.createElement(b.Provider,{children:this.props.children||null,value:this.props.history}))},t}(o.Component);o.Component;o.Component;var w={},k=1e4,x=0;function S(e,t){void 0===t&&(t={}),("string"==typeof t||Array.isArray(t))&&(t={path:t});var n=t,r=n.path,o=n.exact,a=void 0!==o&&o,i=n.strict,l=void 0!==i&&i,s=n.sensitive,c=void 0!==s&&s;return[].concat(r).reduce((function(t,n){if(!n&&""!==n)return null;if(t)return t;var r=function(e,t){var n=""+t.end+t.strict+t.sensitive,r=w[n]||(w[n]={});if(r[e])return r[e];var o=[],a={regexp:d()(e,o,t),keys:o};return x<k&&(r[e]=a,x++),a}(n,{end:a,strict:l,sensitive:c}),o=r.regexp,i=r.keys,s=o.exec(e);if(!s)return null;var u=s[0],p=s.slice(1),f=e===u;return a&&!f?null:{path:n,url:"/"===n&&""===u?"/":u,isExact:f,params:i.reduce((function(e,t,n){return e[t.name]=p[n],e}),{})}}),null)}var E=function(e){function t(){return e.apply(this,arguments)||this}return(0,r.Z)(t,e),t.prototype.render=function(){var e=this;return o.createElement(y.Consumer,null,(function(t){t||(0,s.Z)(!1);var n=e.props.location||t.location,r=e.props.computedMatch?e.props.computedMatch:e.props.path?S(n.pathname,e.props):t.match,a=(0,c.Z)({},t,{location:n,match:r}),i=e.props,l=i.children,u=i.component,d=i.render;return Array.isArray(l)&&function(e){return 0===o.Children.count(e)}(l)&&(l=null),o.createElement(y.Provider,{value:a},a.match?l?"function"==typeof l?l(a):l:u?o.createElement(u,a):d?d(a):null:"function"==typeof l?l(a):null)}))},t}(o.Component);function _(e){return"/"===e.charAt(0)?e:"/"+e}function C(e,t){if(!e)return t;var n=_(e);return 0!==t.pathname.indexOf(n)?t:(0,c.Z)({},t,{pathname:t.pathname.substr(n.length)})}function T(e){return"string"==typeof e?e:(0,l.Ep)(e)}function j(e){return function(){(0,s.Z)(!1)}}function A(){}o.Component;var L=function(e){function t(){return e.apply(this,arguments)||this}return(0,r.Z)(t,e),t.prototype.render=function(){var e=this;return o.createElement(y.Consumer,null,(function(t){t||(0,s.Z)(!1);var n,r,a=e.props.location||t.location;return o.Children.forEach(e.props.children,(function(e){if(null==r&&o.isValidElement(e)){n=e;var i=e.props.path||e.props.from;r=i?S(a.pathname,(0,c.Z)({},e.props,{path:i})):t.match}})),r?o.cloneElement(n,{location:a,computedMatch:r}):null}))},t}(o.Component);var N=o.useContext;function R(){return N(b)}function P(){return N(y).location}},5251:(e,t,n)=>{"use strict";var r=n(7294),o=Symbol.for("react.element"),a=Symbol.for("react.fragment"),i=Object.prototype.hasOwnProperty,l=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};function c(e,t,n){var r,a={},c=null,u=null;for(r in void 0!==n&&(c=""+n),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(u=t.ref),t)i.call(t,r)&&!s.hasOwnProperty(r)&&(a[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===a[r]&&(a[r]=t[r]);return{$$typeof:o,type:e,key:c,ref:u,props:a,_owner:l.current}}t.Fragment=a,t.jsx=c,t.jsxs=c},2408:(e,t)=>{"use strict";var n=Symbol.for("react.element"),r=Symbol.for("react.portal"),o=Symbol.for("react.fragment"),a=Symbol.for("react.strict_mode"),i=Symbol.for("react.profiler"),l=Symbol.for("react.provider"),s=Symbol.for("react.context"),c=Symbol.for("react.forward_ref"),u=Symbol.for("react.suspense"),d=Symbol.for("react.memo"),p=Symbol.for("react.lazy"),f=Symbol.iterator;var m={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,h={};function b(e,t,n){this.props=e,this.context=t,this.refs=h,this.updater=n||m}function y(){}function v(e,t,n){this.props=e,this.context=t,this.refs=h,this.updater=n||m}b.prototype.isReactComponent={},b.prototype.setState=function(e,t){if("object"!=typeof e&&"function"!=typeof e&&null!=e)throw Error("setState(...): takes an object of state variables to update or a function which returns an object of state variables.");this.updater.enqueueSetState(this,e,t,"setState")},b.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,"forceUpdate")},y.prototype=b.prototype;var w=v.prototype=new y;w.constructor=v,g(w,b.prototype),w.isPureReactComponent=!0;var k=Array.isArray,x=Object.prototype.hasOwnProperty,S={current:null},E={key:!0,ref:!0,__self:!0,__source:!0};function _(e,t,r){var o,a={},i=null,l=null;if(null!=t)for(o in void 0!==t.ref&&(l=t.ref),void 0!==t.key&&(i=""+t.key),t)x.call(t,o)&&!E.hasOwnProperty(o)&&(a[o]=t[o]);var s=arguments.length-2;if(1===s)a.children=r;else if(1<s){for(var c=Array(s),u=0;u<s;u++)c[u]=arguments[u+2];a.children=c}if(e&&e.defaultProps)for(o in s=e.defaultProps)void 0===a[o]&&(a[o]=s[o]);return{$$typeof:n,type:e,key:i,ref:l,props:a,_owner:S.current}}function C(e){return"object"==typeof e&&null!==e&&e.$$typeof===n}var T=/\/+/g;function j(e,t){return"object"==typeof e&&null!==e&&null!=e.key?function(e){var t={"=":"=0",":":"=2"};return"$"+e.replace(/[=:]/g,(function(e){return t[e]}))}(""+e.key):t.toString(36)}function A(e,t,o,a,i){var l=typeof e;"undefined"!==l&&"boolean"!==l||(e=null);var s=!1;if(null===e)s=!0;else switch(l){case"string":case"number":s=!0;break;case"object":switch(e.$$typeof){case n:case r:s=!0}}if(s)return i=i(s=e),e=""===a?"."+j(s,0):a,k(i)?(o="",null!=e&&(o=e.replace(T,"$&/")+"/"),A(i,t,o,"",(function(e){return e}))):null!=i&&(C(i)&&(i=function(e,t){return{$$typeof:n,type:e.type,key:t,ref:e.ref,props:e.props,_owner:e._owner}}(i,o+(!i.key||s&&s.key===i.key?"":(""+i.key).replace(T,"$&/")+"/")+e)),t.push(i)),1;if(s=0,a=""===a?".":a+":",k(e))for(var c=0;c<e.length;c++){var u=a+j(l=e[c],c);s+=A(l,t,o,u,i)}else if(u=function(e){return null===e||"object"!=typeof e?null:"function"==typeof(e=f&&e[f]||e["@@iterator"])?e:null}(e),"function"==typeof u)for(e=u.call(e),c=0;!(l=e.next()).done;)s+=A(l=l.value,t,o,u=a+j(l,c++),i);else if("object"===l)throw t=String(e),Error("Objects are not valid as a React child (found: "+("[object Object]"===t?"object with keys {"+Object.keys(e).join(", ")+"}":t)+"). If you meant to render a collection of children, use an array instead.");return s}function L(e,t,n){if(null==e)return e;var r=[],o=0;return A(e,r,"","",(function(e){return t.call(n,e,o++)})),r}function N(e){if(-1===e._status){var t=e._result;(t=t()).then((function(t){0!==e._status&&-1!==e._status||(e._status=1,e._result=t)}),(function(t){0!==e._status&&-1!==e._status||(e._status=2,e._result=t)})),-1===e._status&&(e._status=0,e._result=t)}if(1===e._status)return e._result.default;throw e._result}var R={current:null},P={transition:null},O={ReactCurrentDispatcher:R,ReactCurrentBatchConfig:P,ReactCurrentOwner:S};t.Children={map:L,forEach:function(e,t,n){L(e,(function(){t.apply(this,arguments)}),n)},count:function(e){var t=0;return L(e,(function(){t++})),t},toArray:function(e){return L(e,(function(e){return e}))||[]},only:function(e){if(!C(e))throw Error("React.Children.only expected to receive a single React element child.");return e}},t.Component=b,t.Fragment=o,t.Profiler=i,t.PureComponent=v,t.StrictMode=a,t.Suspense=u,t.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED=O,t.cloneElement=function(e,t,r){if(null==e)throw Error("React.cloneElement(...): The argument must be a React element, but you passed "+e+".");var o=g({},e.props),a=e.key,i=e.ref,l=e._owner;if(null!=t){if(void 0!==t.ref&&(i=t.ref,l=S.current),void 0!==t.key&&(a=""+t.key),e.type&&e.type.defaultProps)var s=e.type.defaultProps;for(c in t)x.call(t,c)&&!E.hasOwnProperty(c)&&(o[c]=void 0===t[c]&&void 0!==s?s[c]:t[c])}var c=arguments.length-2;if(1===c)o.children=r;else if(1<c){s=Array(c);for(var u=0;u<c;u++)s[u]=arguments[u+2];o.children=s}return{$$typeof:n,type:e.type,key:a,ref:i,props:o,_owner:l}},t.createContext=function(e){return(e={$$typeof:s,_currentValue:e,_currentValue2:e,_threadCount:0,Provider:null,Consumer:null,_defaultValue:null,_globalName:null}).Provider={$$typeof:l,_context:e},e.Consumer=e},t.createElement=_,t.createFactory=function(e){var t=_.bind(null,e);return t.type=e,t},t.createRef=function(){return{current:null}},t.forwardRef=function(e){return{$$typeof:c,render:e}},t.isValidElement=C,t.lazy=function(e){return{$$typeof:p,_payload:{_status:-1,_result:e},_init:N}},t.memo=function(e,t){return{$$typeof:d,type:e,compare:void 0===t?null:t}},t.startTransition=function(e){var t=P.transition;P.transition={};try{e()}finally{P.transition=t}},t.unstable_act=function(){throw Error("act(...) is not supported in production builds of React.")},t.useCallback=function(e,t){return R.current.useCallback(e,t)},t.useContext=function(e){return R.current.useContext(e)},t.useDebugValue=function(){},t.useDeferredValue=function(e){return R.current.useDeferredValue(e)},t.useEffect=function(e,t){return R.current.useEffect(e,t)},t.useId=function(){return R.current.useId()},t.useImperativeHandle=function(e,t,n){return R.current.useImperativeHandle(e,t,n)},t.useInsertionEffect=function(e,t){return R.current.useInsertionEffect(e,t)},t.useLayoutEffect=function(e,t){return R.current.useLayoutEffect(e,t)},t.useMemo=function(e,t){return R.current.useMemo(e,t)},t.useReducer=function(e,t,n){return R.current.useReducer(e,t,n)},t.useRef=function(e){return R.current.useRef(e)},t.useState=function(e){return R.current.useState(e)},t.useSyncExternalStore=function(e,t,n){return R.current.useSyncExternalStore(e,t,n)},t.useTransition=function(){return R.current.useTransition()},t.version="18.2.0"},7294:(e,t,n)=>{"use strict";e.exports=n(2408)},5893:(e,t,n)=>{"use strict";e.exports=n(5251)},53:(e,t)=>{"use strict";function n(e,t){var n=e.length;e.push(t);e:for(;0<n;){var r=n-1>>>1,o=e[r];if(!(0<a(o,t)))break e;e[r]=t,e[n]=o,n=r}}function r(e){return 0===e.length?null:e[0]}function o(e){if(0===e.length)return null;var t=e[0],n=e.pop();if(n!==t){e[0]=n;e:for(var r=0,o=e.length,i=o>>>1;r<i;){var l=2*(r+1)-1,s=e[l],c=l+1,u=e[c];if(0>a(s,n))c<o&&0>a(u,s)?(e[r]=u,e[c]=n,r=c):(e[r]=s,e[l]=n,r=l);else{if(!(c<o&&0>a(u,n)))break e;e[r]=u,e[c]=n,r=c}}}return t}function a(e,t){var n=e.sortIndex-t.sortIndex;return 0!==n?n:e.id-t.id}if("object"==typeof performance&&"function"==typeof performance.now){var i=performance;t.unstable_now=function(){return i.now()}}else{var l=Date,s=l.now();t.unstable_now=function(){return l.now()-s}}var c=[],u=[],d=1,p=null,f=3,m=!1,g=!1,h=!1,b="function"==typeof setTimeout?setTimeout:null,y="function"==typeof clearTimeout?clearTimeout:null,v="undefined"!=typeof setImmediate?setImmediate:null;function w(e){for(var t=r(u);null!==t;){if(null===t.callback)o(u);else{if(!(t.startTime<=e))break;o(u),t.sortIndex=t.expirationTime,n(c,t)}t=r(u)}}function k(e){if(h=!1,w(e),!g)if(null!==r(c))g=!0,P(x);else{var t=r(u);null!==t&&O(k,t.startTime-e)}}function x(e,n){g=!1,h&&(h=!1,y(C),C=-1),m=!0;var a=f;try{for(w(n),p=r(c);null!==p&&(!(p.expirationTime>n)||e&&!A());){var i=p.callback;if("function"==typeof i){p.callback=null,f=p.priorityLevel;var l=i(p.expirationTime<=n);n=t.unstable_now(),"function"==typeof l?p.callback=l:p===r(c)&&o(c),w(n)}else o(c);p=r(c)}if(null!==p)var s=!0;else{var d=r(u);null!==d&&O(k,d.startTime-n),s=!1}return s}finally{p=null,f=a,m=!1}}"undefined"!=typeof navigator&&void 0!==navigator.scheduling&&void 0!==navigator.scheduling.isInputPending&&navigator.scheduling.isInputPending.bind(navigator.scheduling);var S,E=!1,_=null,C=-1,T=5,j=-1;function A(){return!(t.unstable_now()-j<T)}function L(){if(null!==_){var e=t.unstable_now();j=e;var n=!0;try{n=_(!0,e)}finally{n?S():(E=!1,_=null)}}else E=!1}if("function"==typeof v)S=function(){v(L)};else if("undefined"!=typeof MessageChannel){var N=new MessageChannel,R=N.port2;N.port1.onmessage=L,S=function(){R.postMessage(null)}}else S=function(){b(L,0)};function P(e){_=e,E||(E=!0,S())}function O(e,n){C=b((function(){e(t.unstable_now())}),n)}t.unstable_IdlePriority=5,t.unstable_ImmediatePriority=1,t.unstable_LowPriority=4,t.unstable_NormalPriority=3,t.unstable_Profiling=null,t.unstable_UserBlockingPriority=2,t.unstable_cancelCallback=function(e){e.callback=null},t.unstable_continueExecution=function(){g||m||(g=!0,P(x))},t.unstable_forceFrameRate=function(e){0>e||125<e?console.error("forceFrameRate takes a positive int between 0 and 125, forcing frame rates higher than 125 fps is not supported"):T=0<e?Math.floor(1e3/e):5},t.unstable_getCurrentPriorityLevel=function(){return f},t.unstable_getFirstCallbackNode=function(){return r(c)},t.unstable_next=function(e){switch(f){case 1:case 2:case 3:var t=3;break;default:t=f}var n=f;f=t;try{return e()}finally{f=n}},t.unstable_pauseExecution=function(){},t.unstable_requestPaint=function(){},t.unstable_runWithPriority=function(e,t){switch(e){case 1:case 2:case 3:case 4:case 5:break;default:e=3}var n=f;f=e;try{return t()}finally{f=n}},t.unstable_scheduleCallback=function(e,o,a){var i=t.unstable_now();switch("object"==typeof a&&null!==a?a="number"==typeof(a=a.delay)&&0<a?i+a:i:a=i,e){case 1:var l=-1;break;case 2:l=250;break;case 5:l=1073741823;break;case 4:l=1e4;break;default:l=5e3}return e={id:d++,callback:o,priorityLevel:e,startTime:a,expirationTime:l=a+l,sortIndex:-1},a>i?(e.sortIndex=a,n(u,e),null===r(c)&&e===r(u)&&(h?(y(C),C=-1):h=!0,O(k,a-i))):(e.sortIndex=l,n(c,e),g||m||(g=!0,P(x))),e},t.unstable_shouldYield=A,t.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}},3840:(e,t,n)=>{"use strict";e.exports=n(53)},6774:e=>{e.exports=function(e,t,n,r){var o=n?n.call(r,e,t):void 0;if(void 0!==o)return!!o;if(e===t)return!0;if("object"!=typeof e||!e||"object"!=typeof t||!t)return!1;var a=Object.keys(e),i=Object.keys(t);if(a.length!==i.length)return!1;for(var l=Object.prototype.hasOwnProperty.bind(t),s=0;s<a.length;s++){var c=a[s];if(!l(c))return!1;var u=e[c],d=t[c];if(!1===(o=n?n.call(r,u,d,c):void 0)||void 0===o&&u!==d)return!1}return!0}},6809:(e,t,n)=>{"use strict";n.r(t),n.d(t,{default:()=>r});const r={title:"TAK Server Documentation",tagline:"Version 5.2",favicon:"img/tak.ico",url:"https://your-docusaurus-site.example.com",baseUrl:"/",onBrokenLinks:"throw",onBrokenMarkdownLinks:"warn",i18n:{defaultLocale:"en",locales:["en"],path:"i18n",localeConfigs:{}},presets:[["classic",{docs:{sidebarPath:"./sidebars.js",editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/"},blog:{showReadingTime:!0,editUrl:"https://gitlab.com/octospacc/editocttrialTools/-/blob/main/docusaurus-static/"},theme:{customCss:"./src/css/custom.css"}}]],themeConfig:{navbar:{title:"TAK Server Documentation",logo:{alt:"TAK Server docs",src:"img/tak.PNG"},items:[{type:"docSidebar",sidebarId:"tutorialSidebar",position:"left",label:"Docs"}],hideOnScroll:!1},prism:{theme:{plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]},darkTheme:{plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]},additionalLanguages:[],magicComments:[{className:"theme-code-block-highlighted-line",line:"highlight-next-line",block:{start:"highlight-start",end:"highlight-end"}}]},colorMode:{defaultMode:"light",disableSwitch:!1,respectPrefersColorScheme:!1},docs:{versionPersistence:"localStorage",sidebar:{hideable:!1,autoCollapseCategories:!1}},metadata:[],tableOfContents:{minHeadingLevel:2,maxHeadingLevel:3}},baseUrlIssueBanner:!0,onBrokenAnchors:"warn",onDuplicateRoutes:"warn",staticDirectories:["static"],customFields:{},plugins:[],themes:[],scripts:[],headTags:[],stylesheets:[],clientModules:[],titleDelimiter:"|",noIndex:!1,markdown:{format:"mdx",mermaid:!1,mdx1Compat:{comments:!0,admonitions:!0,headingIds:!0}}}},7462:(e,t,n)=>{"use strict";function r(){return r=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var r in n)Object.prototype.hasOwnProperty.call(n,r)&&(e[r]=n[r])}return e},r.apply(this,arguments)}n.d(t,{Z:()=>r})},5068:(e,t,n)=>{"use strict";function r(e,t){return r=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(e,t){return e.__proto__=t,e},r(e,t)}function o(e,t){e.prototype=Object.create(t.prototype),e.prototype.constructor=e,r(e,t)}n.d(t,{Z:()=>o})},3366:(e,t,n)=>{"use strict";function r(e,t){if(null==e)return{};var n,r,o={},a=Object.keys(e);for(r=0;r<a.length;r++)n=a[r],t.indexOf(n)>=0||(o[n]=e[n]);return o}n.d(t,{Z:()=>r})},512:(e,t,n)=>{"use strict";function r(e){var t,n,o="";if("string"==typeof e||"number"==typeof e)o+=e;else if("object"==typeof e)if(Array.isArray(e)){var a=e.length;for(t=0;t<a;t++)e[t]&&(n=r(e[t]))&&(o&&(o+=" "),o+=n)}else for(n in e)e[n]&&(o&&(o+=" "),o+=n);return o}n.d(t,{Z:()=>o});const o=function(){for(var e,t,n=0,o="",a=arguments.length;n<a;n++)(e=arguments[n])&&(t=r(e))&&(o&&(o+=" "),o+=t);return o}},2573:(e,t,n)=>{"use strict";n.d(t,{p1:()=>T,y$:()=>ee});var r,o,a,i,l,s,c,u=n(7294),d=n(512),p=Object.create,f=Object.defineProperty,m=Object.defineProperties,g=Object.getOwnPropertyDescriptor,h=Object.getOwnPropertyDescriptors,b=Object.getOwnPropertyNames,y=Object.getOwnPropertySymbols,v=Object.getPrototypeOf,w=Object.prototype.hasOwnProperty,k=Object.prototype.propertyIsEnumerable,x=(e,t,n)=>t in e?f(e,t,{enumerable:!0,configurable:!0,writable:!0,value:n}):e[t]=n,S=(e,t)=>{for(var n in t||(t={}))w.call(t,n)&&x(e,n,t[n]);if(y)for(var n of y(t))k.call(t,n)&&x(e,n,t[n]);return e},E=(e,t)=>m(e,h(t)),_=(e,t)=>{var n={};for(var r in e)w.call(e,r)&&t.indexOf(r)<0&&(n[r]=e[r]);if(null!=e&&y)for(var r of y(e))t.indexOf(r)<0&&k.call(e,r)&&(n[r]=e[r]);return n},C=(r={"../../node_modules/.pnpm/prismjs@1.29.0_patch_hash=vrxx3pzkik6jpmgpayxfjunetu/node_modules/prismjs/prism.js"(e,t){var n=function(){var e=/(?:^|\s)lang(?:uage)?-([\w-]+)(?=\s|$)/i,t=0,n={},r={util:{encode:function e(t){return t instanceof o?new o(t.type,e(t.content),t.alias):Array.isArray(t)?t.map(e):t.replace(/&/g,"&").replace(/</g,"<").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++t}),e.__id},clone:function e(t,n){var o,a;switch(n=n||{},r.util.type(t)){case"Object":if(a=r.util.objId(t),n[a])return n[a];for(var i in o={},n[a]=o,t)t.hasOwnProperty(i)&&(o[i]=e(t[i],n));return o;case"Array":return a=r.util.objId(t),n[a]?n[a]:(o=[],n[a]=o,t.forEach((function(t,r){o[r]=e(t,n)})),o);default:return t}},getLanguage:function(t){for(;t;){var n=e.exec(t.className);if(n)return n[1].toLowerCase();t=t.parentElement}return"none"},setLanguage:function(t,n){t.className=t.className.replace(RegExp(e,"gi"),""),t.classList.add("language-"+n)},isActive:function(e,t,n){for(var r="no-"+t;e;){var o=e.classList;if(o.contains(t))return!0;if(o.contains(r))return!1;e=e.parentElement}return!!n}},languages:{plain:n,plaintext:n,text:n,txt:n,extend:function(e,t){var n=r.util.clone(r.languages[e]);for(var o in t)n[o]=t[o];return n},insertBefore:function(e,t,n,o){var a=(o=o||r.languages)[e],i={};for(var l in a)if(a.hasOwnProperty(l)){if(l==t)for(var s in n)n.hasOwnProperty(s)&&(i[s]=n[s]);n.hasOwnProperty(l)||(i[l]=a[l])}var c=o[e];return o[e]=i,r.languages.DFS(r.languages,(function(t,n){n===c&&t!=e&&(this[t]=i)})),i},DFS:function e(t,n,o,a){a=a||{};var i=r.util.objId;for(var l in t)if(t.hasOwnProperty(l)){n.call(t,l,t[l],o||l);var s=t[l],c=r.util.type(s);"Object"!==c||a[i(s)]?"Array"!==c||a[i(s)]||(a[i(s)]=!0,e(s,n,l,a)):(a[i(s)]=!0,e(s,n,null,a))}}},plugins:{},highlight:function(e,t,n){var a={code:e,grammar:t,language:n};if(r.hooks.run("before-tokenize",a),!a.grammar)throw new Error('The language "'+a.language+'" has no grammar.');return a.tokens=r.tokenize(a.code,a.grammar),r.hooks.run("after-tokenize",a),o.stringify(r.util.encode(a.tokens),a.language)},tokenize:function(e,t){var n=t.rest;if(n){for(var r in n)t[r]=n[r];delete t.rest}var o=new l;return s(o,o.head,e),i(e,o,t,o.head,0),function(e){for(var t=[],n=e.head.next;n!==e.tail;)t.push(n.value),n=n.next;return t}(o)},hooks:{all:{},add:function(e,t){var n=r.hooks.all;n[e]=n[e]||[],n[e].push(t)},run:function(e,t){var n=r.hooks.all[e];if(n&&n.length)for(var o,a=0;o=n[a++];)o(t)}},Token:o};function o(e,t,n,r){this.type=e,this.content=t,this.alias=n,this.length=0|(r||"").length}function a(e,t,n,r){e.lastIndex=t;var o=e.exec(n);if(o&&r&&o[1]){var a=o[1].length;o.index+=a,o[0]=o[0].slice(a)}return o}function i(e,t,n,l,u,d){for(var p in n)if(n.hasOwnProperty(p)&&n[p]){var f=n[p];f=Array.isArray(f)?f:[f];for(var m=0;m<f.length;++m){if(d&&d.cause==p+","+m)return;var g=f[m],h=g.inside,b=!!g.lookbehind,y=!!g.greedy,v=g.alias;if(y&&!g.pattern.global){var w=g.pattern.toString().match(/[imsuy]*$/)[0];g.pattern=RegExp(g.pattern.source,w+"g")}for(var k=g.pattern||g,x=l.next,S=u;x!==t.tail&&!(d&&S>=d.reach);S+=x.value.length,x=x.next){var E=x.value;if(t.length>e.length)return;if(!(E instanceof o)){var _,C=1;if(y){if(!(_=a(k,S,e,b))||_.index>=e.length)break;var T=_.index,j=_.index+_[0].length,A=S;for(A+=x.value.length;T>=A;)A+=(x=x.next).value.length;if(S=A-=x.value.length,x.value instanceof o)continue;for(var L=x;L!==t.tail&&(A<j||"string"==typeof L.value);L=L.next)C++,A+=L.value.length;C--,E=e.slice(S,A),_.index-=S}else if(!(_=a(k,0,E,b)))continue;T=_.index;var N=_[0],R=E.slice(0,T),P=E.slice(T+N.length),O=S+E.length;d&&O>d.reach&&(d.reach=O);var D=x.prev;if(R&&(D=s(t,D,R),S+=R.length),c(t,D,C),x=s(t,D,new o(p,h?r.tokenize(N,h):N,v,N)),P&&s(t,x,P),C>1){var I={cause:p+","+m,reach:O};i(e,t,n,x.prev,S,I),d&&I.reach>d.reach&&(d.reach=I.reach)}}}}}}function l(){var e={value:null,prev:null,next:null},t={value:null,prev:e,next:null};e.next=t,this.head=e,this.tail=t,this.length=0}function s(e,t,n){var r=t.next,o={value:n,prev:t,next:r};return t.next=o,r.prev=o,e.length++,o}function c(e,t,n){for(var r=t.next,o=0;o<n&&r!==e.tail;o++)r=r.next;t.next=r,r.prev=t,e.length-=o}return o.stringify=function e(t,n){if("string"==typeof t)return t;if(Array.isArray(t)){var o="";return t.forEach((function(t){o+=e(t,n)})),o}var a={type:t.type,content:e(t.content,n),tag:"span",classes:["token",t.type],attributes:{},language:n},i=t.alias;i&&(Array.isArray(i)?Array.prototype.push.apply(a.classes,i):a.classes.push(i)),r.hooks.run("wrap",a);var l="";for(var s in a.attributes)l+=" "+s+'="'+(a.attributes[s]||"").replace(/"/g,""")+'"';return"<"+a.tag+' class="'+a.classes.join(" ")+'"'+l+">"+a.content+"</"+a.tag+">"},r}();t.exports=n,n.default=n}},function(){return o||(0,r[b(r)[0]])((o={exports:{}}).exports,o),o.exports}),T=((e,t,n)=>(n=null!=e?p(v(e)):{},((e,t,n,r)=>{if(t&&"object"==typeof t||"function"==typeof t)for(let o of b(t))w.call(e,o)||o===n||f(e,o,{get:()=>t[o],enumerable:!(r=g(t,o))||r.enumerable});return e})(!t&&e&&e.__esModule?n:f(n,"default",{value:e,enumerable:!0}),e)))(C());T.languages.markup={comment:{pattern:/<!--(?:(?!<!--)[\s\S])*?-->/,greedy:!0},prolog:{pattern:/<\?[\s\S]+?\?>/,greedy:!0},doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(^[^\[]*\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/i,name:/[^\s<>'"]+/}},cdata:{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,greedy:!0},tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"special-attr":[],"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},{pattern:/^(\s*)["']|["']$/,lookbehind:!0}]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},T.languages.markup.tag.inside["attr-value"].inside.entity=T.languages.markup.entity,T.languages.markup.doctype.inside["internal-subset"].inside=T.languages.markup,T.hooks.add("wrap",(function(e){"entity"===e.type&&(e.attributes.title=e.content.replace(/&/,"&"))})),Object.defineProperty(T.languages.markup.tag,"addInlined",{value:function(e,t){var n;(t=((n=((n={})["language-"+t]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:T.languages[t]},n.cdata=/^<!\[CDATA\[|\]\]>$/i,{"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:n}}))["language-"+t]={pattern:/[\s\S]+/,inside:T.languages[t]},{}))[e]={pattern:RegExp(/(<__[^>]*>)(?:<!\[CDATA\[(?:[^\]]|\](?!\]>))*\]\]>|(?!<!\[CDATA\[)[\s\S])*?(?=<\/__>)/.source.replace(/__/g,(function(){return e})),"i"),lookbehind:!0,greedy:!0,inside:n},T.languages.insertBefore("markup","cdata",t)}}),Object.defineProperty(T.languages.markup.tag,"addAttribute",{value:function(e,t){T.languages.markup.tag.inside["special-attr"].push({pattern:RegExp(/(^|["'\s])/.source+"(?:"+e+")"+/\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))/.source,"i"),lookbehind:!0,inside:{"attr-name":/^[^\s=]+/,"attr-value":{pattern:/=[\s\S]+/,inside:{value:{pattern:/(^=\s*(["']|(?!["'])))\S[\s\S]*(?=\2$)/,lookbehind:!0,alias:[t,"language-"+t],inside:T.languages[t]},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}}}})}}),T.languages.html=T.languages.markup,T.languages.mathml=T.languages.markup,T.languages.svg=T.languages.markup,T.languages.xml=T.languages.extend("markup",{}),T.languages.ssml=T.languages.xml,T.languages.atom=T.languages.xml,T.languages.rss=T.languages.xml,a=T,i={pattern:/\\[\\(){}[\]^$+*?|.]/,alias:"escape"},s="(?:[^\\\\-]|"+(l=/\\(?:x[\da-fA-F]{2}|u[\da-fA-F]{4}|u\{[\da-fA-F]+\}|0[0-7]{0,2}|[123][0-7]{2}|c[a-zA-Z]|.)/).source+")",s=RegExp(s+"-"+s),c={pattern:/(<|')[^<>']+(?=[>']$)/,lookbehind:!0,alias:"variable"},a.languages.regex={"char-class":{pattern:/((?:^|[^\\])(?:\\\\)*)\[(?:[^\\\]]|\\[\s\S])*\]/,lookbehind:!0,inside:{"char-class-negation":{pattern:/(^\[)\^/,lookbehind:!0,alias:"operator"},"char-class-punctuation":{pattern:/^\[|\]$/,alias:"punctuation"},range:{pattern:s,inside:{escape:l,"range-punctuation":{pattern:/-/,alias:"operator"}}},"special-escape":i,"char-set":{pattern:/\\[wsd]|\\p\{[^{}]+\}/i,alias:"class-name"},escape:l}},"special-escape":i,"char-set":{pattern:/\.|\\[wsd]|\\p\{[^{}]+\}/i,alias:"class-name"},backreference:[{pattern:/\\(?![123][0-7]{2})[1-9]/,alias:"keyword"},{pattern:/\\k<[^<>']+>/,alias:"keyword",inside:{"group-name":c}}],anchor:{pattern:/[$^]|\\[ABbGZz]/,alias:"function"},escape:l,group:[{pattern:/\((?:\?(?:<[^<>']+>|'[^<>']+'|[>:]|<?[=!]|[idmnsuxU]+(?:-[idmnsuxU]+)?:?))?/,alias:"punctuation",inside:{"group-name":c}},{pattern:/\)/,alias:"punctuation"}],quantifier:{pattern:/(?:[+*?]|\{\d+(?:,\d*)?\})[?+]?/,alias:"number"},alternation:{pattern:/\|/,alias:"keyword"}},T.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|trait)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:break|catch|continue|do|else|finally|for|function|if|in|instanceof|new|null|return|throw|try|while)\b/,boolean:/\b(?:false|true)\b/,function:/\b\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/},T.languages.javascript=T.languages.extend("clike",{"class-name":[T.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$A-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\.(?:constructor|prototype))/,lookbehind:!0}],keyword:[{pattern:/((?:^|\})\s*)catch\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|assert(?=\s*\{)|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally(?=\s*(?:\{|$))|for|from(?=\s*(?:['"]|$))|function|(?:get|set)(?=\s*(?:[#\[$\w\xA0-\uFFFF]|$))|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],function:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,number:{pattern:RegExp(/(^|[^\w$])/.source+"(?:"+/NaN|Infinity/.source+"|"+/0[bB][01]+(?:_[01]+)*n?/.source+"|"+/0[oO][0-7]+(?:_[0-7]+)*n?/.source+"|"+/0[xX][\dA-Fa-f]+(?:_[\dA-Fa-f]+)*n?/.source+"|"+/\d+(?:_\d+)*n/.source+"|"+/(?:\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\.\d+(?:_\d+)*)(?:[Ee][+-]?\d+(?:_\d+)*)?/.source+")"+/(?![\w$])/.source),lookbehind:!0},operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),T.languages.javascript["class-name"][0].pattern=/(\b(?:class|extends|implements|instanceof|interface|new)\s+)[\w.\\]+/,T.languages.insertBefore("javascript","keyword",{regex:{pattern:RegExp(/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)/.source+/\//.source+"(?:"+/(?:\[(?:[^\]\\\r\n]|\\.)*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}/.source+"|"+/(?:\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.|\[(?:[^[\]\\\r\n]|\\.)*\])*\])*\]|\\.|[^/\\\[\r\n])+\/[dgimyus]{0,7}v[dgimyus]{0,7}/.source+")"+/(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/.source),lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:T.languages.regex},"regex-delimiter":/^\/|\/$/,"regex-flags":/^[a-z]+$/}},"function-variable":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)?\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\))/,lookbehind:!0,inside:T.languages.javascript},{pattern:/(^|[^$\w\xA0-\uFFFF])(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=>)/i,lookbehind:!0,inside:T.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*=>)/,lookbehind:!0,inside:T.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()\s]|\s+(?![\s)])|\([^()]*\))+(?=\s*\)\s*\{)/,lookbehind:!0,inside:T.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),T.languages.insertBefore("javascript","string",{hashbang:{pattern:/^#!.*/,greedy:!0,alias:"comment"},"template-string":{pattern:/`(?:\\[\s\S]|\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}|(?!\$\{)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$\{(?:[^{}]|\{(?:[^{}]|\{[^}]*\})*\})+\}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\$\{|\}$/,alias:"punctuation"},rest:T.languages.javascript}},string:/[\s\S]+/}},"string-property":{pattern:/((?:^|[,{])[ \t]*)(["'])(?:\\(?:\r\n|[\s\S])|(?!\2)[^\\\r\n])*\2(?=\s*:)/m,lookbehind:!0,greedy:!0,alias:"property"}}),T.languages.insertBefore("javascript","operator",{"literal-property":{pattern:/((?:^|[,{])[ \t]*)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*:)/m,lookbehind:!0,alias:"property"}}),T.languages.markup&&(T.languages.markup.tag.addInlined("script","javascript"),T.languages.markup.tag.addAttribute(/on(?:abort|blur|change|click|composition(?:end|start|update)|dblclick|error|focus(?:in|out)?|key(?:down|up)|load|mouse(?:down|enter|leave|move|out|over|up)|reset|resize|scroll|select|slotchange|submit|unload|wheel)/.source,"javascript")),T.languages.js=T.languages.javascript,T.languages.actionscript=T.languages.extend("javascript",{keyword:/\b(?:as|break|case|catch|class|const|default|delete|do|dynamic|each|else|extends|final|finally|for|function|get|if|implements|import|in|include|instanceof|interface|internal|is|namespace|native|new|null|override|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|use|var|void|while|with)\b/,operator:/\+\+|--|(?:[+\-*\/%^]|&&?|\|\|?|<<?|>>?>?|[!=]=?)=?|[~?@]/}),T.languages.actionscript["class-name"].alias="function",delete T.languages.actionscript.parameter,delete T.languages.actionscript["literal-property"],T.languages.markup&&T.languages.insertBefore("actionscript","string",{xml:{pattern:/(^|[^.])<\/?\w+(?:\s+[^\s>\/=]+=("|')(?:\\[\s\S]|(?!\2)[^\\])*\2)*\s*\/?>/,lookbehind:!0,inside:T.languages.markup}}),function(e){var t=/#(?!\{).+/,n={pattern:/#\{[^}]+\}/,alias:"variable"};e.languages.coffeescript=e.languages.extend("javascript",{comment:t,string:[{pattern:/'(?:\\[\s\S]|[^\\'])*'/,greedy:!0},{pattern:/"(?:\\[\s\S]|[^\\"])*"/,greedy:!0,inside:{interpolation:n}}],keyword:/\b(?:and|break|by|catch|class|continue|debugger|delete|do|each|else|extend|extends|false|finally|for|if|in|instanceof|is|isnt|let|loop|namespace|new|no|not|null|of|off|on|or|own|return|super|switch|then|this|throw|true|try|typeof|undefined|unless|until|when|while|window|with|yes|yield)\b/,"class-member":{pattern:/@(?!\d)\w+/,alias:"variable"}}),e.languages.insertBefore("coffeescript","comment",{"multiline-comment":{pattern:/###[\s\S]+?###/,alias:"comment"},"block-regex":{pattern:/\/{3}[\s\S]*?\/{3}/,alias:"regex",inside:{comment:t,interpolation:n}}}),e.languages.insertBefore("coffeescript","string",{"inline-javascript":{pattern:/`(?:\\[\s\S]|[^\\`])*`/,inside:{delimiter:{pattern:/^`|`$/,alias:"punctuation"},script:{pattern:/[\s\S]+/,alias:"language-javascript",inside:e.languages.javascript}}},"multiline-string":[{pattern:/'''[\s\S]*?'''/,greedy:!0,alias:"string"},{pattern:/"""[\s\S]*?"""/,greedy:!0,alias:"string",inside:{interpolation:n}}]}),e.languages.insertBefore("coffeescript","keyword",{property:/(?!\d)\w+(?=\s*:(?!:))/}),delete e.languages.coffeescript["template-string"],e.languages.coffee=e.languages.coffeescript}(T),function(e){var t=e.languages.javadoclike={parameter:{pattern:/(^[\t ]*(?:\/{3}|\*|\/\*\*)\s*@(?:arg|arguments|param)\s+)\w+/m,lookbehind:!0},keyword:{pattern:/(^[\t ]*(?:\/{3}|\*|\/\*\*)\s*|\{)@[a-z][a-zA-Z-]+\b/m,lookbehind:!0},punctuation:/[{}]/};Object.defineProperty(t,"addSupport",{value:function(t,n){(t="string"==typeof t?[t]:t).forEach((function(t){var r=function(e){e.inside||(e.inside={}),e.inside.rest=n},o="doc-comment";if(a=e.languages[t]){var a,i=a[o];if((i=i||(a=e.languages.insertBefore(t,"comment",{"doc-comment":{pattern:/(^|[^\\])\/\*\*[^/][\s\S]*?(?:\*\/|$)/,lookbehind:!0,alias:"comment"}}))[o])instanceof RegExp&&(i=a[o]={pattern:i}),Array.isArray(i))for(var l=0,s=i.length;l<s;l++)i[l]instanceof RegExp&&(i[l]={pattern:i[l]}),r(i[l]);else r(i)}}))}}),t.addSupport(["java","javascript","php"],t)}(T),function(e){var t=/(?:"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n])*')/;(t=(e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:RegExp("@[\\w-](?:"+/[^;{\s"']|\s+(?!\s)/.source+"|"+t.source+")*?"+/(?:;|(?=\s*\{))/.source),inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\(\s*(?![\s)]))(?:[^()\s]|\s+(?![\s)])|\((?:[^()]|\([^()]*\))*\))+(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|"+/(?:[^\\\r\n()"']|\\[\s\S])*/.source+")\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:{pattern:RegExp("(^|[{}\\s])[^{}\\s](?:[^{};\"'\\s]|\\s+(?![\\s{])|"+t.source+")*(?=\\s*\\{)"),lookbehind:!0},string:{pattern:t,greedy:!0},property:{pattern:/(^|[^-\w\xA0-\uFFFF])(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*(?=\s*:)/i,lookbehind:!0},important:/!important\b/i,function:{pattern:/(^|[^-a-z0-9])[-a-z0-9]+(?=\()/i,lookbehind:!0},punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css,e.languages.markup))&&(t.tag.addInlined("style","css"),t.tag.addAttribute("style","css"))}(T),function(e){var t=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,n=(t=(e.languages.css.selector={pattern:e.languages.css.selector.pattern,lookbehind:!0,inside:t={"pseudo-element":/:(?:after|before|first-letter|first-line|selection)|::[-\w]+/,"pseudo-class":/:[-\w]+/,class:/\.[-\w]+/,id:/#[-\w]+/,attribute:{pattern:RegExp("\\[(?:[^[\\]\"']|"+t.source+")*\\]"),greedy:!0,inside:{punctuation:/^\[|\]$/,"case-sensitivity":{pattern:/(\s)[si]$/i,lookbehind:!0,alias:"keyword"},namespace:{pattern:/^(\s*)(?:(?!\s)[-*\w\xA0-\uFFFF])*\|(?!=)/,lookbehind:!0,inside:{punctuation:/\|$/}},"attr-name":{pattern:/^(\s*)(?:(?!\s)[-\w\xA0-\uFFFF])+/,lookbehind:!0},"attr-value":[t,{pattern:/(=\s*)(?:(?!\s)[-\w\xA0-\uFFFF])+(?=\s*$)/,lookbehind:!0}],operator:/[|~*^$]?=/}},"n-th":[{pattern:/(\(\s*)[+-]?\d*[\dn](?:\s*[+-]\s*\d+)?(?=\s*\))/,lookbehind:!0,inside:{number:/[\dn]+/,operator:/[+-]/}},{pattern:/(\(\s*)(?:even|odd)(?=\s*\))/i,lookbehind:!0}],combinator:/>|\+|~|\|\|/,punctuation:/[(),]/}},e.languages.css.atrule.inside["selector-function-argument"].inside=t,e.languages.insertBefore("css","property",{variable:{pattern:/(^|[^-\w\xA0-\uFFFF])--(?!\s)[-_a-z\xA0-\uFFFF](?:(?!\s)[-\w\xA0-\uFFFF])*/i,lookbehind:!0}}),{pattern:/(\b\d+)(?:%|[a-z]+(?![\w-]))/,lookbehind:!0}),{pattern:/(^|[^\w.-])-?(?:\d+(?:\.\d+)?|\.\d+)/,lookbehind:!0});e.languages.insertBefore("css","function",{operator:{pattern:/(\s)[+\-*\/](?=\s)/,lookbehind:!0},hexcode:{pattern:/\B#[\da-f]{3,8}\b/i,alias:"color"},color:[{pattern:/(^|[^\w-])(?:AliceBlue|AntiqueWhite|Aqua|Aquamarine|Azure|Beige|Bisque|Black|BlanchedAlmond|Blue|BlueViolet|Brown|BurlyWood|CadetBlue|Chartreuse|Chocolate|Coral|CornflowerBlue|Cornsilk|Crimson|Cyan|DarkBlue|DarkCyan|DarkGoldenRod|DarkGr[ae]y|DarkGreen|DarkKhaki|DarkMagenta|DarkOliveGreen|DarkOrange|DarkOrchid|DarkRed|DarkSalmon|DarkSeaGreen|DarkSlateBlue|DarkSlateGr[ae]y|DarkTurquoise|DarkViolet|DeepPink|DeepSkyBlue|DimGr[ae]y|DodgerBlue|FireBrick|FloralWhite|ForestGreen|Fuchsia|Gainsboro|GhostWhite|Gold|GoldenRod|Gr[ae]y|Green|GreenYellow|HoneyDew|HotPink|IndianRed|Indigo|Ivory|Khaki|Lavender|LavenderBlush|LawnGreen|LemonChiffon|LightBlue|LightCoral|LightCyan|LightGoldenRodYellow|LightGr[ae]y|LightGreen|LightPink|LightSalmon|LightSeaGreen|LightSkyBlue|LightSlateGr[ae]y|LightSteelBlue|LightYellow|Lime|LimeGreen|Linen|Magenta|Maroon|MediumAquaMarine|MediumBlue|MediumOrchid|MediumPurple|MediumSeaGreen|MediumSlateBlue|MediumSpringGreen|MediumTurquoise|MediumVioletRed|MidnightBlue|MintCream|MistyRose|Moccasin|NavajoWhite|Navy|OldLace|Olive|OliveDrab|Orange|OrangeRed|Orchid|PaleGoldenRod|PaleGreen|PaleTurquoise|PaleVioletRed|PapayaWhip|PeachPuff|Peru|Pink|Plum|PowderBlue|Purple|RebeccaPurple|Red|RosyBrown|RoyalBlue|SaddleBrown|Salmon|SandyBrown|SeaGreen|SeaShell|Sienna|Silver|SkyBlue|SlateBlue|SlateGr[ae]y|Snow|SpringGreen|SteelBlue|Tan|Teal|Thistle|Tomato|Transparent|Turquoise|Violet|Wheat|White|WhiteSmoke|Yellow|YellowGreen)(?![\w-])/i,lookbehind:!0},{pattern:/\b(?:hsl|rgb)\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*\)\B|\b(?:hsl|rgb)a\(\s*\d{1,3}\s*,\s*\d{1,3}%?\s*,\s*\d{1,3}%?\s*,\s*(?:0|0?\.\d+|1)\s*\)\B/i,inside:{unit:t,number:n,function:/[\w-]+(?=\()/,punctuation:/[(),]/}}],entity:/\\[\da-f]{1,8}/i,unit:t,number:n})}(T),function(e){var t=/[*&][^\s[\]{},]+/,n=/!(?:<[\w\-%#;/?:@&=+$,.!~*'()[\]]+>|(?:[a-zA-Z\d-]*!)?[\w\-%#;/?:@&=+$.~*'()]+)?/,r="(?:"+n.source+"(?:[ \t]+"+t.source+")?|"+t.source+"(?:[ \t]+"+n.source+")?)",o=/(?:[^\s\x00-\x08\x0e-\x1f!"#%&'*,\-:>?@[\]`{|}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]|[?:-]<PLAIN>)(?:[ \t]*(?:(?![#:])<PLAIN>|:<PLAIN>))*/.source.replace(/<PLAIN>/g,(function(){return/[^\s\x00-\x08\x0e-\x1f,[\]{}\x7f-\x84\x86-\x9f\ud800-\udfff\ufffe\uffff]/.source})),a=/"(?:[^"\\\r\n]|\\.)*"|'(?:[^'\\\r\n]|\\.)*'/.source;function i(e,t){t=(t||"").replace(/m/g,"")+"m";var n=/([:\-,[{]\s*(?:\s<<prop>>[ \t]+)?)(?:<<value>>)(?=[ \t]*(?:$|,|\]|\}|(?:[\r\n]\s*)?#))/.source.replace(/<<prop>>/g,(function(){return r})).replace(/<<value>>/g,(function(){return e}));return RegExp(n,t)}e.languages.yaml={scalar:{pattern:RegExp(/([\-:]\s*(?:\s<<prop>>[ \t]+)?[|>])[ \t]*(?:((?:\r?\n|\r)[ \t]+)\S[^\r\n]*(?:\2[^\r\n]+)*)/.source.replace(/<<prop>>/g,(function(){return r}))),lookbehind:!0,alias:"string"},comment:/#.*/,key:{pattern:RegExp(/((?:^|[:\-,[{\r\n?])[ \t]*(?:<<prop>>[ \t]+)?)<<key>>(?=\s*:\s)/.source.replace(/<<prop>>/g,(function(){return r})).replace(/<<key>>/g,(function(){return"(?:"+o+"|"+a+")"}))),lookbehind:!0,greedy:!0,alias:"atrule"},directive:{pattern:/(^[ \t]*)%.+/m,lookbehind:!0,alias:"important"},datetime:{pattern:i(/\d{4}-\d\d?-\d\d?(?:[tT]|[ \t]+)\d\d?:\d{2}:\d{2}(?:\.\d*)?(?:[ \t]*(?:Z|[-+]\d\d?(?::\d{2})?))?|\d{4}-\d{2}-\d{2}|\d\d?:\d{2}(?::\d{2}(?:\.\d*)?)?/.source),lookbehind:!0,alias:"number"},boolean:{pattern:i(/false|true/.source,"i"),lookbehind:!0,alias:"important"},null:{pattern:i(/null|~/.source,"i"),lookbehind:!0,alias:"important"},string:{pattern:i(a),lookbehind:!0,greedy:!0},number:{pattern:i(/[+-]?(?:0x[\da-f]+|0o[0-7]+|(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?|\.inf|\.nan)/.source,"i"),lookbehind:!0},tag:n,important:t,punctuation:/---|[:[\]{}\-,|>?]|\.\.\./},e.languages.yml=e.languages.yaml}(T),function(e){var t=/(?:\\.|[^\\\n\r]|(?:\n|\r\n?)(?![\r\n]))/.source;function n(e){return e=e.replace(/<inner>/g,(function(){return t})),RegExp(/((?:^|[^\\])(?:\\{2})*)/.source+"(?:"+e+")")}var r=/(?:\\.|``(?:[^`\r\n]|`(?!`))+``|`[^`\r\n]+`|[^\\|\r\n`])+/.source,o=/\|?__(?:\|__)+\|?(?:(?:\n|\r\n?)|(?![\s\S]))/.source.replace(/__/g,(function(){return r})),a=/\|?[ \t]*:?-{3,}:?[ \t]*(?:\|[ \t]*:?-{3,}:?[ \t]*)+\|?(?:\n|\r\n?)/.source,i=(e.languages.markdown=e.languages.extend("markup",{}),e.languages.insertBefore("markdown","prolog",{"front-matter-block":{pattern:/(^(?:\s*[\r\n])?)---(?!.)[\s\S]*?[\r\n]---(?!.)/,lookbehind:!0,greedy:!0,inside:{punctuation:/^---|---$/,"front-matter":{pattern:/\S+(?:\s+\S+)*/,alias:["yaml","language-yaml"],inside:e.languages.yaml}}},blockquote:{pattern:/^>(?:[\t ]*>)*/m,alias:"punctuation"},table:{pattern:RegExp("^"+o+a+"(?:"+o+")*","m"),inside:{"table-data-rows":{pattern:RegExp("^("+o+a+")(?:"+o+")*$"),lookbehind:!0,inside:{"table-data":{pattern:RegExp(r),inside:e.languages.markdown},punctuation:/\|/}},"table-line":{pattern:RegExp("^("+o+")"+a+"$"),lookbehind:!0,inside:{punctuation:/\||:?-{3,}:?/}},"table-header-row":{pattern:RegExp("^"+o+"$"),inside:{"table-header":{pattern:RegExp(r),alias:"important",inside:e.languages.markdown},punctuation:/\|/}}}},code:[{pattern:/((?:^|\n)[ \t]*\n|(?:^|\r\n?)[ \t]*\r\n?)(?: {4}|\t).+(?:(?:\n|\r\n?)(?: {4}|\t).+)*/,lookbehind:!0,alias:"keyword"},{pattern:/^```[\s\S]*?^```$/m,greedy:!0,inside:{"code-block":{pattern:/^(```.*(?:\n|\r\n?))[\s\S]+?(?=(?:\n|\r\n?)^```$)/m,lookbehind:!0},"code-language":{pattern:/^(```).+/,lookbehind:!0},punctuation:/```/}}],title:[{pattern:/\S.*(?:\n|\r\n?)(?:==+|--+)(?=[ \t]*$)/m,alias:"important",inside:{punctuation:/==+$|--+$/}},{pattern:/(^\s*)#.+/m,lookbehind:!0,alias:"important",inside:{punctuation:/^#+|#+$/}}],hr:{pattern:/(^\s*)([*-])(?:[\t ]*\2){2,}(?=\s*$)/m,lookbehind:!0,alias:"punctuation"},list:{pattern:/(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m,lookbehind:!0,alias:"punctuation"},"url-reference":{pattern:/!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/,inside:{variable:{pattern:/^(!?\[)[^\]]+/,lookbehind:!0},string:/(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/,punctuation:/^[\[\]!:]|[<>]/},alias:"url"},bold:{pattern:n(/\b__(?:(?!_)<inner>|_(?:(?!_)<inner>)+_)+__\b|\*\*(?:(?!\*)<inner>|\*(?:(?!\*)<inner>)+\*)+\*\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^..)[\s\S]+(?=..$)/,lookbehind:!0,inside:{}},punctuation:/\*\*|__/}},italic:{pattern:n(/\b_(?:(?!_)<inner>|__(?:(?!_)<inner>)+__)+_\b|\*(?:(?!\*)<inner>|\*\*(?:(?!\*)<inner>)+\*\*)+\*/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^.)[\s\S]+(?=.$)/,lookbehind:!0,inside:{}},punctuation:/[*_]/}},strike:{pattern:n(/(~~?)(?:(?!~)<inner>)+\2/.source),lookbehind:!0,greedy:!0,inside:{content:{pattern:/(^~~?)[\s\S]+(?=\1$)/,lookbehind:!0,inside:{}},punctuation:/~~?/}},"code-snippet":{pattern:/(^|[^\\`])(?:``[^`\r\n]+(?:`[^`\r\n]+)*``(?!`)|`[^`\r\n]+`(?!`))/,lookbehind:!0,greedy:!0,alias:["code","keyword"]},url:{pattern:n(/!?\[(?:(?!\])<inner>)+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)|[ \t]?\[(?:(?!\])<inner>)+\])/.source),lookbehind:!0,greedy:!0,inside:{operator:/^!/,content:{pattern:/(^\[)[^\]]+(?=\])/,lookbehind:!0,inside:{}},variable:{pattern:/(^\][ \t]?\[)[^\]]+(?=\]$)/,lookbehind:!0},url:{pattern:/(^\]\()[^\s)]+/,lookbehind:!0},string:{pattern:/(^[ \t]+)"(?:\\.|[^"\\])*"(?=\)$)/,lookbehind:!0}}}}),["url","bold","italic","strike"].forEach((function(t){["url","bold","italic","strike","code-snippet"].forEach((function(n){t!==n&&(e.languages.markdown[t].inside.content.inside[n]=e.languages.markdown[n])}))})),e.hooks.add("after-tokenize",(function(e){"markdown"!==e.language&&"md"!==e.language||function e(t){if(t&&"string"!=typeof t)for(var n=0,r=t.length;n<r;n++){var o,a=t[n];"code"!==a.type?e(a.content):(o=a.content[1],a=a.content[3],o&&a&&"code-language"===o.type&&"code-block"===a.type&&"string"==typeof o.content&&(o=o.content.replace(/\b#/g,"sharp").replace(/\b\+\+/g,"pp"),o="language-"+(o=(/[a-z][\w-]*/i.exec(o)||[""])[0].toLowerCase()),a.alias?"string"==typeof a.alias?a.alias=[a.alias,o]:a.alias.push(o):a.alias=[o]))}}(e.tokens)})),e.hooks.add("wrap",(function(t){if("code-block"===t.type){for(var n="",r=0,o=t.classes.length;r<o;r++){var a=t.classes[r];if(a=/language-(.+)/.exec(a)){n=a[1];break}}var c,u=e.languages[n];u?t.content=e.highlight(t.content.replace(i,"").replace(/&(\w{1,8}|#x?[\da-f]{1,8});/gi,(function(e,t){var n;return"#"===(t=t.toLowerCase())[0]?(n="x"===t[1]?parseInt(t.slice(2),16):Number(t.slice(1)),s(n)):l[t]||e})),u,n):n&&"none"!==n&&e.plugins.autoloader&&(c="md-"+(new Date).valueOf()+"-"+Math.floor(1e16*Math.random()),t.attributes.id=c,e.plugins.autoloader.loadLanguages(n,(function(){var t=document.getElementById(c);t&&(t.innerHTML=e.highlight(t.textContent,e.languages[n],n))})))}})),RegExp(e.languages.markup.tag.pattern.source,"gi")),l={amp:"&",lt:"<",gt:">",quot:'"'},s=String.fromCodePoint||String.fromCharCode;e.languages.md=e.languages.markdown}(T),T.languages.graphql={comment:/#.*/,description:{pattern:/(?:"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*")(?=\s*[a-z_])/i,greedy:!0,alias:"string",inside:{"language-markdown":{pattern:/(^"(?:"")?)(?!\1)[\s\S]+(?=\1$)/,lookbehind:!0,inside:T.languages.markdown}}},string:{pattern:/"""(?:[^"]|(?!""")")*"""|"(?:\\.|[^\\"\r\n])*"/,greedy:!0},number:/(?:\B-|\b)\d+(?:\.\d+)?(?:e[+-]?\d+)?\b/i,boolean:/\b(?:false|true)\b/,variable:/\$[a-z_]\w*/i,directive:{pattern:/@[a-z_]\w*/i,alias:"function"},"attr-name":{pattern:/\b[a-z_]\w*(?=\s*(?:\((?:[^()"]|"(?:\\.|[^\\"\r\n])*")*\))?:)/i,greedy:!0},"atom-input":{pattern:/\b[A-Z]\w*Input\b/,alias:"class-name"},scalar:/\b(?:Boolean|Float|ID|Int|String)\b/,constant:/\b[A-Z][A-Z_\d]*\b/,"class-name":{pattern:/(\b(?:enum|implements|interface|on|scalar|type|union)\s+|&\s*|:\s*|\[)[A-Z_]\w*/,lookbehind:!0},fragment:{pattern:/(\bfragment\s+|\.{3}\s*(?!on\b))[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-mutation":{pattern:/(\bmutation\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},"definition-query":{pattern:/(\bquery\s+)[a-zA-Z_]\w*/,lookbehind:!0,alias:"function"},keyword:/\b(?:directive|enum|extend|fragment|implements|input|interface|mutation|on|query|repeatable|scalar|schema|subscription|type|union)\b/,operator:/[!=|&]|\.{3}/,"property-query":/\w+(?=\s*\()/,object:/\w+(?=\s*\{)/,punctuation:/[!(){}\[\]:=,]/,property:/\w+/},T.hooks.add("after-tokenize",(function(e){if("graphql"===e.language)for(var t=e.tokens.filter((function(e){return"string"!=typeof e&&"comment"!==e.type&&"scalar"!==e.type})),n=0;n<t.length;){var r=t[n++];if("keyword"===r.type&&"mutation"===r.content){var o=[];if(d(["definition-mutation","punctuation"])&&"("===u(1).content){n+=2;var a=p(/^\($/,/^\)$/);if(-1===a)continue;for(;n<a;n++){var i=u(0);"variable"===i.type&&(f(i,"variable-input"),o.push(i.content))}n=a+1}if(d(["punctuation","property-query"])&&"{"===u(0).content&&(n++,f(u(0),"property-mutation"),0<o.length)){var l=p(/^\{$/,/^\}$/);if(-1!==l)for(var s=n;s<l;s++){var c=t[s];"variable"===c.type&&0<=o.indexOf(c.content)&&f(c,"variable-input")}}}}function u(e){return t[n+e]}function d(e,t){t=t||0;for(var n=0;n<e.length;n++){var r=u(n+t);if(!r||r.type!==e[n])return}return 1}function p(e,r){for(var o=1,a=n;a<t.length;a++){var i=t[a],l=i.content;if("punctuation"===i.type&&"string"==typeof l)if(e.test(l))o++;else if(r.test(l)&&0==--o)return a}return-1}function f(e,t){var n=e.alias;n?Array.isArray(n)||(e.alias=n=[n]):e.alias=n=[],n.push(t)}})),T.languages.sql={comment:{pattern:/(^|[^\\])(?:\/\*[\s\S]*?\*\/|(?:--|\/\/|#).*)/,lookbehind:!0},variable:[{pattern:/@(["'`])(?:\\[\s\S]|(?!\1)[^\\])+\1/,greedy:!0},/@[\w.$]+/],string:{pattern:/(^|[^@\\])("|')(?:\\[\s\S]|(?!\2)[^\\]|\2\2)*\2/,greedy:!0,lookbehind:!0},identifier:{pattern:/(^|[^@\\])`(?:\\[\s\S]|[^`\\]|``)*`/,greedy:!0,lookbehind:!0,inside:{punctuation:/^`|`$/}},function:/\b(?:AVG|COUNT|FIRST|FORMAT|LAST|LCASE|LEN|MAX|MID|MIN|MOD|NOW|ROUND|SUM|UCASE)(?=\s*\()/i,keyword:/\b(?:ACTION|ADD|AFTER|ALGORITHM|ALL|ALTER|ANALYZE|ANY|APPLY|AS|ASC|AUTHORIZATION|AUTO_INCREMENT|BACKUP|BDB|BEGIN|BERKELEYDB|BIGINT|BINARY|BIT|BLOB|BOOL|BOOLEAN|BREAK|BROWSE|BTREE|BULK|BY|CALL|CASCADED?|CASE|CHAIN|CHAR(?:ACTER|SET)?|CHECK(?:POINT)?|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMNS?|COMMENT|COMMIT(?:TED)?|COMPUTE|CONNECT|CONSISTENT|CONSTRAINT|CONTAINS(?:TABLE)?|CONTINUE|CONVERT|CREATE|CROSS|CURRENT(?:_DATE|_TIME|_TIMESTAMP|_USER)?|CURSOR|CYCLE|DATA(?:BASES?)?|DATE(?:TIME)?|DAY|DBCC|DEALLOCATE|DEC|DECIMAL|DECLARE|DEFAULT|DEFINER|DELAYED|DELETE|DELIMITERS?|DENY|DESC|DESCRIBE|DETERMINISTIC|DISABLE|DISCARD|DISK|DISTINCT|DISTINCTROW|DISTRIBUTED|DO|DOUBLE|DROP|DUMMY|DUMP(?:FILE)?|DUPLICATE|ELSE(?:IF)?|ENABLE|ENCLOSED|END|ENGINE|ENUM|ERRLVL|ERRORS|ESCAPED?|EXCEPT|EXEC(?:UTE)?|EXISTS|EXIT|EXPLAIN|EXTENDED|FETCH|FIELDS|FILE|FILLFACTOR|FIRST|FIXED|FLOAT|FOLLOWING|FOR(?: EACH ROW)?|FORCE|FOREIGN|FREETEXT(?:TABLE)?|FROM|FULL|FUNCTION|GEOMETRY(?:COLLECTION)?|GLOBAL|GOTO|GRANT|GROUP|HANDLER|HASH|HAVING|HOLDLOCK|HOUR|IDENTITY(?:COL|_INSERT)?|IF|IGNORE|IMPORT|INDEX|INFILE|INNER|INNODB|INOUT|INSERT|INT|INTEGER|INTERSECT|INTERVAL|INTO|INVOKER|ISOLATION|ITERATE|JOIN|KEYS?|KILL|LANGUAGE|LAST|LEAVE|LEFT|LEVEL|LIMIT|LINENO|LINES|LINESTRING|LOAD|LOCAL|LOCK|LONG(?:BLOB|TEXT)|LOOP|MATCH(?:ED)?|MEDIUM(?:BLOB|INT|TEXT)|MERGE|MIDDLEINT|MINUTE|MODE|MODIFIES|MODIFY|MONTH|MULTI(?:LINESTRING|POINT|POLYGON)|NATIONAL|NATURAL|NCHAR|NEXT|NO|NONCLUSTERED|NULLIF|NUMERIC|OFF?|OFFSETS?|ON|OPEN(?:DATASOURCE|QUERY|ROWSET)?|OPTIMIZE|OPTION(?:ALLY)?|ORDER|OUT(?:ER|FILE)?|OVER|PARTIAL|PARTITION|PERCENT|PIVOT|PLAN|POINT|POLYGON|PRECEDING|PRECISION|PREPARE|PREV|PRIMARY|PRINT|PRIVILEGES|PROC(?:EDURE)?|PUBLIC|PURGE|QUICK|RAISERROR|READS?|REAL|RECONFIGURE|REFERENCES|RELEASE|RENAME|REPEAT(?:ABLE)?|REPLACE|REPLICATION|REQUIRE|RESIGNAL|RESTORE|RESTRICT|RETURN(?:ING|S)?|REVOKE|RIGHT|ROLLBACK|ROUTINE|ROW(?:COUNT|GUIDCOL|S)?|RTREE|RULE|SAVE(?:POINT)?|SCHEMA|SECOND|SELECT|SERIAL(?:IZABLE)?|SESSION(?:_USER)?|SET(?:USER)?|SHARE|SHOW|SHUTDOWN|SIMPLE|SMALLINT|SNAPSHOT|SOME|SONAME|SQL|START(?:ING)?|STATISTICS|STATUS|STRIPED|SYSTEM_USER|TABLES?|TABLESPACE|TEMP(?:ORARY|TABLE)?|TERMINATED|TEXT(?:SIZE)?|THEN|TIME(?:STAMP)?|TINY(?:BLOB|INT|TEXT)|TOP?|TRAN(?:SACTIONS?)?|TRIGGER|TRUNCATE|TSEQUAL|TYPES?|UNBOUNDED|UNCOMMITTED|UNDEFINED|UNION|UNIQUE|UNLOCK|UNPIVOT|UNSIGNED|UPDATE(?:TEXT)?|USAGE|USE|USER|USING|VALUES?|VAR(?:BINARY|CHAR|CHARACTER|YING)|VIEW|WAITFOR|WARNINGS|WHEN|WHERE|WHILE|WITH(?: ROLLUP|IN)?|WORK|WRITE(?:TEXT)?|YEAR)\b/i,boolean:/\b(?:FALSE|NULL|TRUE)\b/i,number:/\b0x[\da-f]+\b|\b\d+(?:\.\d*)?|\B\.\d+\b/i,operator:/[-+*\/=%^~]|&&?|\|\|?|!=?|<(?:=>?|<|>)?|>[>=]?|\b(?:AND|BETWEEN|DIV|ILIKE|IN|IS|LIKE|NOT|OR|REGEXP|RLIKE|SOUNDS LIKE|XOR)\b/i,punctuation:/[;[\]()`,.]/},function(e){var t=e.languages.javascript["template-string"],n=t.pattern.source,r=t.inside.interpolation,o=r.inside["interpolation-punctuation"],a=r.pattern.source;function i(t,r){if(e.languages[t])return{pattern:RegExp("((?:"+r+")\\s*)"+n),lookbehind:!0,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},"embedded-code":{pattern:/[\s\S]+/,alias:t}}}}function l(t,n,r){return t={code:t,grammar:n,language:r},e.hooks.run("before-tokenize",t),t.tokens=e.tokenize(t.code,t.grammar),e.hooks.run("after-tokenize",t),t.tokens}function s(t,n,i){var s=e.tokenize(t,{interpolation:{pattern:RegExp(a),lookbehind:!0}}),c=0,u={},d=(s=l(s.map((function(e){if("string"==typeof e)return e;var n,r;for(e=e.content;-1!==t.indexOf((r=c++,n="___"+i.toUpperCase()+"_"+r+"___")););return u[n]=e,n})).join(""),n,i),Object.keys(u));return c=0,function t(n){for(var a=0;a<n.length;a++){if(c>=d.length)return;var i,s,p,f,m,g,h,b=n[a];"string"==typeof b||"string"==typeof b.content?(i=d[c],-1!==(h=(g="string"==typeof b?b:b.content).indexOf(i))&&(++c,s=g.substring(0,h),m=u[i],p=void 0,(f={})["interpolation-punctuation"]=o,3===(f=e.tokenize(m,f)).length&&((p=[1,1]).push.apply(p,l(f[1],e.languages.javascript,"javascript")),f.splice.apply(f,p)),p=new e.Token("interpolation",f,r.alias,m),f=g.substring(h+i.length),m=[],s&&m.push(s),m.push(p),f&&(t(g=[f]),m.push.apply(m,g)),"string"==typeof b?(n.splice.apply(n,[a,1].concat(m)),a+=m.length-1):b.content=m)):(h=b.content,Array.isArray(h)?t(h):t([h]))}}(s),new e.Token(i,s,"language-"+i,t)}e.languages.javascript["template-string"]=[i("css",/\b(?:styled(?:\([^)]*\))?(?:\s*\.\s*\w+(?:\([^)]*\))*)*|css(?:\s*\.\s*(?:global|resolve))?|createGlobalStyle|keyframes)/.source),i("html",/\bhtml|\.\s*(?:inner|outer)HTML\s*\+?=/.source),i("svg",/\bsvg/.source),i("markdown",/\b(?:markdown|md)/.source),i("graphql",/\b(?:gql|graphql(?:\s*\.\s*experimental)?)/.source),i("sql",/\bsql/.source),t].filter(Boolean);var c={javascript:!0,js:!0,typescript:!0,ts:!0,jsx:!0,tsx:!0};function u(e){return"string"==typeof e?e:Array.isArray(e)?e.map(u).join(""):u(e.content)}e.hooks.add("after-tokenize",(function(t){t.language in c&&function t(n){for(var r=0,o=n.length;r<o;r++){var a,i,l,c=n[r];"string"!=typeof c&&(a=c.content,Array.isArray(a)?"template-string"===c.type?(c=a[1],3===a.length&&"string"!=typeof c&&"embedded-code"===c.type&&(i=u(c),c=c.alias,c=Array.isArray(c)?c[0]:c,l=e.languages[c])&&(a[1]=s(i,l,c))):t(a):"string"!=typeof a&&t([a]))}}(t.tokens)}))}(T),function(e){e.languages.typescript=e.languages.extend("javascript",{"class-name":{pattern:/(\b(?:class|extends|implements|instanceof|interface|new|type)\s+)(?!keyof\b)(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?:\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>)?/,lookbehind:!0,greedy:!0,inside:null},builtin:/\b(?:Array|Function|Promise|any|boolean|console|never|number|string|symbol|unknown)\b/}),e.languages.typescript.keyword.push(/\b(?:abstract|declare|is|keyof|readonly|require)\b/,/\b(?:asserts|infer|interface|module|namespace|type)\b(?=\s*(?:[{_$a-zA-Z\xA0-\uFFFF]|$))/,/\btype\b(?=\s*(?:[\{*]|$))/),delete e.languages.typescript.parameter,delete e.languages.typescript["literal-property"];var t=e.languages.extend("typescript",{});delete t["class-name"],e.languages.typescript["class-name"].inside=t,e.languages.insertBefore("typescript","function",{decorator:{pattern:/@[$\w\xA0-\uFFFF]+/,inside:{at:{pattern:/^@/,alias:"operator"},function:/^[\s\S]+/}},"generic-function":{pattern:/#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>(?=\s*\()/,greedy:!0,inside:{function:/^#?(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:t}}}}),e.languages.ts=e.languages.typescript}(T),function(e){var t=e.languages.javascript,n=/\{(?:[^{}]|\{(?:[^{}]|\{[^{}]*\})*\})+\}/.source,r="(@(?:arg|argument|param|property)\\s+(?:"+n+"\\s+)?)";e.languages.jsdoc=e.languages.extend("javadoclike",{parameter:{pattern:RegExp(r+/(?:(?!\s)[$\w\xA0-\uFFFF.])+(?=\s|$)/.source),lookbehind:!0,inside:{punctuation:/\./}}}),e.languages.insertBefore("jsdoc","keyword",{"optional-parameter":{pattern:RegExp(r+/\[(?:(?!\s)[$\w\xA0-\uFFFF.])+(?:=[^[\]]+)?\](?=\s|$)/.source),lookbehind:!0,inside:{parameter:{pattern:/(^\[)[$\w\xA0-\uFFFF\.]+/,lookbehind:!0,inside:{punctuation:/\./}},code:{pattern:/(=)[\s\S]*(?=\]$)/,lookbehind:!0,inside:t,alias:"language-javascript"},punctuation:/[=[\]]/}},"class-name":[{pattern:RegExp(/(@(?:augments|class|extends|interface|memberof!?|template|this|typedef)\s+(?:<TYPE>\s+)?)[A-Z]\w*(?:\.[A-Z]\w*)*/.source.replace(/<TYPE>/g,(function(){return n}))),lookbehind:!0,inside:{punctuation:/\./}},{pattern:RegExp("(@[a-z]+\\s+)"+n),lookbehind:!0,inside:{string:t.string,number:t.number,boolean:t.boolean,keyword:e.languages.typescript.keyword,operator:/=>|\.\.\.|[&|?:*]/,punctuation:/[.,;=<>{}()[\]]/}}],example:{pattern:/(@example\s+(?!\s))(?:[^@\s]|\s+(?!\s))+?(?=\s*(?:\*\s*)?(?:@\w|\*\/))/,lookbehind:!0,inside:{code:{pattern:/^([\t ]*(?:\*\s*)?)\S.*$/m,lookbehind:!0,inside:t,alias:"language-javascript"}}}}),e.languages.javadoclike.addSupport("javascript",e.languages.jsdoc)}(T),function(e){e.languages.flow=e.languages.extend("javascript",{}),e.languages.insertBefore("flow","keyword",{type:[{pattern:/\b(?:[Bb]oolean|Function|[Nn]umber|[Ss]tring|[Ss]ymbol|any|mixed|null|void)\b/,alias:"class-name"}]}),e.languages.flow["function-variable"].pattern=/(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*(?=\s*=\s*(?:function\b|(?:\([^()]*\)(?:\s*:\s*\w+)?|(?!\s)[_$a-z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*)\s*=>))/i,delete e.languages.flow.parameter,e.languages.insertBefore("flow","operator",{"flow-punctuation":{pattern:/\{\||\|\}/,alias:"punctuation"}}),Array.isArray(e.languages.flow.keyword)||(e.languages.flow.keyword=[e.languages.flow.keyword]),e.languages.flow.keyword.unshift({pattern:/(^|[^$]\b)(?:Class|declare|opaque|type)\b(?!\$)/,lookbehind:!0},{pattern:/(^|[^$]\B)\$(?:Diff|Enum|Exact|Keys|ObjMap|PropertyType|Record|Shape|Subtype|Supertype|await)\b(?!\$)/,lookbehind:!0})}(T),T.languages.n4js=T.languages.extend("javascript",{keyword:/\b(?:Array|any|boolean|break|case|catch|class|const|constructor|continue|debugger|declare|default|delete|do|else|enum|export|extends|false|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|module|new|null|number|package|private|protected|public|return|set|static|string|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/}),T.languages.insertBefore("n4js","constant",{annotation:{pattern:/@+\w+/,alias:"operator"}}),T.languages.n4jsd=T.languages.n4js,function(e){function t(e,t){return RegExp(e.replace(/<ID>/g,(function(){return/(?!\s)[_$a-zA-Z\xA0-\uFFFF](?:(?!\s)[$\w\xA0-\uFFFF])*/.source})),t)}e.languages.insertBefore("javascript","function-variable",{"method-variable":{pattern:RegExp("(\\.\\s*)"+e.languages.javascript["function-variable"].pattern.source),lookbehind:!0,alias:["function-variable","method","function","property-access"]}}),e.languages.insertBefore("javascript","function",{method:{pattern:RegExp("(\\.\\s*)"+e.languages.javascript.function.source),lookbehind:!0,alias:["function","property-access"]}}),e.languages.insertBefore("javascript","constant",{"known-class-name":[{pattern:/\b(?:(?:Float(?:32|64)|(?:Int|Uint)(?:8|16|32)|Uint8Clamped)?Array|ArrayBuffer|BigInt|Boolean|DataView|Date|Error|Function|Intl|JSON|(?:Weak)?(?:Map|Set)|Math|Number|Object|Promise|Proxy|Reflect|RegExp|String|Symbol|WebAssembly)\b/,alias:"class-name"},{pattern:/\b(?:[A-Z]\w*)Error\b/,alias:"class-name"}]}),e.languages.insertBefore("javascript","keyword",{imports:{pattern:t(/(\bimport\b\s*)(?:<ID>(?:\s*,\s*(?:\*\s*as\s+<ID>|\{[^{}]*\}))?|\*\s*as\s+<ID>|\{[^{}]*\})(?=\s*\bfrom\b)/.source),lookbehind:!0,inside:e.languages.javascript},exports:{pattern:t(/(\bexport\b\s*)(?:\*(?:\s*as\s+<ID>)?(?=\s*\bfrom\b)|\{[^{}]*\})/.source),lookbehind:!0,inside:e.languages.javascript}}),e.languages.javascript.keyword.unshift({pattern:/\b(?:as|default|export|from|import)\b/,alias:"module"},{pattern:/\b(?:await|break|catch|continue|do|else|finally|for|if|return|switch|throw|try|while|yield)\b/,alias:"control-flow"},{pattern:/\bnull\b/,alias:["null","nil"]},{pattern:/\bundefined\b/,alias:"nil"}),e.languages.insertBefore("javascript","operator",{spread:{pattern:/\.{3}/,alias:"operator"},arrow:{pattern:/=>/,alias:"operator"}}),e.languages.insertBefore("javascript","punctuation",{"property-access":{pattern:t(/(\.\s*)#?<ID>/.source),lookbehind:!0},"maybe-class-name":{pattern:/(^|[^$\w\xA0-\uFFFF])[A-Z][$\w\xA0-\uFFFF]+/,lookbehind:!0},dom:{pattern:/\b(?:document|(?:local|session)Storage|location|navigator|performance|window)\b/,alias:"variable"},console:{pattern:/\bconsole(?=\s*\.)/,alias:"class-name"}});for(var n=["function","function-variable","method","method-variable","property-access"],r=0;r<n.length;r++){var o=n[r],a=e.languages.javascript[o];o=(a="RegExp"===e.util.type(a)?e.languages.javascript[o]={pattern:a}:a).inside||{};(a.inside=o)["maybe-class-name"]=/^[A-Z][\s\S]*/}}(T),function(e){var t=e.util.clone(e.languages.javascript),n=/(?:\s|\/\/.*(?!.)|\/\*(?:[^*]|\*(?!\/))\*\/)/.source,r=/(?:\{(?:\{(?:\{[^{}]*\}|[^{}])*\}|[^{}])*\})/.source,o=/(?:\{<S>*\.{3}(?:[^{}]|<BRACES>)*\})/.source;function a(e,t){return e=e.replace(/<S>/g,(function(){return n})).replace(/<BRACES>/g,(function(){return r})).replace(/<SPREAD>/g,(function(){return o})),RegExp(e,t)}function i(t){for(var n=[],r=0;r<t.length;r++){var o=t[r],a=!1;"string"!=typeof o&&("tag"===o.type&&o.content[0]&&"tag"===o.content[0].type?"</"===o.content[0].content[0].content?0<n.length&&n[n.length-1].tagName===l(o.content[0].content[1])&&n.pop():"/>"!==o.content[o.content.length-1].content&&n.push({tagName:l(o.content[0].content[1]),openedBraces:0}):0<n.length&&"punctuation"===o.type&&"{"===o.content?n[n.length-1].openedBraces++:0<n.length&&0<n[n.length-1].openedBraces&&"punctuation"===o.type&&"}"===o.content?n[n.length-1].openedBraces--:a=!0),(a||"string"==typeof o)&&0<n.length&&0===n[n.length-1].openedBraces&&(a=l(o),r<t.length-1&&("string"==typeof t[r+1]||"plain-text"===t[r+1].type)&&(a+=l(t[r+1]),t.splice(r+1,1)),0<r&&("string"==typeof t[r-1]||"plain-text"===t[r-1].type)&&(a=l(t[r-1])+a,t.splice(r-1,1),r--),t[r]=new e.Token("plain-text",a,null,a)),o.content&&"string"!=typeof o.content&&i(o.content)}}o=a(o).source,e.languages.jsx=e.languages.extend("markup",t),e.languages.jsx.tag.pattern=a(/<\/?(?:[\w.:-]+(?:<S>+(?:[\w.:$-]+(?:=(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s{'"/>=]+|<BRACES>))?|<SPREAD>))*<S>*\/?)?>/.source),e.languages.jsx.tag.inside.tag.pattern=/^<\/?[^\s>\/]*/,e.languages.jsx.tag.inside["attr-value"].pattern=/=(?!\{)(?:"(?:\\[\s\S]|[^\\"])*"|'(?:\\[\s\S]|[^\\'])*'|[^\s'">]+)/,e.languages.jsx.tag.inside.tag.inside["class-name"]=/^[A-Z]\w*(?:\.[A-Z]\w*)*$/,e.languages.jsx.tag.inside.comment=t.comment,e.languages.insertBefore("inside","attr-name",{spread:{pattern:a(/<SPREAD>/.source),inside:e.languages.jsx}},e.languages.jsx.tag),e.languages.insertBefore("inside","special-attr",{script:{pattern:a(/=<BRACES>/.source),alias:"language-javascript",inside:{"script-punctuation":{pattern:/^=(?=\{)/,alias:"punctuation"},rest:e.languages.jsx}}},e.languages.jsx.tag);var l=function(e){return e?"string"==typeof e?e:"string"==typeof e.content?e.content:e.content.map(l).join(""):""};e.hooks.add("after-tokenize",(function(e){"jsx"!==e.language&&"tsx"!==e.language||i(e.tokens)}))}(T),function(e){var t=e.util.clone(e.languages.typescript);(t=(e.languages.tsx=e.languages.extend("jsx",t),delete e.languages.tsx.parameter,delete e.languages.tsx["literal-property"],e.languages.tsx.tag)).pattern=RegExp(/(^|[^\w$]|(?=<\/))/.source+"(?:"+t.pattern.source+")",t.pattern.flags),t.lookbehind=!0}(T),T.languages.swift={comment:{pattern:/(^|[^\\:])(?:\/\/.*|\/\*(?:[^/*]|\/(?!\*)|\*(?!\/)|\/\*(?:[^*]|\*(?!\/))*\*\/)*\*\/)/,lookbehind:!0,greedy:!0},"string-literal":[{pattern:RegExp(/(^|[^"#])/.source+"(?:"+/"(?:\\(?:\((?:[^()]|\([^()]*\))*\)|\r\n|[^(])|[^\\\r\n"])*"/.source+"|"+/"""(?:\\(?:\((?:[^()]|\([^()]*\))*\)|[^(])|[^\\"]|"(?!""))*"""/.source+")"+/(?!["#])/.source),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\\($/,alias:"punctuation"},punctuation:/\\(?=[\r\n])/,string:/[\s\S]+/}},{pattern:RegExp(/(^|[^"#])(#+)/.source+"(?:"+/"(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|\r\n|[^#])|[^\\\r\n])*?"/.source+"|"+/"""(?:\\(?:#+\((?:[^()]|\([^()]*\))*\)|[^#])|[^\\])*?"""/.source+")\\2"),lookbehind:!0,greedy:!0,inside:{interpolation:{pattern:/(\\#+\()(?:[^()]|\([^()]*\))*(?=\))/,lookbehind:!0,inside:null},"interpolation-punctuation":{pattern:/^\)|\\#+\($/,alias:"punctuation"},string:/[\s\S]+/}}],directive:{pattern:RegExp(/#/.source+"(?:"+/(?:elseif|if)\b/.source+"(?:[ \t]*"+/(?:![ \t]*)?(?:\b\w+\b(?:[ \t]*\((?:[^()]|\([^()]*\))*\))?|\((?:[^()]|\([^()]*\))*\))(?:[ \t]*(?:&&|\|\|))?/.source+")+|"+/(?:else|endif)\b/.source+")"),alias:"property",inside:{"directive-name":/^#\w+/,boolean:/\b(?:false|true)\b/,number:/\b\d+(?:\.\d+)*\b/,operator:/!|&&|\|\||[<>]=?/,punctuation:/[(),]/}},literal:{pattern:/#(?:colorLiteral|column|dsohandle|file(?:ID|Literal|Path)?|function|imageLiteral|line)\b/,alias:"constant"},"other-directive":{pattern:/#\w+\b/,alias:"property"},attribute:{pattern:/@\w+/,alias:"atrule"},"function-definition":{pattern:/(\bfunc\s+)\w+/,lookbehind:!0,alias:"function"},label:{pattern:/\b(break|continue)\s+\w+|\b[a-zA-Z_]\w*(?=\s*:\s*(?:for|repeat|while)\b)/,lookbehind:!0,alias:"important"},keyword:/\b(?:Any|Protocol|Self|Type|actor|as|assignment|associatedtype|associativity|async|await|break|case|catch|class|continue|convenience|default|defer|deinit|didSet|do|dynamic|else|enum|extension|fallthrough|fileprivate|final|for|func|get|guard|higherThan|if|import|in|indirect|infix|init|inout|internal|is|isolated|lazy|left|let|lowerThan|mutating|none|nonisolated|nonmutating|open|operator|optional|override|postfix|precedencegroup|prefix|private|protocol|public|repeat|required|rethrows|return|right|safe|self|set|some|static|struct|subscript|super|switch|throw|throws|try|typealias|unowned|unsafe|var|weak|where|while|willSet)\b/,boolean:/\b(?:false|true)\b/,nil:{pattern:/\bnil\b/,alias:"constant"},"short-argument":/\$\d+\b/,omit:{pattern:/\b_\b/,alias:"keyword"},number:/\b(?:[\d_]+(?:\.[\de_]+)?|0x[a-f0-9_]+(?:\.[a-f0-9p_]+)?|0b[01_]+|0o[0-7_]+)\b/i,"class-name":/\b[A-Z](?:[A-Z_\d]*[a-z]\w*)?\b/,function:/\b[a-z_]\w*(?=\s*\()/i,constant:/\b(?:[A-Z_]{2,}|k[A-Z][A-Za-z_]+)\b/,operator:/[-+*/%=!<>&|^~?]+|\.[.\-+*/%=!<>&|^~?]+/,punctuation:/[{}[\]();,.:\\]/},T.languages.swift["string-literal"].forEach((function(e){e.inside.interpolation.inside=T.languages.swift})),function(e){e.languages.kotlin=e.languages.extend("clike",{keyword:{pattern:/(^|[^.])\b(?:abstract|actual|annotation|as|break|by|catch|class|companion|const|constructor|continue|crossinline|data|do|dynamic|else|enum|expect|external|final|finally|for|fun|get|if|import|in|infix|init|inline|inner|interface|internal|is|lateinit|noinline|null|object|open|operator|out|override|package|private|protected|public|reified|return|sealed|set|super|suspend|tailrec|this|throw|to|try|typealias|val|var|vararg|when|where|while)\b/,lookbehind:!0},function:[{pattern:/(?:`[^\r\n`]+`|\b\w+)(?=\s*\()/,greedy:!0},{pattern:/(\.)(?:`[^\r\n`]+`|\w+)(?=\s*\{)/,lookbehind:!0,greedy:!0}],number:/\b(?:0[xX][\da-fA-F]+(?:_[\da-fA-F]+)*|0[bB][01]+(?:_[01]+)*|\d+(?:_\d+)*(?:\.\d+(?:_\d+)*)?(?:[eE][+-]?\d+(?:_\d+)*)?[fFL]?)\b/,operator:/\+[+=]?|-[-=>]?|==?=?|!(?:!|==?)?|[\/*%<>]=?|[?:]:?|\.\.|&&|\|\||\b(?:and|inv|or|shl|shr|ushr|xor)\b/}),delete e.languages.kotlin["class-name"];var t={"interpolation-punctuation":{pattern:/^\$\{?|\}$/,alias:"punctuation"},expression:{pattern:/[\s\S]+/,inside:e.languages.kotlin}};e.languages.insertBefore("kotlin","string",{"string-literal":[{pattern:/"""(?:[^$]|\$(?:(?!\{)|\{[^{}]*\}))*?"""/,alias:"multiline",inside:{interpolation:{pattern:/\$(?:[a-z_]\w*|\{[^{}]*\})/i,inside:t},string:/[\s\S]+/}},{pattern:/"(?:[^"\\\r\n$]|\\.|\$(?:(?!\{)|\{[^{}]*\}))*"/,alias:"singleline",inside:{interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\$(?:[a-z_]\w*|\{[^{}]*\})/i,lookbehind:!0,inside:t},string:/[\s\S]+/}}],char:{pattern:/'(?:[^'\\\r\n]|\\(?:.|u[a-fA-F0-9]{0,4}))'/,greedy:!0}}),delete e.languages.kotlin.string,e.languages.insertBefore("kotlin","keyword",{annotation:{pattern:/\B@(?:\w+:)?(?:[A-Z]\w*|\[[^\]]+\])/,alias:"builtin"}}),e.languages.insertBefore("kotlin","function",{label:{pattern:/\b\w+@|@\w+\b/,alias:"symbol"}}),e.languages.kt=e.languages.kotlin,e.languages.kts=e.languages.kotlin}(T),T.languages.c=T.languages.extend("clike",{comment:{pattern:/\/\/(?:[^\r\n\\]|\\(?:\r\n?|\n|(?![\r\n])))*|\/\*[\s\S]*?(?:\*\/|$)/,greedy:!0},string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},"class-name":{pattern:/(\b(?:enum|struct)\s+(?:__attribute__\s*\(\([\s\S]*?\)\)\s*)?)\w+|\b[a-z]\w*_t\b/,lookbehind:!0},keyword:/\b(?:_Alignas|_Alignof|_Atomic|_Bool|_Complex|_Generic|_Imaginary|_Noreturn|_Static_assert|_Thread_local|__attribute__|asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|inline|int|long|register|return|short|signed|sizeof|static|struct|switch|typedef|typeof|union|unsigned|void|volatile|while)\b/,function:/\b[a-z_]\w*(?=\s*\()/i,number:/(?:\b0x(?:[\da-f]+(?:\.[\da-f]*)?|\.[\da-f]+)(?:p[+-]?\d+)?|(?:\b\d+(?:\.\d*)?|\B\.\d+)(?:e[+-]?\d+)?)[ful]{0,4}/i,operator:/>>=?|<<=?|->|([-+&|:])\1|[?:~]|[-+*/%&|^!=<>]=?/}),T.languages.insertBefore("c","string",{char:{pattern:/'(?:\\(?:\r\n|[\s\S])|[^'\\\r\n]){0,32}'/,greedy:!0}}),T.languages.insertBefore("c","string",{macro:{pattern:/(^[\t ]*)#\s*[a-z](?:[^\r\n\\/]|\/(?!\*)|\/\*(?:[^*]|\*(?!\/))*\*\/|\\(?:\r\n|[\s\S]))*/im,lookbehind:!0,greedy:!0,alias:"property",inside:{string:[{pattern:/^(#\s*include\s*)<[^>]+>/,lookbehind:!0},T.languages.c.string],char:T.languages.c.char,comment:T.languages.c.comment,"macro-name":[{pattern:/(^#\s*define\s+)\w+\b(?!\()/i,lookbehind:!0},{pattern:/(^#\s*define\s+)\w+\b(?=\()/i,lookbehind:!0,alias:"function"}],directive:{pattern:/^(#\s*)[a-z]+/,lookbehind:!0,alias:"keyword"},"directive-hash":/^#/,punctuation:/##|\\(?=[\r\n])/,expression:{pattern:/\S[\s\S]*/,inside:T.languages.c}}}}),T.languages.insertBefore("c","function",{constant:/\b(?:EOF|NULL|SEEK_CUR|SEEK_END|SEEK_SET|__DATE__|__FILE__|__LINE__|__TIMESTAMP__|__TIME__|__func__|stderr|stdin|stdout)\b/}),delete T.languages.c.boolean,T.languages.objectivec=T.languages.extend("c",{string:{pattern:/@?"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"/,greedy:!0},keyword:/\b(?:asm|auto|break|case|char|const|continue|default|do|double|else|enum|extern|float|for|goto|if|in|inline|int|long|register|return|self|short|signed|sizeof|static|struct|super|switch|typedef|typeof|union|unsigned|void|volatile|while)\b|(?:@interface|@end|@implementation|@protocol|@class|@public|@protected|@private|@property|@try|@catch|@finally|@throw|@synthesize|@dynamic|@selector)\b/,operator:/-[->]?|\+\+?|!=?|<<?=?|>>?=?|==?|&&?|\|\|?|[~^%?*\/@]/}),delete T.languages.objectivec["class-name"],T.languages.objc=T.languages.objectivec,T.languages.reason=T.languages.extend("clike",{string:{pattern:/"(?:\\(?:\r\n|[\s\S])|[^\\\r\n"])*"/,greedy:!0},"class-name":/\b[A-Z]\w*/,keyword:/\b(?:and|as|assert|begin|class|constraint|do|done|downto|else|end|exception|external|for|fun|function|functor|if|in|include|inherit|initializer|lazy|let|method|module|mutable|new|nonrec|object|of|open|or|private|rec|sig|struct|switch|then|to|try|type|val|virtual|when|while|with)\b/,operator:/\.{3}|:[:=]|\|>|->|=(?:==?|>)?|<=?|>=?|[|^?'#!~`]|[+\-*\/]\.?|\b(?:asr|land|lor|lsl|lsr|lxor|mod)\b/}),T.languages.insertBefore("reason","class-name",{char:{pattern:/'(?:\\x[\da-f]{2}|\\o[0-3][0-7][0-7]|\\\d{3}|\\.|[^'\\\r\n])'/,greedy:!0},constructor:/\b[A-Z]\w*\b(?!\s*\.)/,label:{pattern:/\b[a-z]\w*(?=::)/,alias:"symbol"}}),delete T.languages.reason.function,function(e){for(var t=/\/\*(?:[^*/]|\*(?!\/)|\/(?!\*)|<self>)*\*\//.source,n=0;n<2;n++)t=t.replace(/<self>/g,(function(){return t}));t=t.replace(/<self>/g,(function(){return/[^\s\S]/.source})),e.languages.rust={comment:[{pattern:RegExp(/(^|[^\\])/.source+t),lookbehind:!0,greedy:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/b?"(?:\\[\s\S]|[^\\"])*"|b?r(#*)"(?:[^"]|"(?!\1))*"\1/,greedy:!0},char:{pattern:/b?'(?:\\(?:x[0-7][\da-fA-F]|u\{(?:[\da-fA-F]_*){1,6}\}|.)|[^\\\r\n\t'])'/,greedy:!0},attribute:{pattern:/#!?\[(?:[^\[\]"]|"(?:\\[\s\S]|[^\\"])*")*\]/,greedy:!0,alias:"attr-name",inside:{string:null}},"closure-params":{pattern:/([=(,:]\s*|\bmove\s*)\|[^|]*\||\|[^|]*\|(?=\s*(?:\{|->))/,lookbehind:!0,greedy:!0,inside:{"closure-punctuation":{pattern:/^\||\|$/,alias:"punctuation"},rest:null}},"lifetime-annotation":{pattern:/'\w+/,alias:"symbol"},"fragment-specifier":{pattern:/(\$\w+:)[a-z]+/,lookbehind:!0,alias:"punctuation"},variable:/\$\w+/,"function-definition":{pattern:/(\bfn\s+)\w+/,lookbehind:!0,alias:"function"},"type-definition":{pattern:/(\b(?:enum|struct|trait|type|union)\s+)\w+/,lookbehind:!0,alias:"class-name"},"module-declaration":[{pattern:/(\b(?:crate|mod)\s+)[a-z][a-z_\d]*/,lookbehind:!0,alias:"namespace"},{pattern:/(\b(?:crate|self|super)\s*)::\s*[a-z][a-z_\d]*\b(?:\s*::(?:\s*[a-z][a-z_\d]*\s*::)*)?/,lookbehind:!0,alias:"namespace",inside:{punctuation:/::/}}],keyword:[/\b(?:Self|abstract|as|async|await|become|box|break|const|continue|crate|do|dyn|else|enum|extern|final|fn|for|if|impl|in|let|loop|macro|match|mod|move|mut|override|priv|pub|ref|return|self|static|struct|super|trait|try|type|typeof|union|unsafe|unsized|use|virtual|where|while|yield)\b/,/\b(?:bool|char|f(?:32|64)|[ui](?:8|16|32|64|128|size)|str)\b/],function:/\b[a-z_]\w*(?=\s*(?:::\s*<|\())/,macro:{pattern:/\b\w+!/,alias:"property"},constant:/\b[A-Z_][A-Z_\d]+\b/,"class-name":/\b[A-Z]\w*\b/,namespace:{pattern:/(?:\b[a-z][a-z_\d]*\s*::\s*)*\b[a-z][a-z_\d]*\s*::(?!\s*<)/,inside:{punctuation:/::/}},number:/\b(?:0x[\dA-Fa-f](?:_?[\dA-Fa-f])*|0o[0-7](?:_?[0-7])*|0b[01](?:_?[01])*|(?:(?:\d(?:_?\d)*)?\.)?\d(?:_?\d)*(?:[Ee][+-]?\d+)?)(?:_?(?:f32|f64|[iu](?:8|16|32|64|size)?))?\b/,boolean:/\b(?:false|true)\b/,punctuation:/->|\.\.=|\.{1,3}|::|[{}[\];(),:]/,operator:/[-+*\/%!^]=?|=[=>]?|&[&=]?|\|[|=]?|<<?=?|>>?=?|[@?]/},e.languages.rust["closure-params"].inside.rest=e.languages.rust,e.languages.rust.attribute.inside.string=e.languages.rust.string}(T),T.languages.go=T.languages.extend("clike",{string:{pattern:/(^|[^\\])"(?:\\.|[^"\\\r\n])*"|`[^`]*`/,lookbehind:!0,greedy:!0},keyword:/\b(?:break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(?:to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,boolean:/\b(?:_|false|iota|nil|true)\b/,number:[/\b0(?:b[01_]+|o[0-7_]+)i?\b/i,/\b0x(?:[a-f\d_]+(?:\.[a-f\d_]*)?|\.[a-f\d_]+)(?:p[+-]?\d+(?:_\d+)*)?i?(?!\w)/i,/(?:\b\d[\d_]*(?:\.[\d_]*)?|\B\.\d[\d_]*)(?:e[+-]?[\d_]+)?i?(?!\w)/i],operator:/[*\/%^!=]=?|\+[=+]?|-[=-]?|\|[=|]?|&(?:=|&|\^=?)?|>(?:>=?|=)?|<(?:<=?|=|-)?|:=|\.\.\./,builtin:/\b(?:append|bool|byte|cap|close|complex|complex(?:64|128)|copy|delete|error|float(?:32|64)|u?int(?:8|16|32|64)?|imag|len|make|new|panic|print(?:ln)?|real|recover|rune|string|uintptr)\b/}),T.languages.insertBefore("go","string",{char:{pattern:/'(?:\\.|[^'\\\r\n]){0,10}'/,greedy:!0}}),delete T.languages.go["class-name"],function(e){var t=/\b(?:alignas|alignof|asm|auto|bool|break|case|catch|char|char16_t|char32_t|char8_t|class|co_await|co_return|co_yield|compl|concept|const|const_cast|consteval|constexpr|constinit|continue|decltype|default|delete|do|double|dynamic_cast|else|enum|explicit|export|extern|final|float|for|friend|goto|if|import|inline|int|int16_t|int32_t|int64_t|int8_t|long|module|mutable|namespace|new|noexcept|nullptr|operator|override|private|protected|public|register|reinterpret_cast|requires|return|short|signed|sizeof|static|static_assert|static_cast|struct|switch|template|this|thread_local|throw|try|typedef|typeid|typename|uint16_t|uint32_t|uint64_t|uint8_t|union|unsigned|using|virtual|void|volatile|wchar_t|while)\b/,n=/\b(?!<keyword>)\w+(?:\s*\.\s*\w+)*\b/.source.replace(/<keyword>/g,(function(){return t.source}));e.languages.cpp=e.languages.extend("c",{"class-name":[{pattern:RegExp(/(\b(?:class|concept|enum|struct|typename)\s+)(?!<keyword>)\w+/.source.replace(/<keyword>/g,(function(){return t.source}))),lookbehind:!0},/\b[A-Z]\w*(?=\s*::\s*\w+\s*\()/,/\b[A-Z_]\w*(?=\s*::\s*~\w+\s*\()/i,/\b\w+(?=\s*<(?:[^<>]|<(?:[^<>]|<[^<>]*>)*>)*>\s*::\s*\w+\s*\()/],keyword:t,number:{pattern:/(?:\b0b[01']+|\b0x(?:[\da-f']+(?:\.[\da-f']*)?|\.[\da-f']+)(?:p[+-]?[\d']+)?|(?:\b[\d']+(?:\.[\d']*)?|\B\.[\d']+)(?:e[+-]?[\d']+)?)[ful]{0,4}/i,greedy:!0},operator:/>>=?|<<=?|->|--|\+\+|&&|\|\||[?:~]|<=>|[-+*/%&|^!=<>]=?|\b(?:and|and_eq|bitand|bitor|not|not_eq|or|or_eq|xor|xor_eq)\b/,boolean:/\b(?:false|true)\b/}),e.languages.insertBefore("cpp","string",{module:{pattern:RegExp(/(\b(?:import|module)\s+)/.source+"(?:"+/"(?:\\(?:\r\n|[\s\S])|[^"\\\r\n])*"|<[^<>\r\n]*>/.source+"|"+/<mod-name>(?:\s*:\s*<mod-name>)?|:\s*<mod-name>/.source.replace(/<mod-name>/g,(function(){return n}))+")"),lookbehind:!0,greedy:!0,inside:{string:/^[<"][\s\S]+/,operator:/:/,punctuation:/\./}},"raw-string":{pattern:/R"([^()\\ ]{0,16})\([\s\S]*?\)\1"/,alias:"string",greedy:!0}}),e.languages.insertBefore("cpp","keyword",{"generic-function":{pattern:/\b(?!operator\b)[a-z_]\w*\s*<(?:[^<>]|<[^<>]*>)*>(?=\s*\()/i,inside:{function:/^\w+/,generic:{pattern:/<[\s\S]+/,alias:"class-name",inside:e.languages.cpp}}}}),e.languages.insertBefore("cpp","operator",{"double-colon":{pattern:/::/,alias:"punctuation"}}),e.languages.insertBefore("cpp","class-name",{"base-clause":{pattern:/(\b(?:class|struct)\s+\w+\s*:\s*)[^;{}"'\s]+(?:\s+[^;{}"'\s]+)*(?=\s*[;{])/,lookbehind:!0,greedy:!0,inside:e.languages.extend("cpp",{})}}),e.languages.insertBefore("inside","double-colon",{"class-name":/\b[a-z_]\w*\b(?!\s*::)/i},e.languages.cpp["base-clause"])}(T),T.languages.python={comment:{pattern:/(^|[^\\])#.*/,lookbehind:!0,greedy:!0},"string-interpolation":{pattern:/(?:f|fr|rf)(?:("""|''')[\s\S]*?\1|("|')(?:\\.|(?!\2)[^\\\r\n])*\2)/i,greedy:!0,inside:{interpolation:{pattern:/((?:^|[^{])(?:\{\{)*)\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}]|\{(?!\{)(?:[^{}])+\})+\})+\}/,lookbehind:!0,inside:{"format-spec":{pattern:/(:)[^:(){}]+(?=\}$)/,lookbehind:!0},"conversion-option":{pattern:/![sra](?=[:}]$)/,alias:"punctuation"},rest:null}},string:/[\s\S]+/}},"triple-quoted-string":{pattern:/(?:[rub]|br|rb)?("""|''')[\s\S]*?\1/i,greedy:!0,alias:"string"},string:{pattern:/(?:[rub]|br|rb)?("|')(?:\\.|(?!\1)[^\\\r\n])*\1/i,greedy:!0},function:{pattern:/((?:^|\s)def[ \t]+)[a-zA-Z_]\w*(?=\s*\()/g,lookbehind:!0},"class-name":{pattern:/(\bclass\s+)\w+/i,lookbehind:!0},decorator:{pattern:/(^[\t ]*)@\w+(?:\.\w+)*/m,lookbehind:!0,alias:["annotation","punctuation"],inside:{punctuation:/\./}},keyword:/\b(?:_(?=\s*:)|and|as|assert|async|await|break|case|class|continue|def|del|elif|else|except|exec|finally|for|from|global|if|import|in|is|lambda|match|nonlocal|not|or|pass|print|raise|return|try|while|with|yield)\b/,builtin:/\b(?:__import__|abs|all|any|apply|ascii|basestring|bin|bool|buffer|bytearray|bytes|callable|chr|classmethod|cmp|coerce|compile|complex|delattr|dict|dir|divmod|enumerate|eval|execfile|file|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|intern|isinstance|issubclass|iter|len|list|locals|long|map|max|memoryview|min|next|object|oct|open|ord|pow|property|range|raw_input|reduce|reload|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|unichr|unicode|vars|xrange|zip)\b/,boolean:/\b(?:False|None|True)\b/,number:/\b0(?:b(?:_?[01])+|o(?:_?[0-7])+|x(?:_?[a-f0-9])+)\b|(?:\b\d+(?:_\d+)*(?:\.(?:\d+(?:_\d+)*)?)?|\B\.\d+(?:_\d+)*)(?:e[+-]?\d+(?:_\d+)*)?j?(?!\w)/i,operator:/[-+%=]=?|!=|:=|\*\*?=?|\/\/?=?|<[<=>]?|>[=>]?|[&|^~]/,punctuation:/[{}[\];(),.:]/},T.languages.python["string-interpolation"].inside.interpolation.inside.rest=T.languages.python,T.languages.py=T.languages.python;((e,t)=>{for(var n in t)f(e,n,{get:t[n],enumerable:!0})})({},{dracula:()=>j,duotoneDark:()=>A,duotoneLight:()=>L,github:()=>N,jettwaveDark:()=>H,jettwaveLight:()=>Z,nightOwl:()=>R,nightOwlLight:()=>P,oceanicNext:()=>I,okaidia:()=>F,oneDark:()=>G,oneLight:()=>V,palenight:()=>M,shadesOfPurple:()=>z,synthwave84:()=>B,ultramin:()=>$,vsDark:()=>U,vsLight:()=>q});var j={plain:{color:"#F8F8F2",backgroundColor:"#282A36"},styles:[{types:["prolog","constant","builtin"],style:{color:"rgb(189, 147, 249)"}},{types:["inserted","function"],style:{color:"rgb(80, 250, 123)"}},{types:["deleted"],style:{color:"rgb(255, 85, 85)"}},{types:["changed"],style:{color:"rgb(255, 184, 108)"}},{types:["punctuation","symbol"],style:{color:"rgb(248, 248, 242)"}},{types:["string","char","tag","selector"],style:{color:"rgb(255, 121, 198)"}},{types:["keyword","variable"],style:{color:"rgb(189, 147, 249)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(98, 114, 164)"}},{types:["attr-name"],style:{color:"rgb(241, 250, 140)"}}]},A={plain:{backgroundColor:"#2a2734",color:"#9a86fd"},styles:[{types:["comment","prolog","doctype","cdata","punctuation"],style:{color:"#6c6783"}},{types:["namespace"],style:{opacity:.7}},{types:["tag","operator","number"],style:{color:"#e09142"}},{types:["property","function"],style:{color:"#9a86fd"}},{types:["tag-id","selector","atrule-id"],style:{color:"#eeebff"}},{types:["attr-name"],style:{color:"#c4b9fe"}},{types:["boolean","string","entity","url","attr-value","keyword","control","directive","unit","statement","regex","atrule","placeholder","variable"],style:{color:"#ffcc99"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"#c4b9fe"}}]},L={plain:{backgroundColor:"#faf8f5",color:"#728fcb"},styles:[{types:["comment","prolog","doctype","cdata","punctuation"],style:{color:"#b6ad9a"}},{types:["namespace"],style:{opacity:.7}},{types:["tag","operator","number"],style:{color:"#063289"}},{types:["property","function"],style:{color:"#b29762"}},{types:["tag-id","selector","atrule-id"],style:{color:"#2d2006"}},{types:["attr-name"],style:{color:"#896724"}},{types:["boolean","string","entity","url","attr-value","keyword","control","directive","unit","statement","regex","atrule"],style:{color:"#728fcb"}},{types:["placeholder","variable"],style:{color:"#93abdc"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"#896724"}}]},N={plain:{color:"#393A34",backgroundColor:"#f6f8fa"},styles:[{types:["comment","prolog","doctype","cdata"],style:{color:"#999988",fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}},{types:["string","attr-value"],style:{color:"#e3116c"}},{types:["punctuation","operator"],style:{color:"#393A34"}},{types:["entity","url","symbol","number","boolean","variable","constant","property","regex","inserted"],style:{color:"#36acaa"}},{types:["atrule","keyword","attr-name","selector"],style:{color:"#00a4db"}},{types:["function","deleted","tag"],style:{color:"#d73a49"}},{types:["function-variable"],style:{color:"#6f42c1"}},{types:["tag","selector","keyword"],style:{color:"#00009f"}}]},R={plain:{color:"#d6deeb",backgroundColor:"#011627"},styles:[{types:["changed"],style:{color:"rgb(162, 191, 252)",fontStyle:"italic"}},{types:["deleted"],style:{color:"rgba(239, 83, 80, 0.56)",fontStyle:"italic"}},{types:["inserted","attr-name"],style:{color:"rgb(173, 219, 103)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(99, 119, 119)",fontStyle:"italic"}},{types:["string","url"],style:{color:"rgb(173, 219, 103)"}},{types:["variable"],style:{color:"rgb(214, 222, 235)"}},{types:["number"],style:{color:"rgb(247, 140, 108)"}},{types:["builtin","char","constant","function"],style:{color:"rgb(130, 170, 255)"}},{types:["punctuation"],style:{color:"rgb(199, 146, 234)"}},{types:["selector","doctype"],style:{color:"rgb(199, 146, 234)",fontStyle:"italic"}},{types:["class-name"],style:{color:"rgb(255, 203, 139)"}},{types:["tag","operator","keyword"],style:{color:"rgb(127, 219, 202)"}},{types:["boolean"],style:{color:"rgb(255, 88, 116)"}},{types:["property"],style:{color:"rgb(128, 203, 196)"}},{types:["namespace"],style:{color:"rgb(178, 204, 214)"}}]},P={plain:{color:"#403f53",backgroundColor:"#FBFBFB"},styles:[{types:["changed"],style:{color:"rgb(162, 191, 252)",fontStyle:"italic"}},{types:["deleted"],style:{color:"rgba(239, 83, 80, 0.56)",fontStyle:"italic"}},{types:["inserted","attr-name"],style:{color:"rgb(72, 118, 214)",fontStyle:"italic"}},{types:["comment"],style:{color:"rgb(152, 159, 177)",fontStyle:"italic"}},{types:["string","builtin","char","constant","url"],style:{color:"rgb(72, 118, 214)"}},{types:["variable"],style:{color:"rgb(201, 103, 101)"}},{types:["number"],style:{color:"rgb(170, 9, 130)"}},{types:["punctuation"],style:{color:"rgb(153, 76, 195)"}},{types:["function","selector","doctype"],style:{color:"rgb(153, 76, 195)",fontStyle:"italic"}},{types:["class-name"],style:{color:"rgb(17, 17, 17)"}},{types:["tag"],style:{color:"rgb(153, 76, 195)"}},{types:["operator","property","keyword","namespace"],style:{color:"rgb(12, 150, 155)"}},{types:["boolean"],style:{color:"rgb(188, 84, 84)"}}]},O="#c5a5c5",D="#8dc891",I={plain:{backgroundColor:"#282c34",color:"#ffffff"},styles:[{types:["attr-name"],style:{color:O}},{types:["attr-value"],style:{color:D}},{types:["comment","block-comment","prolog","doctype","cdata","shebang"],style:{color:"#999999"}},{types:["property","number","function-name","constant","symbol","deleted"],style:{color:"#5a9bcf"}},{types:["boolean"],style:{color:"#ff8b50"}},{types:["tag"],style:{color:"#fc929e"}},{types:["string"],style:{color:D}},{types:["punctuation"],style:{color:D}},{types:["selector","char","builtin","inserted"],style:{color:"#D8DEE9"}},{types:["function"],style:{color:"#79b6f2"}},{types:["operator","entity","url","variable"],style:{color:"#d7deea"}},{types:["keyword"],style:{color:O}},{types:["atrule","class-name"],style:{color:"#FAC863"}},{types:["important"],style:{fontWeight:"400"}},{types:["bold"],style:{fontWeight:"bold"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["namespace"],style:{opacity:.7}}]},F={plain:{color:"#f8f8f2",backgroundColor:"#272822"},styles:[{types:["changed"],style:{color:"rgb(162, 191, 252)",fontStyle:"italic"}},{types:["deleted"],style:{color:"#f92672",fontStyle:"italic"}},{types:["inserted"],style:{color:"rgb(173, 219, 103)",fontStyle:"italic"}},{types:["comment"],style:{color:"#8292a2",fontStyle:"italic"}},{types:["string","url"],style:{color:"#a6e22e"}},{types:["variable"],style:{color:"#f8f8f2"}},{types:["number"],style:{color:"#ae81ff"}},{types:["builtin","char","constant","function","class-name"],style:{color:"#e6db74"}},{types:["punctuation"],style:{color:"#f8f8f2"}},{types:["selector","doctype"],style:{color:"#a6e22e",fontStyle:"italic"}},{types:["tag","operator","keyword"],style:{color:"#66d9ef"}},{types:["boolean"],style:{color:"#ae81ff"}},{types:["namespace"],style:{color:"rgb(178, 204, 214)",opacity:.7}},{types:["tag","property"],style:{color:"#f92672"}},{types:["attr-name"],style:{color:"#a6e22e !important"}},{types:["doctype"],style:{color:"#8292a2"}},{types:["rule"],style:{color:"#e6db74"}}]},M={plain:{color:"#bfc7d5",backgroundColor:"#292d3e"},styles:[{types:["comment"],style:{color:"rgb(105, 112, 152)",fontStyle:"italic"}},{types:["string","inserted"],style:{color:"rgb(195, 232, 141)"}},{types:["number"],style:{color:"rgb(247, 140, 108)"}},{types:["builtin","char","constant","function"],style:{color:"rgb(130, 170, 255)"}},{types:["punctuation","selector"],style:{color:"rgb(199, 146, 234)"}},{types:["variable"],style:{color:"rgb(191, 199, 213)"}},{types:["class-name","attr-name"],style:{color:"rgb(255, 203, 107)"}},{types:["tag","deleted"],style:{color:"rgb(255, 85, 114)"}},{types:["operator"],style:{color:"rgb(137, 221, 255)"}},{types:["boolean"],style:{color:"rgb(255, 88, 116)"}},{types:["keyword"],style:{fontStyle:"italic"}},{types:["doctype"],style:{color:"rgb(199, 146, 234)",fontStyle:"italic"}},{types:["namespace"],style:{color:"rgb(178, 204, 214)"}},{types:["url"],style:{color:"rgb(221, 221, 221)"}}]},z={plain:{color:"#9EFEFF",backgroundColor:"#2D2A55"},styles:[{types:["changed"],style:{color:"rgb(255, 238, 128)"}},{types:["deleted"],style:{color:"rgba(239, 83, 80, 0.56)"}},{types:["inserted"],style:{color:"rgb(173, 219, 103)"}},{types:["comment"],style:{color:"rgb(179, 98, 255)",fontStyle:"italic"}},{types:["punctuation"],style:{color:"rgb(255, 255, 255)"}},{types:["constant"],style:{color:"rgb(255, 98, 140)"}},{types:["string","url"],style:{color:"rgb(165, 255, 144)"}},{types:["variable"],style:{color:"rgb(255, 238, 128)"}},{types:["number","boolean"],style:{color:"rgb(255, 98, 140)"}},{types:["attr-name"],style:{color:"rgb(255, 180, 84)"}},{types:["keyword","operator","property","namespace","tag","selector","doctype"],style:{color:"rgb(255, 157, 0)"}},{types:["builtin","char","constant","function","class-name"],style:{color:"rgb(250, 208, 0)"}}]},B={plain:{backgroundColor:"linear-gradient(to bottom, #2a2139 75%, #34294f)",backgroundImage:"#34294f",color:"#f92aad",textShadow:"0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3"},styles:[{types:["comment","block-comment","prolog","doctype","cdata"],style:{color:"#495495",fontStyle:"italic"}},{types:["punctuation"],style:{color:"#ccc"}},{types:["tag","attr-name","namespace","number","unit","hexcode","deleted"],style:{color:"#e2777a"}},{types:["property","selector"],style:{color:"#72f1b8",textShadow:"0 0 2px #100c0f, 0 0 10px #257c5575, 0 0 35px #21272475"}},{types:["function-name"],style:{color:"#6196cc"}},{types:["boolean","selector-id","function"],style:{color:"#fdfdfd",textShadow:"0 0 2px #001716, 0 0 3px #03edf975, 0 0 5px #03edf975, 0 0 8px #03edf975"}},{types:["class-name","maybe-class-name","builtin"],style:{color:"#fff5f6",textShadow:"0 0 2px #000, 0 0 10px #fc1f2c75, 0 0 5px #fc1f2c75, 0 0 25px #fc1f2c75"}},{types:["constant","symbol"],style:{color:"#f92aad",textShadow:"0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3"}},{types:["important","atrule","keyword","selector-class"],style:{color:"#f4eee4",textShadow:"0 0 2px #393a33, 0 0 8px #f39f0575, 0 0 2px #f39f0575"}},{types:["string","char","attr-value","regex","variable"],style:{color:"#f87c32"}},{types:["parameter"],style:{fontStyle:"italic"}},{types:["entity","url"],style:{color:"#67cdcc"}},{types:["operator"],style:{color:"ffffffee"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["entity"],style:{cursor:"help"}},{types:["inserted"],style:{color:"green"}}]},$={plain:{color:"#282a2e",backgroundColor:"#ffffff"},styles:[{types:["comment"],style:{color:"rgb(197, 200, 198)"}},{types:["string","number","builtin","variable"],style:{color:"rgb(150, 152, 150)"}},{types:["class-name","function","tag","attr-name"],style:{color:"rgb(40, 42, 46)"}}]},U={plain:{color:"#9CDCFE",backgroundColor:"#1E1E1E"},styles:[{types:["prolog"],style:{color:"rgb(0, 0, 128)"}},{types:["comment"],style:{color:"rgb(106, 153, 85)"}},{types:["builtin","changed","keyword","interpolation-punctuation"],style:{color:"rgb(86, 156, 214)"}},{types:["number","inserted"],style:{color:"rgb(181, 206, 168)"}},{types:["constant"],style:{color:"rgb(100, 102, 149)"}},{types:["attr-name","variable"],style:{color:"rgb(156, 220, 254)"}},{types:["deleted","string","attr-value","template-punctuation"],style:{color:"rgb(206, 145, 120)"}},{types:["selector"],style:{color:"rgb(215, 186, 125)"}},{types:["tag"],style:{color:"rgb(78, 201, 176)"}},{types:["tag"],languages:["markup"],style:{color:"rgb(86, 156, 214)"}},{types:["punctuation","operator"],style:{color:"rgb(212, 212, 212)"}},{types:["punctuation"],languages:["markup"],style:{color:"#808080"}},{types:["function"],style:{color:"rgb(220, 220, 170)"}},{types:["class-name"],style:{color:"rgb(78, 201, 176)"}},{types:["char"],style:{color:"rgb(209, 105, 105)"}}]},q={plain:{color:"#000000",backgroundColor:"#ffffff"},styles:[{types:["comment"],style:{color:"rgb(0, 128, 0)"}},{types:["builtin"],style:{color:"rgb(0, 112, 193)"}},{types:["number","variable","inserted"],style:{color:"rgb(9, 134, 88)"}},{types:["operator"],style:{color:"rgb(0, 0, 0)"}},{types:["constant","char"],style:{color:"rgb(129, 31, 63)"}},{types:["tag"],style:{color:"rgb(128, 0, 0)"}},{types:["attr-name"],style:{color:"rgb(255, 0, 0)"}},{types:["deleted","string"],style:{color:"rgb(163, 21, 21)"}},{types:["changed","punctuation"],style:{color:"rgb(4, 81, 165)"}},{types:["function","keyword"],style:{color:"rgb(0, 0, 255)"}},{types:["class-name"],style:{color:"rgb(38, 127, 153)"}}]},H={plain:{color:"#f8fafc",backgroundColor:"#011627"},styles:[{types:["prolog"],style:{color:"#000080"}},{types:["comment"],style:{color:"#6A9955"}},{types:["builtin","changed","keyword","interpolation-punctuation"],style:{color:"#569CD6"}},{types:["number","inserted"],style:{color:"#B5CEA8"}},{types:["constant"],style:{color:"#f8fafc"}},{types:["attr-name","variable"],style:{color:"#9CDCFE"}},{types:["deleted","string","attr-value","template-punctuation"],style:{color:"#cbd5e1"}},{types:["selector"],style:{color:"#D7BA7D"}},{types:["tag"],style:{color:"#0ea5e9"}},{types:["tag"],languages:["markup"],style:{color:"#0ea5e9"}},{types:["punctuation","operator"],style:{color:"#D4D4D4"}},{types:["punctuation"],languages:["markup"],style:{color:"#808080"}},{types:["function"],style:{color:"#7dd3fc"}},{types:["class-name"],style:{color:"#0ea5e9"}},{types:["char"],style:{color:"#D16969"}}]},Z={plain:{color:"#0f172a",backgroundColor:"#f1f5f9"},styles:[{types:["prolog"],style:{color:"#000080"}},{types:["comment"],style:{color:"#6A9955"}},{types:["builtin","changed","keyword","interpolation-punctuation"],style:{color:"#0c4a6e"}},{types:["number","inserted"],style:{color:"#B5CEA8"}},{types:["constant"],style:{color:"#0f172a"}},{types:["attr-name","variable"],style:{color:"#0c4a6e"}},{types:["deleted","string","attr-value","template-punctuation"],style:{color:"#64748b"}},{types:["selector"],style:{color:"#D7BA7D"}},{types:["tag"],style:{color:"#0ea5e9"}},{types:["tag"],languages:["markup"],style:{color:"#0ea5e9"}},{types:["punctuation","operator"],style:{color:"#475569"}},{types:["punctuation"],languages:["markup"],style:{color:"#808080"}},{types:["function"],style:{color:"#0e7490"}},{types:["class-name"],style:{color:"#0ea5e9"}},{types:["char"],style:{color:"#D16969"}}]},G={plain:{backgroundColor:"hsl(220, 13%, 18%)",color:"hsl(220, 14%, 71%)",textShadow:"0 1px rgba(0, 0, 0, 0.3)"},styles:[{types:["comment","prolog","cdata"],style:{color:"hsl(220, 10%, 40%)"}},{types:["doctype","punctuation","entity"],style:{color:"hsl(220, 14%, 71%)"}},{types:["attr-name","class-name","maybe-class-name","boolean","constant","number","atrule"],style:{color:"hsl(29, 54%, 61%)"}},{types:["keyword"],style:{color:"hsl(286, 60%, 67%)"}},{types:["property","tag","symbol","deleted","important"],style:{color:"hsl(355, 65%, 65%)"}},{types:["selector","string","char","builtin","inserted","regex","attr-value"],style:{color:"hsl(95, 38%, 62%)"}},{types:["variable","operator","function"],style:{color:"hsl(207, 82%, 66%)"}},{types:["url"],style:{color:"hsl(187, 47%, 55%)"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"hsl(220, 14%, 71%)"}}]},V={plain:{backgroundColor:"hsl(230, 1%, 98%)",color:"hsl(230, 8%, 24%)"},styles:[{types:["comment","prolog","cdata"],style:{color:"hsl(230, 4%, 64%)"}},{types:["doctype","punctuation","entity"],style:{color:"hsl(230, 8%, 24%)"}},{types:["attr-name","class-name","boolean","constant","number","atrule"],style:{color:"hsl(35, 99%, 36%)"}},{types:["keyword"],style:{color:"hsl(301, 63%, 40%)"}},{types:["property","tag","symbol","deleted","important"],style:{color:"hsl(5, 74%, 59%)"}},{types:["selector","string","char","builtin","inserted","regex","attr-value","punctuation"],style:{color:"hsl(119, 34%, 47%)"}},{types:["variable","operator","function"],style:{color:"hsl(221, 87%, 60%)"}},{types:["url"],style:{color:"hsl(198, 99%, 37%)"}},{types:["deleted"],style:{textDecorationLine:"line-through"}},{types:["inserted"],style:{textDecorationLine:"underline"}},{types:["italic"],style:{fontStyle:"italic"}},{types:["important","bold"],style:{fontWeight:"bold"}},{types:["important"],style:{color:"hsl(230, 8%, 24%)"}}]},W=(e,t)=>{const{plain:n}=e,r=e.styles.reduce(((e,n)=>{const{languages:r,style:o}=n;return r&&!r.includes(t)||n.types.forEach((t=>{const n=S(S({},e[t]),o);e[t]=n})),e}),{});return r.root=n,r.plain=E(S({},n),{backgroundColor:void 0}),r},Q=/\r\n|\r|\n/,K=e=>{0===e.length?e.push({types:["plain"],content:"\n",empty:!0}):1===e.length&&""===e[0].content&&(e[0].content="\n",e[0].empty=!0)},Y=(e,t)=>{const n=e.length;return n>0&&e[n-1]===t?e:e.concat(t)},X=e=>{const t=[[]],n=[e],r=[0],o=[e.length];let a=0,i=0,l=[];const s=[l];for(;i>-1;){for(;(a=r[i]++)<o[i];){let e,c=t[i];const u=n[i][a];if("string"==typeof u?(c=i>0?c:["plain"],e=u):(c=Y(c,u.type),u.alias&&(c=Y(c,u.alias)),e=u.content),"string"!=typeof e){i++,t.push(c),n.push(e),r.push(0),o.push(e.length);continue}const d=e.split(Q),p=d.length;l.push({types:c,content:d[0]});for(let t=1;t<p;t++)K(l),s.push(l=[]),l.push({types:c,content:d[t]})}i--,t.pop(),n.pop(),r.pop(),o.pop()}return K(l),s},J=({children:e,language:t,code:n,theme:r,prism:o})=>{const a=t.toLowerCase(),i=((e,t)=>{const[n,r]=(0,u.useState)(W(t,e)),o=(0,u.useRef)(),a=(0,u.useRef)();return(0,u.useEffect)((()=>{t===o.current&&e===a.current||(o.current=t,a.current=e,r(W(t,e)))}),[e,t]),n})(a,r),l=(e=>(0,u.useCallback)((t=>{var n=t,{className:r,style:o,line:a}=n,i=_(n,["className","style","line"]);const l=E(S({},i),{className:(0,d.Z)("token-line",r)});return"object"==typeof e&&"plain"in e&&(l.style=e.plain),"object"==typeof o&&(l.style=S(S({},l.style||{}),o)),l}),[e]))(i),s=(e=>{const t=(0,u.useCallback)((({types:t,empty:n})=>{if(null!=e)return 1===t.length&&"plain"===t[0]?null!=n?{display:"inline-block"}:void 0:1===t.length&&null!=n?e[t[0]]:Object.assign(null!=n?{display:"inline-block"}:{},...t.map((t=>e[t])))}),[e]);return(0,u.useCallback)((e=>{var n=e,{token:r,className:o,style:a}=n,i=_(n,["token","className","style"]);const l=E(S({},i),{className:(0,d.Z)("token",...r.types,o),children:r.content,style:t(r)});return null!=a&&(l.style=S(S({},l.style||{}),a)),l}),[t])})(i),c=(({prism:e,code:t,grammar:n,language:r})=>{const o=(0,u.useRef)(e);return(0,u.useMemo)((()=>{if(null==n)return X([t]);const e={code:t,grammar:n,language:r,tokens:[]};return o.current.hooks.run("before-tokenize",e),e.tokens=o.current.tokenize(t,n),o.current.hooks.run("after-tokenize",e),X(e.tokens)}),[t,n,r])})({prism:o,language:a,code:n,grammar:o.languages[a]});return e({tokens:c,className:`prism-code language-${a}`,style:null!=i?i.root:{},getLineProps:l,getTokenProps:s})},ee=e=>(0,u.createElement)(J,E(S({},e),{prism:e.prism||T,theme:e.theme||U,code:e.code,language:e.language}))},8776:(e,t,n)=>{"use strict";n.d(t,{Z:()=>a});var r=!0,o="Invariant failed";function a(e,t){if(!e){if(r)throw new Error(o);var n="function"==typeof t?t():t,a=n?"".concat(o,": ").concat(n):o;throw new Error(a)}}},7529:e=>{"use strict";e.exports={}},6887:e=>{"use strict";e.exports=JSON.parse('{"/blog-1ae":{"__comp":"a6aa9e1f","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","items":[{"content":"0a2c82fb"},{"content":"7661071f"},{"content":"f4f34a3a"},{"content":"8717b14a"},{"content":"925b3f96"}],"metadata":"b2b675dd"},"/blog/2024/01/28/welcome-to-docusaurus-static-fb0":{"__comp":"ccc49370","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","content":"3660216b"},"/blog/archive-c5b":{"__comp":"9e4087bc","__context":{"plugin":"b23f3d07"},"archive":"b2f554cd"},"/blog/first-blog-post-3c5":{"__comp":"ccc49370","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","content":"e273c56f"},"/blog/long-blog-post-d8b":{"__comp":"ccc49370","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","content":"73664a40"},"/blog/mdx-blog-post-2ec":{"__comp":"ccc49370","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","content":"59362658"},"/blog/tags-8e7":{"__comp":"01a85c17","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","tags":"a7023ddc"},"/blog/tags/docusaurus-85a":{"__comp":"6875c492","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","items":[{"content":"7661071f"},{"content":"f4f34a3a"},{"content":"8717b14a"},{"content":"925b3f96"}],"tag":"a80da1cf","listMetadata":"608ae6a4"},"/blog/tags/docusaurus-static-057":{"__comp":"6875c492","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","items":[{"content":"0a2c82fb"}],"tag":"a5758fe9","listMetadata":"4604b011"},"/blog/tags/facebook-045":{"__comp":"6875c492","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","items":[{"content":"7661071f"}],"tag":"031793e1","listMetadata":"096bfee4"},"/blog/tags/hello-e34":{"__comp":"6875c492","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","items":[{"content":"0a2c82fb"},{"content":"7661071f"},{"content":"8717b14a"}],"tag":"30a24c52","listMetadata":"66406991"},"/blog/tags/hola-e26":{"__comp":"6875c492","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","items":[{"content":"925b3f96"}],"tag":"e16015ca","listMetadata":"4c9e35b1"},"/blog/welcome-d6d":{"__comp":"ccc49370","__context":{"plugin":"b23f3d07"},"sidebar":"814f3328","content":"d9f32620"},"/markdown-page-567":{"__comp":"1f391b9e","__context":{"plugin":"4cd5e786"},"content":"393be207"},"/docs-2ed":{"__comp":"5e95c892","__context":{"plugin":"46bbbc4b"}},"/docs-be7":{"__comp":"a7bd4aaa","version":"935f2afb"},"/docs-882":{"__comp":"a94703ab"},"/docs/about-99a":{"__comp":"17896441","content":"3d8d21df"},"/docs/appendixa-ba3":{"__comp":"17896441","content":"5f4ff9a9"},"/docs/appendixb-27f":{"__comp":"17896441","content":"c7687382"},"/docs/appendixc-512":{"__comp":"17896441","content":"7feddd0b"},"/docs/appendixd-cad":{"__comp":"17896441","content":"7c7b5a9b"},"/docs/appendixe-af4":{"__comp":"17896441","content":"21d68595"},"/docs/changelog-fe5":{"__comp":"17896441","content":"9beb87c2"},"/docs/configuration/authenticationbackends-6e3":{"__comp":"17896441","content":"20598c5e"},"/docs/configuration/configuremessagingrepositorywebui-f84":{"__comp":"17896441","content":"08c34d24"},"/docs/configuration/configurewebui-3e5":{"__comp":"17896441","content":"3262a34b"},"/docs/configuration/groupassignmentbyinput-7db":{"__comp":"17896441","content":"867bb2d0"},"/docs/configuration/groupassignmentusingauth-301":{"__comp":"17896441","content":"ce40d3b6"},"/docs/configuration/groupassignmentusingclientcerts-cae":{"__comp":"17896441","content":"e3eb95dd"},"/docs/configuration/groupfiltering-b22":{"__comp":"17896441","content":"062c1b27"},"/docs/configuration/optionallydisableui-3a8":{"__comp":"17896441","content":"8838a7ac"},"/docs/configuration/overview-99e":{"__comp":"17896441","content":"0f425520"},"/docs/configuration/vbmadminconfig-c39":{"__comp":"17896441","content":"b1e73d9b"},"/docs/dataretentiontool-a6a":{"__comp":"17896441","content":"30688810"},"/docs/deviceprofiles-a69":{"__comp":"17896441","content":"3682177e"},"/docs/dockerinstall/buildinstall-56e":{"__comp":"17896441","content":"5817ad8d"},"/docs/dockerinstall/ironbank-52c":{"__comp":"17896441","content":"ee07fdd2"},"/docs/federation/datapackagemissionfileblocker-35a":{"__comp":"17896441","content":"b31dded6"},"/docs/federation/enablefederation-c3a":{"__comp":"17896441","content":"c451c5ed"},"/docs/federation/federatedgroupmapping-d22":{"__comp":"17896441","content":"1b9776b6"},"/docs/federation/federationdisruptiontolerance-6bc":{"__comp":"17896441","content":"8c65a213"},"/docs/federation/federationexample-67f":{"__comp":"17896441","content":"e54870e4"},"/docs/federation/maketheconnection-5a3":{"__comp":"17896441","content":"25f60d21"},"/docs/federation/overview-847":{"__comp":"17896441","content":"17495a52"},"/docs/federation/uploadfederatecert-ea8":{"__comp":"17896441","content":"4e810807"},"/docs/firewall/overview-f8f":{"__comp":"17896441","content":"55d8b2aa"},"/docs/firewall/rhelrockycentos-b08":{"__comp":"17896441","content":"bb1b26fa"},"/docs/firewall/ubunturaspberrypi-c94":{"__comp":"17896441","content":"93acc585"},"/docs/groupfilteringformulticast-f11":{"__comp":"17896441","content":"f555bcff"},"/docs/imagetest-0d6":{"__comp":"17896441","content":"24dd378c"},"/docs/installation/oneserver/prerequisite-2a7":{"__comp":"17896441","content":"3e56fa0f"},"/docs/installation/oneserver/takserverconfiguration-ccf":{"__comp":"17896441","content":"89d39a78"},"/docs/installation/oneserver/takserverinstallation-338":{"__comp":"17896441","content":"d43d2919"},"/docs/installation/overview-2d7":{"__comp":"17896441","content":"cfff6e91"},"/docs/installation/setup_wizard-372":{"__comp":"17896441","content":"00483006"},"/docs/installation/twoserver/overview-5ad":{"__comp":"17896441","content":"642e9f94"},"/docs/installation/twoserver/serverone/dependencysetup-7e8":{"__comp":"17896441","content":"4a298a69"},"/docs/installation/twoserver/servertwo/configuretakserver-b42":{"__comp":"17896441","content":"587818ee"},"/docs/installation/twoserver/servertwo/installtakserver-638":{"__comp":"17896441","content":"c92107ec"},"/docs/installation/twoserver/servertwo/prerequisites-69c":{"__comp":"17896441","content":"520e22a5"},"/docs/logging-9cf":{"__comp":"17896441","content":"3abe8fb9"},"/docs/metrics-d90":{"__comp":"17896441","content":"1dba1ecf"},"/docs/oath2authentication-72a":{"__comp":"17896441","content":"2808364d"},"/docs/softwareinstallationlocation-f33":{"__comp":"17896441","content":"da5cb8ba"},"/docs/system-requirements/awsrequirements-9aa":{"__comp":"17896441","content":"09573330"},"/docs/system-requirements/serverrequirements-081":{"__comp":"17896441","content":"4b4e9c91"},"/docs/system-requirements/systemrequirements-1f4":{"__comp":"17896441","content":"995e17fc"},"/docs/upgrade/overview-bbd":{"__comp":"17896441","content":"660a8848"},"/docs/upgrade/singleserver-f22":{"__comp":"17896441","content":"ff1782e3"},"/docs/upgrade/twoserver-b08":{"__comp":"17896441","content":"0a229eeb"},"/docs/usermanagementui-7ab":{"__comp":"17896441","content":"19826855"},"/docs/webtak-a03":{"__comp":"17896441","content":"2c26bb54"},"/-141":{"__comp":"c4f5d8e4","__context":{"plugin":"4cd5e786"},"config":"5e9f5e1a"}}')}},e=>{e.O(0,[532],(()=>{return t=7221,e(e.s=t);var t}));e.O()}]); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/main.633f55f0.js.LICENSE.txt b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/main.633f55f0.js.LICENSE.txt new file mode 100644 index 00000000..91dc8949 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/main.633f55f0.js.LICENSE.txt @@ -0,0 +1,64 @@ +/* NProgress, (c) 2013, 2014 Rico Sta. Cruz - http://ricostacruz.com/nprogress + * @license MIT */ + +/*! Bundled license information: + +prismjs/prism.js: + (** + * Prism: Lightweight, robust, elegant syntax highlighting + * + * @license MIT <https://opensource.org/licenses/MIT> + * @author Lea Verou <https://lea.verou.me> + * @namespace + * @public + *) +*/ + +/** + * @license React + * react-dom.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @license React + * react-jsx-runtime.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @license React + * react.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** + * @license React + * scheduler.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +/** @license React v16.13.1 + * react-is.production.min.js + * + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/runtime~main.2e0bcfb1.js b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/runtime~main.2e0bcfb1.js new file mode 100644 index 00000000..7ab208b3 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/assets/js/runtime~main.2e0bcfb1.js @@ -0,0 +1 @@ +(()=>{"use strict";var e,a,b,f,c,d={},t={};function r(e){var a=t[e];if(void 0!==a)return a.exports;var b=t[e]={id:e,loaded:!1,exports:{}};return d[e].call(b.exports,b,b.exports,r),b.loaded=!0,b.exports}r.m=d,r.c=t,e=[],r.O=(a,b,f,c)=>{if(!b){var d=1/0;for(i=0;i<e.length;i++){b=e[i][0],f=e[i][1],c=e[i][2];for(var t=!0,o=0;o<b.length;o++)(!1&c||d>=c)&&Object.keys(r.O).every((e=>r.O[e](b[o])))?b.splice(o--,1):(t=!1,c<d&&(d=c));if(t){e.splice(i--,1);var n=f();void 0!==n&&(a=n)}}return a}c=c||0;for(var i=e.length;i>0&&e[i-1][2]>c;i--)e[i]=e[i-1];e[i]=[b,f,c]},r.n=e=>{var a=e&&e.__esModule?()=>e.default:()=>e;return r.d(a,{a:a}),a},b=Object.getPrototypeOf?e=>Object.getPrototypeOf(e):e=>e.__proto__,r.t=function(e,f){if(1&f&&(e=this(e)),8&f)return e;if("object"==typeof e&&e){if(4&f&&e.__esModule)return e;if(16&f&&"function"==typeof e.then)return e}var c=Object.create(null);r.r(c);var d={};a=a||[null,b({}),b([]),b(b)];for(var t=2&f&&e;"object"==typeof t&&!~a.indexOf(t);t=b(t))Object.getOwnPropertyNames(t).forEach((a=>d[a]=()=>e[a]));return d.default=()=>e,r.d(c,d),c},r.d=(e,a)=>{for(var b in a)r.o(a,b)&&!r.o(e,b)&&Object.defineProperty(e,b,{enumerable:!0,get:a[b]})},r.f={},r.e=e=>Promise.all(Object.keys(r.f).reduce(((a,b)=>(r.f[b](e,a),a)),[])),r.u=e=>"assets/js/"+({53:"935f2afb",54:"0a2c82fb",60:"e54870e4",80:"9beb87c2",110:"66406991",191:"24dd378c",278:"25f60d21",308:"2c26bb54",453:"30a24c52",533:"b2b675dd",722:"5f4ff9a9",894:"a5758fe9",948:"8717b14a",1071:"b1e73d9b",1477:"b2f554cd",1506:"55d8b2aa",1633:"031793e1",1713:"a7023ddc",1881:"660a8848",1914:"d9f32620",1925:"0a229eeb",2200:"da5cb8ba",2267:"59362658",2362:"e273c56f",2372:"e3eb95dd",2399:"20598c5e",2510:"1dba1ecf",2535:"814f3328",2537:"3682177e",2546:"ee07fdd2",2547:"c7687382",2587:"bb1b26fa",2914:"4b4e9c91",2981:"2808364d",3085:"1f391b9e",3089:"a6aa9e1f",3205:"a80da1cf",3268:"8838a7ac",3310:"46bbbc4b",3354:"d43d2919",3511:"ce40d3b6",3514:"73664a40",3608:"9e4087bc",4013:"01a85c17",4020:"4cd5e786",4195:"c4f5d8e4",4368:"a94703ab",4546:"93acc585",4620:"17495a52",5017:"09573330",5097:"8c65a213",5243:"867bb2d0",5521:"3abe8fb9",5550:"995e17fc",5582:"5817ad8d",6074:"00483006",6103:"ccc49370",6129:"c92107ec",6203:"4a298a69",6317:"30688810",6425:"3e56fa0f",6535:"3d8d21df",6938:"608ae6a4",7178:"096bfee4",7240:"0f425520",7357:"08c34d24",7414:"393be207",7861:"642e9f94",7918:"17896441",7922:"3660216b",7995:"ff1782e3",8199:"062c1b27",8278:"c451c5ed",8444:"3262a34b",8518:"a7bd4aaa",8610:"6875c492",8636:"f4f34a3a",8654:"f555bcff",8761:"1b9776b6",8875:"19826855",8888:"21d68595",8954:"89d39a78",9003:"925b3f96",9035:"4c9e35b1",9077:"b23f3d07",9261:"b31dded6",9317:"7c7b5a9b",9349:"4e810807",9516:"587818ee",9642:"7661071f",9647:"4604b011",9661:"5e95c892",9700:"e16015ca",9713:"520e22a5",9740:"cfff6e91",9882:"7feddd0b"}[e]||e)+"."+{53:"6924f0c5",54:"30dfbad5",60:"43760141",80:"f1fe94f7",110:"83a2d198",191:"aa6744bd",278:"02d9f023",308:"05cf3577",453:"a606a441",533:"d4c820a0",722:"86c60a80",894:"cddfce92",948:"78f07aea",1071:"341c4aee",1477:"6bbd0984",1506:"ac42e502",1633:"4541628a",1713:"00580228",1772:"3883f26e",1881:"59981709",1914:"34b19fce",1925:"6408989c",2200:"59bc4eaf",2267:"1df61e98",2362:"464b1d37",2372:"384da0ba",2399:"81cf8894",2510:"46716bc6",2535:"78f850a6",2537:"ee1f77aa",2546:"f6eccc4f",2547:"1346df97",2587:"bc233fea",2914:"38590bd2",2981:"198e1c76",3085:"5c51203d",3089:"1fe44632",3205:"154440f1",3268:"11272d03",3310:"bb245aaf",3354:"bc9a61f9",3511:"3d1f0f07",3514:"5bf78c3a",3608:"79fe55e5",4013:"c5cc029e",4020:"bca5b41a",4195:"278ca1e5",4368:"7bdd16ec",4546:"395b236f",4620:"b51d3ff0",5017:"2192c522",5097:"65611896",5243:"ebab9f45",5521:"a5b4885a",5550:"38645dd8",5582:"eb9b0de6",6074:"6a921100",6103:"c8f8ad68",6129:"2b0a69d8",6203:"19965ada",6317:"40bb9634",6425:"954e0ae9",6535:"4c91e48b",6938:"777bc2ca",7178:"574352e1",7240:"db72f9d7",7357:"281f4211",7395:"6a5d886e",7414:"08ff365d",7861:"e1061e45",7918:"a1d40f2d",7922:"15fd4814",7995:"f417be7e",8199:"b08cd01f",8278:"574fb96c",8444:"2fa47d2f",8518:"07e993a8",8610:"133c1817",8636:"d66c6973",8654:"dc659cba",8761:"7472ebb5",8875:"9478e407",8888:"6f8e5ccd",8954:"3f495133",9003:"17aa319a",9035:"2f0ea90f",9077:"42d2421f",9261:"6a386031",9317:"993c3eca",9349:"1baa3bb8",9516:"428eef97",9642:"9856c1fe",9647:"2ac8481c",9661:"4aa2b89a",9677:"38f534c1",9700:"bc4e4817",9713:"413648fc",9740:"a587f6fb",9882:"d9cbe026"}[e]+".js",r.miniCssF=e=>{},r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.o=(e,a)=>Object.prototype.hasOwnProperty.call(e,a),f={},c="docusaurus-static:",r.l=(e,a,b,d)=>{if(f[e])f[e].push(a);else{var t,o;if(void 0!==b)for(var n=document.getElementsByTagName("script"),i=0;i<n.length;i++){var u=n[i];if(u.getAttribute("src")==e||u.getAttribute("data-webpack")==c+b){t=u;break}}t||(o=!0,(t=document.createElement("script")).charset="utf-8",t.timeout=120,r.nc&&t.setAttribute("nonce",r.nc),t.setAttribute("data-webpack",c+b),t.src=e),f[e]=[a];var s=(a,b)=>{t.onerror=t.onload=null,clearTimeout(l);var c=f[e];if(delete f[e],t.parentNode&&t.parentNode.removeChild(t),c&&c.forEach((e=>e(b))),a)return a(b)},l=setTimeout(s.bind(null,void 0,{type:"timeout",target:t}),12e4);t.onerror=s.bind(null,t.onerror),t.onload=s.bind(null,t.onload),o&&document.head.appendChild(t)}},r.r=e=>{"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.p="/",r.gca=function(e){return e={17896441:"7918",19826855:"8875",30688810:"6317",59362658:"2267",66406991:"110","935f2afb":"53","0a2c82fb":"54",e54870e4:"60","9beb87c2":"80","24dd378c":"191","25f60d21":"278","2c26bb54":"308","30a24c52":"453",b2b675dd:"533","5f4ff9a9":"722",a5758fe9:"894","8717b14a":"948",b1e73d9b:"1071",b2f554cd:"1477","55d8b2aa":"1506","031793e1":"1633",a7023ddc:"1713","660a8848":"1881",d9f32620:"1914","0a229eeb":"1925",da5cb8ba:"2200",e273c56f:"2362",e3eb95dd:"2372","20598c5e":"2399","1dba1ecf":"2510","814f3328":"2535","3682177e":"2537",ee07fdd2:"2546",c7687382:"2547",bb1b26fa:"2587","4b4e9c91":"2914","2808364d":"2981","1f391b9e":"3085",a6aa9e1f:"3089",a80da1cf:"3205","8838a7ac":"3268","46bbbc4b":"3310",d43d2919:"3354",ce40d3b6:"3511","73664a40":"3514","9e4087bc":"3608","01a85c17":"4013","4cd5e786":"4020",c4f5d8e4:"4195",a94703ab:"4368","93acc585":"4546","17495a52":"4620","09573330":"5017","8c65a213":"5097","867bb2d0":"5243","3abe8fb9":"5521","995e17fc":"5550","5817ad8d":"5582","00483006":"6074",ccc49370:"6103",c92107ec:"6129","4a298a69":"6203","3e56fa0f":"6425","3d8d21df":"6535","608ae6a4":"6938","096bfee4":"7178","0f425520":"7240","08c34d24":"7357","393be207":"7414","642e9f94":"7861","3660216b":"7922",ff1782e3:"7995","062c1b27":"8199",c451c5ed:"8278","3262a34b":"8444",a7bd4aaa:"8518","6875c492":"8610",f4f34a3a:"8636",f555bcff:"8654","1b9776b6":"8761","21d68595":"8888","89d39a78":"8954","925b3f96":"9003","4c9e35b1":"9035",b23f3d07:"9077",b31dded6:"9261","7c7b5a9b":"9317","4e810807":"9349","587818ee":"9516","7661071f":"9642","4604b011":"9647","5e95c892":"9661",e16015ca:"9700","520e22a5":"9713",cfff6e91:"9740","7feddd0b":"9882"}[e]||e,r.p+r.u(e)},(()=>{var e={1303:0,532:0};r.f.j=(a,b)=>{var f=r.o(e,a)?e[a]:void 0;if(0!==f)if(f)b.push(f[2]);else if(/^(1303|532)$/.test(a))e[a]=0;else{var c=new Promise(((b,c)=>f=e[a]=[b,c]));b.push(f[2]=c);var d=r.p+r.u(a),t=new Error;r.l(d,(b=>{if(r.o(e,a)&&(0!==(f=e[a])&&(e[a]=void 0),f)){var c=b&&("load"===b.type?"missing":b.type),d=b&&b.target&&b.target.src;t.message="Loading chunk "+a+" failed.\n("+c+": "+d+")",t.name="ChunkLoadError",t.type=c,t.request=d,f[1](t)}}),"chunk-"+a,a)}},r.O.j=a=>0===e[a];var a=(a,b)=>{var f,c,d=b[0],t=b[1],o=b[2],n=0;if(d.some((a=>0!==e[a]))){for(f in t)r.o(t,f)&&(r.m[f]=t[f]);if(o)var i=o(r)}for(a&&a(b);n<d.length;n++)c=d[n],r.o(e,c)&&e[c]&&e[c][0](),e[c]=0;return r.O(i)},b=self.webpackChunkdocusaurus_static=self.webpackChunkdocusaurus_static||[];b.forEach(a.bind(null,0)),b.push=a.bind(null,b.push.bind(b))})()})(); \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/2024/01/28/welcome-to-docusaurus-static/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/2024/01/28/welcome-to-docusaurus-static/index.html new file mode 100644 index 00000000..e5e1c6df --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/2024/01/28/welcome-to-docusaurus-static/index.html @@ -0,0 +1,15 @@ +<!doctype html> +<html lang="en" dir="ltr" class="blog-wrapper blog-post-page plugin-blog plugin-id-default" data-has-hydrated="false"> +<head> +<meta charset="UTF-8"> +<meta name="generator" content="Docusaurus v3.1.1"> +<title data-rh="true">Welcome to Docusaurus-Static! | TAK Server Documentation + + + + + +

Welcome to Docusaurus-Static!

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/archive/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/archive/index.html new file mode 100644 index 00000000..7903d01e --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/archive/index.html @@ -0,0 +1,14 @@ + + + + + +Archive | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/atom.xml b/src/takserver-core/src/main/webapp/Marti/documentation/blog/atom.xml new file mode 100644 index 00000000..6759bada --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/atom.xml @@ -0,0 +1,114 @@ + + + https://your-docusaurus-site.example.com/blog + TAK Server Documentation Blog + 2024-01-28T00:00:00.000Z + https://github.com/jpmonette/feed + + TAK Server Documentation Blog + https://your-docusaurus-site.example.com/img/tak.ico + + <![CDATA[Welcome to Docusaurus-Static!]]> + https://your-docusaurus-site.example.com/blog/2024/01/28/welcome-to-docusaurus-static + + 2024-01-28T00:00:00.000Z + + Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

]]>
+ + OctoSpacc + https://hub.octt.eu.org + + + +
+ + <![CDATA[Welcome to Docusaurus!]]> + https://your-docusaurus-site.example.com/blog/welcome + + 2021-08-26T00:00:00.000Z + + Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

]]>
+ + Sébastien Lorber + https://sebastienlorber.com + + + Yangshun Tay + https://github.com/yangshun + + + + +
+ + <![CDATA[MDX Blog Post]]> + https://your-docusaurus-site.example.com/blog/mdx-blog-post + + 2021-08-01T00:00:00.000Z + + Blog posts support Docusaurus Markdown features, such as MDX.

+
tip

Use the power of React to create interactive blog posts.

<button onClick={() => alert('button clicked!')}>Click me!</button>
]]>
+ + Sébastien Lorber + https://sebastienlorber.com + + +
+ + <![CDATA[Long Blog Post]]> + https://your-docusaurus-site.example.com/blog/long-blog-post + + 2019-05-29T00:00:00.000Z + + This is the summary of a very long blog post,

+

Use a <!-- truncate --> comment to limit blog post size in the list view.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

]]>
+ + Endilie Yacop Sucipto + https://github.com/endiliey + + + +
+ + <![CDATA[First Blog Post]]> + https://your-docusaurus-site.example.com/blog/first-blog-post + + 2019-05-28T00:00:00.000Z + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

]]>
+ + Gao Wei + https://github.com/wgao19 + + + +
+
\ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/first-blog-post/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/first-blog-post/index.html new file mode 100644 index 00000000..379b469d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/first-blog-post/index.html @@ -0,0 +1,14 @@ + + + + + +First Blog Post | TAK Server Documentation + + + + + +

First Blog Post

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/index.html new file mode 100644 index 00000000..723fa6c9 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/index.html @@ -0,0 +1,28 @@ + + + + + +Blog | TAK Server Documentation + + + + + +

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/long-blog-post/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/long-blog-post/index.html new file mode 100644 index 00000000..3660a943 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/long-blog-post/index.html @@ -0,0 +1,31 @@ + + + + + +Long Blog Post | TAK Server Documentation + + + + + +

Long Blog Post

· 3 min read
Endilie Yacop Sucipto

This is the summary of a very long blog post,

+

Use a <!-- truncate --> comment to limit blog post size in the list view.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/mdx-blog-post/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/mdx-blog-post/index.html new file mode 100644 index 00000000..ea226fba --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/mdx-blog-post/index.html @@ -0,0 +1,15 @@ + + + + + +MDX Blog Post | TAK Server Documentation + + + + + +
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/rss.xml b/src/takserver-core/src/main/webapp/Marti/documentation/blog/rss.xml new file mode 100644 index 00000000..6817c77f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/rss.xml @@ -0,0 +1,92 @@ + + + + TAK Server Documentation Blog + https://your-docusaurus-site.example.com/blog + TAK Server Documentation Blog + Sun, 28 Jan 2024 00:00:00 GMT + https://validator.w3.org/feed/docs/rss2.html + https://github.com/jpmonette/feed + en + + <![CDATA[Welcome to Docusaurus-Static!]]> + https://your-docusaurus-site.example.com/blog/2024/01/28/welcome-to-docusaurus-static + https://your-docusaurus-site.example.com/blog/2024/01/28/welcome-to-docusaurus-static + Sun, 28 Jan 2024 00:00:00 GMT + + Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

]]>
+ hello + docusaurus-static +
+ + <![CDATA[Welcome to Docusaurus!]]> + https://your-docusaurus-site.example.com/blog/welcome + https://your-docusaurus-site.example.com/blog/welcome + Thu, 26 Aug 2021 00:00:00 GMT + + Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

]]>
+ facebook + hello + docusaurus +
+ + <![CDATA[MDX Blog Post]]> + https://your-docusaurus-site.example.com/blog/mdx-blog-post + https://your-docusaurus-site.example.com/blog/mdx-blog-post + Sun, 01 Aug 2021 00:00:00 GMT + + Blog posts support Docusaurus Markdown features, such as MDX.

+
tip

Use the power of React to create interactive blog posts.

<button onClick={() => alert('button clicked!')}>Click me!</button>
]]>
+ docusaurus +
+ + <![CDATA[Long Blog Post]]> + https://your-docusaurus-site.example.com/blog/long-blog-post + https://your-docusaurus-site.example.com/blog/long-blog-post + Wed, 29 May 2019 00:00:00 GMT + + This is the summary of a very long blog post,

+

Use a <!-- truncate --> comment to limit blog post size in the list view.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

]]>
+ hello + docusaurus +
+ + <![CDATA[First Blog Post]]> + https://your-docusaurus-site.example.com/blog/first-blog-post + https://your-docusaurus-site.example.com/blog/first-blog-post + Tue, 28 May 2019 00:00:00 GMT + + Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

]]>
+ hola + docusaurus +
+
+
\ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/docusaurus-static/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/docusaurus-static/index.html new file mode 100644 index 00000000..d2ae2c93 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/docusaurus-static/index.html @@ -0,0 +1,15 @@ + + + + + +One post tagged with "docusaurus-static" | TAK Server Documentation + + + + + +

One post tagged with "docusaurus-static"

View All Tags

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/docusaurus/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/docusaurus/index.html new file mode 100644 index 00000000..f7c56634 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/docusaurus/index.html @@ -0,0 +1,27 @@ + + + + + +4 posts tagged with "docusaurus" | TAK Server Documentation + + + + + +

4 posts tagged with "docusaurus"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/facebook/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/facebook/index.html new file mode 100644 index 00000000..fde3640e --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/facebook/index.html @@ -0,0 +1,25 @@ + + + + + +One post tagged with "facebook" | TAK Server Documentation + + + + + +

One post tagged with "facebook"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/hello/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/hello/index.html new file mode 100644 index 00000000..4c81ecea --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/hello/index.html @@ -0,0 +1,27 @@ + + + + + +3 posts tagged with "hello" | TAK Server Documentation + + + + + +

3 posts tagged with "hello"

View All Tags

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/hola/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/hola/index.html new file mode 100644 index 00000000..e2027d83 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/hola/index.html @@ -0,0 +1,14 @@ + + + + + +One post tagged with "hola" | TAK Server Documentation + + + + + +

One post tagged with "hola"

View All Tags

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/index.html new file mode 100644 index 00000000..f2070195 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/tags/index.html @@ -0,0 +1,14 @@ + + + + + +Tags | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/blog/welcome/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/blog/welcome/index.html new file mode 100644 index 00000000..8e662d17 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/blog/welcome/index.html @@ -0,0 +1,25 @@ + + + + + +Welcome to Docusaurus! | TAK Server Documentation + + + + + +

Welcome to Docusaurus!

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/about/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/about/index.html new file mode 100644 index 00000000..f71ae35b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/about/index.html @@ -0,0 +1,15 @@ + + + + + +About TAK Server | TAK Server Documentation + + + + + +

About TAK Server

+

TAK Server is a situational awareness server, that provides a dynamic Common Operating Picture to users of the Team Awareness Kit, including ATAK (Android), WinTAK (Windows) and WebTAK. TAK enables sharing of geolocated information in real time for military forces, law enforcement, and emergency responders. It supports both wireless and wired networks, as well as cloud and data center deployment.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixa/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixa/index.html new file mode 100644 index 00000000..944f1961 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixa/index.html @@ -0,0 +1,45 @@ + + + + + +Appendix A: Acronyms and Abbreviations | TAK Server Documentation + + + + + +

Appendix A: Acronyms and Abbreviations

+
    +
  • ATAK Android Team Awareness Kit
  • +
  • CA Certificate Authority (for digital certificates)
  • +
  • CN Common Name (of a digital certificate)
  • +
  • CoT Cursor-on-Target, an XML-based data interchange format
  • +
  • CRL Certificate Revocation List
  • +
  • DoD Department of Defense (United States)
  • +
  • DISA Defense Information Systems Agency
  • +
  • ESAPI Enterprise Security Application Programming Interface
  • +
  • EPL Evaluated Products List
  • +
  • HTTP Hypertext Transfer Protocol
  • +
  • IA Information Assurance
  • +
  • IP Internet Protocol
  • +
  • IPv4 Internet Protocol, version 4. The commonly-used version of IP, in which addresses consist of four integers from zero to 255 (inclusive), separated by periods, such as 192.168.123.4
  • +
  • JCE Java Cryptography Extensions
  • +
  • JDK Java Development Kit, a JRE with additional tools and libraries.
  • +
  • JKS Java Key Store
  • +
  • JRE Java Runtime Environment
  • +
  • KML Keyhole Markup Language, the XML-based data format used by Google Earth
  • +
  • OS Operating System
  • +
  • OWASP Open Web Application Security Project
  • +
  • NIAP National Information Assurance Partnership
  • +
  • PKCS12 Public-Key Cryptography Standard #12
  • +
  • TCP Transmission Control Protocol
  • +
  • RHEL Red Hat Enterprise Linux
  • +
  • UDP User Datagram Protocol
  • +
  • SSL Secure Sockets Layer
  • +
  • TAK Team Awareness Kit, a mobile or desktop application that sends and receives real-time information through TAK Server.
  • +
  • TLS Transport Layer Security, a newer and more-secure protocol derived from SSL. The terms SSL and TLS are often used interchangeably. Technically, TLS provides a superset of SSL's capabilities and should always be preferred.
  • +
  • XML Extensible Markup Language
  • +
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixb/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixb/index.html new file mode 100644 index 00000000..cc857d41 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixb/index.html @@ -0,0 +1,58 @@ + + + + + +Appendix B: Certificate Generation | TAK Server Documentation + + + + + +

Appendix B: Certificate Generation

+

TAK Server includes scripts for generating a private security enclave, which will create a Certificate Authority (CA) as well as server and client certificates.

+

First, figure out how many client certificates you are going to need. Ideally you should have a different client cert for each ATAK device on your network.

+

Become the tak user:

+
sudo su tak
+

Edit the certificate-generation configuration file, at this location:

+
/opt/tak/certs/cert-metadata.sh
+

Set options for country, state, city, organization, and organizational_unit.

+

Change directory:

+
cd /opt/tak/certs
+

Create a certificate authority (CA):

+
./makeRootCa.sh
+

Follow the prompt to name the CA.

+

Create a server certificate:

+
./makeCert.sh server takserver
+

This command will result in a server certificate named '/opt/tak/certs/files/takserver.jks'

+

Create one or more client certificates. You should use a different client cert for each ATAK device on your network. This username will be provisioned in the certificate as the CN (Common Name). When using certs on devices that are connected to an input that is configured for group filtering without authentication messages, this username will be used by TAK Server to look up group membership information in an authentication repository, such as Active Directory (AD). This command will create a cert for the username user:

+
./makeCert.sh client user
+

Generate another cert, named admin to access the admin UI:

+
./makeCert.sh client admin
+

The generated CA truststores and certs will be located here:

+
/opt/tak/certs/files
+

Follow the instruction on "Configure TAK Server Certificate" to set up the server to use the generated certs and to authenticate users on a TLS port. If using the default configuration, TLS will be correctly set up on 8443.

+

Become a normal user:

+
exit
+

Restart the TAK Server:

+
sudo systemctl restart takserver
+

Authorize the admin cert to perform administrative functions using the UI:

+
sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem
+

Configure TAK Server Certificate

+

In /opt/tak, check the following settings in CoreConfig.xml:

+
    +
  1. In the <tls> element, the keystoreFile attribute should be set to the server keystore that was generated with makeCerts.sh, above. If you followed the instructions verbatim, the server keystore is '/opt/tak/certs/files/takserver.jks'.
  2. +
  3. Also in the <tls> element, the truststoreFile attribute should be set to the the trust store that was generated with makeCerts.sh, above. If you used the default arguments, your trust store file is '/opt/tak/certs/files/truststore-root.jks'.
  4. +
  5. In the <network> element, add a TLS input, specifying group-based filtering without requiring an authentication message:
  6. +
+
<input _name="stdssl" protocol="tls" port="8089" auth="x509"/>
+

You can change the port number if you want. +Example:

+
<network multicastTTL="5">
<!-- <input _name="stdtcp" protocol="tcp" port="8087"/> -->
<!-- <input _name="stdudp" protocol="udp" port="8087"/> -->
<input _name="stdssl" protocol="tls" port="8089" auth="x509"/>
<!-- <input _name="streamtcp" protocol="stcp" port="8088"/> -->
<!-- <input _name="SAproxy" protocol="mcast" group="239.2.3.1" port="6969" proxy="true"/> -->
<!-- <input _name="GeoChatproxy" protocol="mcast" group="224.10.10.1" port="17012" proxy="true"/> -->
<!--<announce enable="true" uid="Marti1" group="239.2.3.1" port="6969" interval="1" ip="192.168.1.137" />-->
</network>
...
<security>
<tls context="TLSv1" keymanager="SunX509" keystore="JKS" keystoreFile="certs/files/takserver.jks" keystorePass="atakatak" truststore="JKS" truststoreFile="certs/files/truststore-root.jks" truststorePass="atakatak">
(Uncomment the following if you are using a CRL)
<!-- <crl _name="Marti CA" crlFile="certs/ca.crl"/> -->
</tls>
</security>
+

Then (re)start the TAK Server as normal.

+

Installing Client Certificates

+

Take the truststore-root.p12 and user.p12 files and copy them to your Android device. In ATAK, open 'Settings -> General Settings -> Network Settings' and set the SSL/TLS Truststore and Client Certificate preferences to point to those .p12 files.

+

Repeat the procedure described above for creating a new server connection, but be sure to select SSL as the protocol.

+

These same .p12 files can be installed in a browser, and used to access the Web UI (for admin use) and WebTAK (for normal users or admins). The process to install these files varies by browser and operating system, but can generally be configured by going to the browser preferences, and the security or certificates section.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixc/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixc/index.html new file mode 100644 index 00000000..1f8ac73e --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixc/index.html @@ -0,0 +1,21 @@ + + + + + +Appendix C: Certificate Signing | TAK Server Documentation + + + + + +

Appendix C: Certificate Signing

+

TAK Clients can enroll for new client certificates by submitting a Certificate Signing Request (CSR) to TAK Server. The Certificate Signing endpoint resides on port 8446 and requires HTTP Basic Authentication backed by either File or LDAP authentication. Ensure that the tomcat connector for port 8446 is active within tomcat-home/conf/server.xml.

+

The CertificateSigning section in CoreConfig.xml specifies how CSRs are processed. TAK Server can be configured to sign certificates directly, or proxy CSRs to a Microsoft CA instance running Certificate Enrollment Services. To configure TAK Server to sign certificates, set the CA attribute to "TAKServer". To configure TAK Server to proxy the CSR to MS CA, set the CA attribute to "MicrosoftCA".

+
<certificateSigning CA="{TAKServer | MicrosoftCA}">
<certificateConfig>
<nameEntries>
<nameEntry name="O" value="Test Org Name"/>
<nameEntry name="OU" value="Test Org Unit Name"/>
</nameEntries>
</certificateConfig>
<TAKServerCAConfig
keystore="JKS"
keystoreFile="../certs/files_intCA/intermediate-ca-signing.jks"
keystorePass="atakatak"
validityDays="30"
signatureAlg="SHA256WithRSA" />
<MicrosoftCAConfig
username="{MS CA Username}"
password="{MS CA Password}"
truststore="/opt/tak/certs/files_MSCA/keystore.jks"
truststorePass="atakatak"
svcUrl="https://win-kbtud3n1hjl.tak.net/tak-WIN-KBTUD3N1HJL-CA_CES_UsernamePassword/service.svc"
templateName="Copy of User"/>
</certificateSigning>
+

Prior to submitting a CSR, Clients query TAK Server for Relative Distinguished Names (RDNs) that need to go into the CSR. The nameEntries element in CoreConfig.xml specifies the required RDNs, giving the administrator control over generated certificates. The CN value in the CSR will be equal to the HTTP username. TAK Server validates all required fields in the CSR prior to signing.

+

The extra step of having client query TAK Server for RDNs wouldn't be required if TAK Server were signing certificates exclusively, since TAK Server could just add these names to the certificate. However, when proxying the CSR to an external CA, this allows additional flexibility in controlling the subject name within the certificate.

+

The TAKServerCAConfig element specifies the keystore that TAK Server will use for signing certificates. The keystore must hold the CA's private key along with it's full trust chain. The makeCert.sh script will produce a signing keystore when generating an intermediate CA certificate. Certificates signed by TAK Server will be valid for the specified validityDays, and will be signed using the algorithm specified by signatureAlg.

+

The MicrosoftCAConfig element defines how TAK Server will connect to the Certificate Enrollment Services (CES) endpoint. The CES endpoint is defined by the svcUrl attribute. The CES endpoint must be configured to use Username/Password authentication, and by default will include 'UsernamePassword' in the URL. The username and password attributes refer to an account configured on the MS CA Server to access the the CES endpoint. The truststore and truststorePass attrbitues point to a Java keystore (.jks) file that contains the trust chain for the svcUrl endpoint. Lastly, the templateName defines the certificate template that will be used to sign CSRs sent from TAK Server.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixd/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixd/index.html new file mode 100644 index 00000000..0fc5e351 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixd/index.html @@ -0,0 +1,97 @@ + + + + + +Appendix D: PostgreSQL TLS Configuration | TAK Server Documentation + + + + + +

Appendix D: PostgreSQL TLS Configuration

+

Configure PostgreSQL server to use TLS

+
    +
  • +

    Follow the steps in Appendix B (Certificate Generation) to generate CA keys and certificates if not already done so.

    +
  • +
  • +

    Generate PostgreSQL server keys and certificates:

    +
  • +
+
cd /opt/tak/certs
sudo su tak
./makeCert.sh server takdb
+

Become a normal user

+
exit
sudo chown postgres /opt/tak/certs/files/takdb.key
+
    +
  • Update postgresql.conf. The file location can be different depending on your PostgreSQL installation: +
      +
    • RHEL/Rocky/CentOS: /var/lib/pgsql/15/data/postgresql.conf
    • +
    • Ubuntu/RaspPi: /etc/postgresql/15/main/postgresql.conf
    • +
    +
  • +
+
sudo vim /var/lib/pgsql/15/data/postgresql.conf
ssl = on
ssl_ca_file = '/opt/tak/certs/files/ca.pem'
ssl_cert_file = '/opt/tak/certs/files/takdb.pem'
ssl_key_file = '/opt/tak/certs/files/takdb.key'
# Make sure to update the next line to use the correct passphrase as configured in cert-metadata.sh.
ssl_passphrase_command = 'echo "atakatak"'
ssl_passphrase_command_supports_reload = on
+
    +
  • Update pg_hba.conf. The file location can be different depending on your PostgreSQL installation: +
      +
    • RHEL/Rocky/CentOS: /var/lib/pgsql/15/data/pg_hba.conf
    • +
    • Ubuntu/RaspPi: /etc/postgresql/15/main/pg_hba.conf
    • +
    +
  • +
+
sudo vim /var/lib/pgsql/15/data/pg_hba.conf
+

Add this new line:

+
hostssl	 all             all             all                     cert
Comment out the following lines if you also require SSL authentication for IPv4/IPv6 local connections
# host all all 127.0.0.1/32 trust
# host all all ::1/128 trust
+
    +
  • Restart PostgreSQL server. Make sure it starts successfully. +
      +
    • RHEL, Rocky Linux, and CentOS installations: +
      sudo systemctl restart postgresql-15
      sudo systemctl status postgresql-15
      +
    • +
    • Ubuntu and Raspberry Pi installations: +
      sudo systemctl restart postgresql
      sudo systemctl status postgresql
      +
    • +
    +
  • +
+

Generate Client keys and certificates

+
    +
  • Generate client keys and certificates:
  • +
+
cd /opt/tak/certs
sudo su tak
./makeCert.sh dbclient
+

Client keys and certificates named "martiuser" (by default) will be created in the "files" directory.

+
    +
  • Test SSL connection using the generated client certificate:
  • +
+
psql "host=127.0.0.1 port=5432 user=martiuser dbname=cot sslmode=verify-ca sslcert=files/martiuser.pem sslkey=files/martiuser.key sslrootcert=files/ca.pem"
+

If you don’t want to verify the server’s credential:

+
psql "host=127.0.0.1 port=5432 user=martiuser dbname=cot sslmode=require sslcert=files/martiuser.pem sslkey=files/martiuser.key"
+

The sslmode "verify-ca" means "I want to be sure that I connect to a server that I trust." The sslmode "require" means "I trust that the network will make sure I always connect to the server I want."

+

More information on the sslmode can be found here: https://www.postgresql.org/docs/current/libpq-ssl.html

+
    +
  • Test database permission from the psql prompt:
  • +
+
select count(*) from cot_router;
+

NOTE: If you want to use a different name for certificates, you would also need to add a new user to the PostgreSQL database and grant permissions for the user. For example, following these steps to create a certificate named "takdbuser"

+
./makeCert.sh dbclient takdbuser
sudo su - postgres
+

Connect to Postgres:

+
psql -d cot
# List all users/roles:
\du
SELECT * FROM pg_roles;
# Create a new user ("takdbuser") and grant the user necessary roles ("martiuser"). The name of the user must match the CN in the client certificate.
CREATE USER takdbuser;
grant martiuser to takdbuser;
# Optional: Double check using \du and "SELECT * FROM pg_roles;"
+

Configure TAK Server to use SSL

+
    +
  • +

    Note that when you created a database client certificate (./makeCert.sh dbclient), an additional private key file in PKCS#8 format was created. Use this file for the param sslkey in CoreConfig.xml instead of using the files with .key extension.

    +
  • +
  • +

    Update CoreConfig.xml: +Update the <connection> tag in <repository> (Remember to use a correct hostname/IP)

    +
  • +
+
<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" sslEnabled="true" sslMode="verify-ca" sslCert="certs/files/martiuser.pem" sslKey="certs/files/martiuser.key.pk8" sslRootCert="certs/files/ca.pem"/>
+

If you don’t want to verify the server’s credential (not recommended in production):

+
<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" sslEnabled="true" sslMode="require" sslCert="certs/files/martiuser.pem" sslKey="certs/files/martiuser.key.pk8" />
+
    +
  • Start/Restart TAK server.
  • +
+
 sudo systemctl restart takserver
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixe/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixe/index.html new file mode 100644 index 00000000..ee0a6d1a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/appendixe/index.html @@ -0,0 +1,26 @@ + + + + + +Appendix E: Proper Use of Trusted CAs | TAK Server Documentation + + + + + +

Appendix E: Proper Use of Trusted CAs

+

TAK uses Mutual TLS (MTLS) authentication to establish secure communications channels between TAK clients and TAK Server. It's critical that deployments use a CA created by the TAK server scripts, or another private CA, to establish the root of trust. Failure to follow this guidance could result in exposing your deployment to a Man-In-The-Middle (MITM) attack.

+
    +
  • To ensure secure communications, it's critical that truststores deployed to TAK clients only contain CAs created by the TAK Server scripts or another private CA.
  • +
  • There is never a need to add a LetsEncrypt, Digicert or any other public CA certificate to a truststore on a TAK client or TAK Server.
  • +
  • When using Quick Connect, the LetsEncrypt or DigiCert server certificate should only be configured within your 8446 connector, and never within your <tls> configuration.
  • +
+

Appendix B of the TAK Server Configuration Guide (see Downloadable Resources section here https://tak.gov/products/tak-server) contains steps for creating a root CA to use in your TAK deployment. The makeRootCa.sh script creates a private key and self-signed certificate for the CA, and packages up the CA certificate within truststores that can be configured on TAK clients and on TAK Server.

+

Appendix B contains additional steps for creating a client and server certificates, signed by the root CA. Appendix C describes TAK Server's Certificate Enrollment capability (https://wiki.tak.gov/display/DEV/Certificate+Enrollment) that automates the provisioning of client certificates. Users authenticate to the Certificate Enrollment endpoints with username/password, provide a CSR and receive a signed client certificate in return.

+

When enrolling using a server certificate from a self-signed TAK CA, clients must be bootstrapped with server's CA certificate prior to enrollment. To streamline provisioning of clients, TAK provides the Quick Connect feature that performs Certificate Enrollment using trust provided by LetsEncrypt or DigiCert CAs. TAK clients have embedded the LetsEncrypt and DigiCert CAs and will use these CAs when validating connections to the enrollment port. In this configuration, the TAK client will be able to leverage the embedded LetsEncrypt CA certificate, along with TLS hostname verification, to ensure the integrity of the connection.

+

To configure Quick Connect, the TAK server admin adds the keystore, keystoreFile, and keystorePass attributes on the 8446 connector to use the trusted server cert for enrollment, as shown below.

+
	    <connector port="8446" clientAuth="false" _name="cert_https" keystore="JKS" keystoreFile="certs/files/letsencrypt-server-cert.jks" keystorePass="example-pass"/>
+

When using Quick Connect, you never have to do any configuration with LetsEncrypt or DigiCert CA itself. That is handled by embedding the CAs within the clients. The only reference to any key material from LetsEncrypt or DigiCert within your environment will be the server certificate contained in keystoreFile that's referenced by your 8446 connector.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/changelog/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/changelog/index.html new file mode 100644 index 00000000..c7282fb8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/changelog/index.html @@ -0,0 +1,15 @@ + + + + + +Change Log | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/authenticationbackends/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/authenticationbackends/index.html new file mode 100644 index 00000000..beb92e92 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/authenticationbackends/index.html @@ -0,0 +1,25 @@ + + + + + +Authentication Backends | TAK Server Documentation + + + + + +

Authentication Backends

+

File-Based

+

There is now a flat-file option available to inputs. Previously the only valid value for the <input> “auth” attribute was “ldap”. “file” is now another valid value. The example configuration file (CoreConfig.example.xml) contains an example of how to configure the File-based backend.

+

A utility for creating and maintaining that flat file is included in the release. Run

+
sudo java -jar /opt/tak/utils/UserManager.jar
+

and look at the various options for the 'offlineFileAuth'

+

Active Directory (AD) / LDAP

+

TAK Server can be configured to use an Active Directory or LDAP server to authenticate users, and assign groups. LDAP configuration for TAK Server varies depending on the configuration of the AD or LDAP server. Here is an example. Note that it contains credentials for a service account. This is required for group membership lookup using a client cert:

+
<auth>
<ldap url="ldap://a.b.com/ou=MyUserOU,DC=a,DC=b,DC=com" userstring="{username}@MYDOMAIN" updateinterval="60000" style="AD" serviceAccountDN="mysearchuser@MYDOMAIN" serviceAccountCredential="password" />
</auth>
+

Configuring LDAP Through Web Interface

+

Authentication Configuration Web Interface

+

The LDAP configuration can be changed through an easy to use web page. To access this go to Configuration > Manage Security and Authentication. Under the Authentication heading will be the current LDAP configuration (the values will be empty if LDAP is not configured yet). Click on "Edit Authentication" to be directed to a form to enter desired LDAP settings. Note: Changes made here will only take effect after a server restart.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/configuremessagingrepositorywebui/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/configuremessagingrepositorywebui/index.html new file mode 100644 index 00000000..320b4abd --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/configuremessagingrepositorywebui/index.html @@ -0,0 +1,16 @@ + + + + + +Configuring Messaging and Repository Settings through Web UI | TAK Server Documentation + + + + + +

Configuring Messaging and Repository Settings through Web UI

+

Messaging Configuration Web Interface

+

Messaging/Repository settings configuration can be done through the input definitions page. To get there go to Configuration > Input Definitions in the menu bar. This page displays the current input definitions at the top and at the bottom the current configuration of Messaging and Repository settings are displayed. To edit these setting click "Edit Configuration". Note: Changes made here will only take effect after a server restart.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/configurewebui/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/configurewebui/index.html new file mode 100644 index 00000000..b9d7dd0f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/configurewebui/index.html @@ -0,0 +1,18 @@ + + + + + +Configuring Security Through Web UI (Certificates/TLS) | TAK Server Documentation + + + + + +

Configuring Security Through Web UI (Certificates/TLS)

+

Security Configuration

+

Security Configuration Web interface

+

Security and authentication options for TAK Server can be set up using a web interface. To access this page in the menu bar go to Configuration > Manage Security and Authentication Configuration. This page will contain both Security and Authentication configuration current values. To modify the Security Configuration click "Edit Security". This will allow changes to the server's certificates, the version of TLS used, x509 Groups settings and x509 Add Anonymous settings.

+

Note: Changes made here will only take effect after a server restart.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentbyinput/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentbyinput/index.html new file mode 100644 index 00000000..95bb35dc --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentbyinput/index.html @@ -0,0 +1,22 @@ + + + + + +Group Assignment by Input | TAK Server Documentation + + + + + +

Group Assignment by Input

+

<inputs> can drive group filtering, even without authentication messages. Version 1.3.0 added group filtering based on LDAP groups. This necessitated a new authentication message from ATAK. This worked for the streaming connections, but wouldn't work for the connection-less UDP traffic.

+

We added an additional configuration option for inputs to allow the connection-less traffic to be routed according to the group filtering. An input definition like this:

+
   <input _name="stdudp" protocol="udp" port="8087">
<filtergroup>TEST1</filtergroup>
</input>

+

would have the effect of making every CoT event that came into the 'stdudp' input be associated with the “TEST1” group instead of the anonymous group. If there is no filtergroup specified, the default is the old behavior, which is a special anonymous group. The anonymous group has a name “__ANON__” that can be used to explicitly add it back in if needed. The filtergroup option can be used with the streaming input protocols as well (stcp, tls), the effect of which is that any subscriptions made by connecting to that port inherit the filter group from the input. <filtergroup> cannot be used in conjunction with the “auth” attribute on the same input. You can however use them on separate inputs, for example:

+
   <input _name="stdudp" protocol="udp" port="8087">
<filtergroup>CN=TAK1,DC=...</filtergroup>
</input>
<input _name="sec" protocol="tls" port="8089" auth="ldap" />
+

Note that when trying to interact with LDAP groups, you need to use the fully qualified group name that LDAP/ActiveDirectory reports.

+

Input Configuration UI

+

Inputs can be dynamically added, modified and deleted in the TAK Server user interface, under the menu heading Configuration → Input Definitions. The UI also shows activity for each input, in terms of number of reads and messages. For the streaming protocols (stcp, tls), the activity is the sum for all connections made using that particular input port.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentusingauth/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentusingauth/index.html new file mode 100644 index 00000000..e43e1a75 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentusingauth/index.html @@ -0,0 +1,18 @@ + + + + + +Group Assignment Using Authentication Messages | TAK Server Documentation + + + + + +

Group Assignment Using Authentication Messages

+

If ATAK or another TAK client is configured to send an authentication message after establishing a connection to TAK Server, the username and password credentials contained in that message will be used, in conjunction with an authentication backend, to determine the group membership of a user. TAK Server will then filter messages according to common group membership, in a similar fashion to filtering configured by <filtergroups> for a given <input>.

+

TAK Server can be configured at the input level to expect authentication messages with each new client connection. If the authentication message is not sent, or is invalid, the client will be disconnected. The “auth” attribute on the input indicates which authentication strategy will be used when processing authentication messages. A value of “file” tells TAK Server to validate authentication credentials using the flat-file backend. A value of “ldap” indicates that an Active Directory or LDAP server should be used to validate the credentials.

+

For example, this input definition specifies streaming TCP, encrypted with TLS, authenticating the user with a client certificate and also requiring an authentication message, and using the LDAP authentication backend:

+
  <input _name="ldapssl" protocol="tls" port="8091"  auth="ldap"/>
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentusingclientcerts/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentusingclientcerts/index.html new file mode 100644 index 00000000..f549d7a4 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupassignmentusingclientcerts/index.html @@ -0,0 +1,19 @@ + + + + + +Group Assignment using Client Certificates | TAK Server Documentation + + + + + +

Group Assignment using Client Certificates

+

TAK Server can be configured to use only the information contained in a client certificate, when looking up group membership for a user. In this case, the TAK client is configured to use TLS and a client certificate when connecting to TAK server, but does not send an authentication message. This eliminates the requirement to cache credentials on the device, or enter credentials prior to establishment of each new connection. TAK Server will then filter messages according to common group membership, in a similar fashion to input-level filtering with filter groups. When analyzing the X.509 client certificate presented by the TAK client, TAK Server will look at the DN attribute in the certificate, extract the CN value from the DN (if present). The CN is regarded as the username, and is used to look up group membership in authentication backends. For example, consider this DN in a client certificate:

+
CN=jkirk, O=TAK, C=US
+

The CN value jkirk will be used as the username. The process for deciding which authentication backend to use depends on whether or not an Active Directory (AD) LDAP configuration is present in CoreConfig.xml. Valid service account credentials must be configured in CoreConfig.. If AD authentication is configured, the user account is matched by the sAMAccountName LDAP attribute. At client authentication time, if groups are found in AD for the user, those groups are used by TAK Server. If no groups are found, the flat-file authentication backend is searched for a match on the username. If no groups are found for the user in either repository, the user is assigned to the __ANON__ group.

+

When configuring the input, a TLS input with an auth type of x509 directs TAK Server to use the client certificate for both authentication and group assignment. On the input configuration, the on or example, this input definition specifies streaming TCP encrypted with TLS, authenticating the user with a client certificate and also requiring an authentication message, and using the LDAP authentication backend:

+
  <input _name="ldapssl" protocol="tls" port="8091" auth="x509"/>
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupfiltering/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupfiltering/index.html new file mode 100644 index 00000000..ab38a49a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/groupfiltering/index.html @@ -0,0 +1,21 @@ + + + + + +Group Filtering | TAK Server Documentation + + + + + +

Group Filtering

+

TAK Server has the ability to segment users so they only see a subset of the other users on the system. This is achieved by assigning groups to individual connections. If ATAK-A shares common group membership in at least one group with ATAK-B, they share data with each other. If not otherwise specified, all connections default to being in the special “ANON” group (note 2 underscores as prefix and postfix). There are three ways to assign groups to a connection:

+
    +
  • Assigning <filtergroup> elements to <inputs>: this is simple, but provides no access control if you have multiple ports configured on the same server.
  • +
  • Active Directory / LDAP / Flat file with additional authentication message
  • +
  • Active Directory / LDAP / Flat file without additional authentication messages (uses certificate-based identification)
  • +
+

Details on the three options:

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/optionallydisableui/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/optionallydisableui/index.html new file mode 100644 index 00000000..4ae39587 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/optionallydisableui/index.html @@ -0,0 +1,23 @@ + + + + + +Optionally Disabling UI and WebTAK on HTTPS Ports | TAK Server Documentation + + + + + +

Optionally Disabling UI and WebTAK on HTTPS Ports

+

TAK Server can be configured to optionally disable the Admin UI, non-admin UI or WebTAK on any HTTPS connector (port). These options can be used to fine-tune the security profile for each HTTPS connector. For example, the admin web interface can be moved to an alternate port that is protected by a firewall from access on the public Internet.

+

In the CoreConfig.xml, the enableAdminUI, enableWebtak, and enableNonAdminUI attributes on each <connector> can be used to optionally disable access to any of these three functions for a given HTTPS connector port. The default value for each of these attributes is true, so by default these functions are available.

+

Usage Examples: +Disable webtak on port 8443:

+
<connector port="8443" _name="https" enableWebtak="false" />
+

On port 8452, disable admin UI, but enable WebTAK and non-admin UI:

+
<connector port="8452" _name="https" enableAdminUI="false" />
+

Disable WebTAK on OAuth port 8446:

+
<connector port="8446" clientAuth="false" _name="cert_https" enableWebtak="false"/>
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/overview/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/overview/index.html new file mode 100644 index 00000000..1f2186ae --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/overview/index.html @@ -0,0 +1,28 @@ + + + + + +Overview | TAK Server Documentation + + + + + +

Overview

+

Configuration is primarily done through the web interface. Changes made in the interface will be reflected in the /opt/tak/CoreConfig.xml file. If that file does not exist (e.g. on a fresh install), then when TAK Server starts up it will copy /opt/tak/CoreConfig.example.xml. The example has many commented out options. Notable configuration options:

+
    +
  • inputs: In the <network> section there are a series of <input> elements. These define ports the server will listen on. Protocol options are as follows: +
      +
    • udp: standard CoT udp protocol; unencrypted
    • +
    • mcast: like udp, but has additional configuration option for multicast group
    • +
    • tcp: publish-only port; standard CoT tcp protocol; unencrypted
    • +
    • stcp: streaming/bi-directional; this is for ATAK to connect to. Unencrypted, for testing only
    • +
    • tls: TCP+TLS streaming/bi-directional for encrypted communication with TAK clients
    • +
    +
  • +
  • <auth> : you can use either a flat file or an LDAP backend for group filtering support
  • +
  • <security>: here you specify the keystore files to use for the secure port(s)
  • +
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/vbmadminconfig/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/vbmadminconfig/index.html new file mode 100644 index 00000000..e8083021 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/configuration/vbmadminconfig/index.html @@ -0,0 +1,27 @@ + + + + + +VBM Admin Configuration | TAK Server Documentation + + + + + +

VBM Admin Configuration

+

TAK Server can allow for additional filtering of cot messages recieved from inputs (server ports) and data feeds by using the VBM Configuration page in the Admin UI. To navigate there, go to Administative > VBM Configuration as shown below.

+

VBM Admin Configuration

+

You will then see the following options.

+

VBM Admin Configuration

+

To modify the VBM configurations select the checkbox next to the desired option and when you're finished click "Save changes".

+

NOTE: "Disable SA Sharing" and "Disable Chat Sharing" will only be used if "Enable VBM" is selected.

+

The VBM options have the following impact:

+

If "Enable VBM" is selected, messages recieved on a data feed are only brokered to clients which are subscribed to the mission if the message falls within the bounding box specified by the mission. For example, the message represented by the blue dot would be passed on while the message represented by the green dot would be filtered out for the mission with the displayed bounding box only if "Enable VBM" is turned on.

+

VBM Admin Configuration

+

The second two options are only activated if "Enable VBM" is on and they refer to messages recieved from inputs (server ports). These options filter messages based on whether they are chat messages. Chat messages in this context are cot messages which have a type set to "b-t-f".

+

If "Disable SA Sharing" is selected, messages recieved from inputs are passed on if the message is a chat message as defined above.

+

If "Disable Chat Sharing" is selected, messages recieved from inputs are passed on if the messages is NOT a chat message as defined above.

+

These options are not mutually exclusive. Therefore, having both selected will filter out all messages recieved on inputs.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/dataretentiontool/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/dataretentiontool/index.html new file mode 100644 index 00000000..d3581c83 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/dataretentiontool/index.html @@ -0,0 +1,16 @@ + + + + + +Data Retention Tool | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/deviceprofiles/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/deviceprofiles/index.html new file mode 100644 index 00000000..aa3ec040 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/deviceprofiles/index.html @@ -0,0 +1,16 @@ + + + + + +Device Profiles | TAK Server Documentation + + + + + +

Device Profiles

+

TAK Server can now assist in provisioning ATAK devices through Device Profiles. The Device Profile Manager (under Administrative Menu, Device Profiles) allows administrators to build profiles that can be applied to clients when enrolling for certificates, and when connecting to TAK server. The Profile consists of a sets of files, which can include settings and data in any file format that is supported by TAK Mission Packages. Profiles can be made public or restricted to individual Groups.

+

When an ATAK device enrolls for client certificate, or optionally after connecting to TAK server, TAK server will return all profiles that need to be applied to the device. The TAK server administrator can also push a profile to a connected user by clicking the Send link within the Device Profile Manager.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/dockerinstall/buildinstall/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/dockerinstall/buildinstall/index.html new file mode 100644 index 00000000..64d315f4 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/dockerinstall/buildinstall/index.html @@ -0,0 +1,165 @@ + + + + + +Building and Installing Container Images Using Docker | TAK Server Documentation + + + + + +

Building and Installing Container Images Using Docker

+

TAK Server can be installed using docker. Start by downloading container images from tak.gov. You will need the docker release which comes as a zip file called 'takserver-docker-<version>.zip'.

+

If you using CentOS 7, follow these instructions first to install docker, start the docker daemon and use it as a regular user: +https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-centos-7

+

Next, unzip the docker zip file. All further commands should be run from this top level 'takserver-docker-<version>' directory. If you are familiar with the rpm install, the 'tak' folder within the 'takserver-docker-<version>' directory represents the '/opt/tak' directory installed by the rpm. When the takserver containers are built, the 'tak' directory will be mounted to '/opt/tak' within the containers. Therefore, any references to '/opt/tak' outside this section of the guide will be equivalent to the 'tak' directory you have on the host, or '/opt/tak' if you are working from inside the container. The 'tak' directory is where the coreconfig, certificates, logs and other TAK configuration/tools will live. This folder is shared between the host, the takserver container and the takserver database container. This means you can tail the logs or manually edit the coreconfig from the host without being inside the container.

+

Notes for running in Windows Subsystem for Linux (WSL) 2:

+

When running TAK Server in the WSL2 environment, follow the steps outlined in the Best Practices section here https://docs.docker.com/desktop/windows/wsl/ to maximize TAK Server performance. Specifically, it’s recommended that you copy the 'takserver-docker-<version>.zip' file into your WSL user’s home directory and execute all docker commands from there (vs accessing your Windows filesystem from /mnt). From there, unzip the file and run the docker commands below for TAK Server. It’s important to unzip the file from within WSL to ensure permissions are setup correctly.

+

TAK Server CoreConfig Setup:

+
    +
  1. Open tak/CoreConfig.example.xml and set a database password
  2. +
  3. Make any other configuration changes you need
  4. +
+

TAK Server Database Container Setup:

+
    +
  1. Build TAK server database image:
  2. +
+
docker build -t takserver-db:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver-db .
+
    +
  1. Create a new docker network for the current tak version:
  2. +
+
docker network create takserver-"$(cat tak/version.txt)"
+
    +
  1. The TAK Server database container can be configured to persist data directly to the host or only within the container.
  2. +
+

a. To persist to the host, create an empty host directory (unless you have a directory from a previous docker install you want to reuse). For upgrading purposes, we recommend that you keep the takserver database directory outside of the 'takserver-docker-<version>' directory structure.

+
docker run -d -v <absolute path to takserver database directory>:/var/lib/postgresql/data:z -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)"
+

b. To run TAK server database with container only persistence

+
docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)"
+

TAK Server Container Setup:

+
    +
  1. Build TAK Server image:
  2. +
+
docker build -t takserver:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver .
+
    +
  1. Running TAK Server container: use -p <host port>:<container port> to map any additional ports you have configured. Adding new inputs or changing ports while the container is running will require the container to be recreated so that the new port mapping can be added.
  2. +
+
docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -p 9000:9000 -p 9001:9001 --network takserver-"$(cat tak/version.txt)" --name takserver-"$(cat tak/version.txt)" takserver:"$(cat tak/version.txt)"
+
    +
  1. +

    Before using the TAK Server, you must setup the certificates for secure operation. If you have already configured certificates you can skip this step. You can also copy existing certificates into 'tak/certs/files' and a UserAuthetication.xml file into 'tak/' to reuse existing certificate authentication settings. Any change to certificates while the container is running will require either a TAK server restart or container restart. Additional certificate details can be found in Appendix B.

    +

    a. Edit tak/certs/cert-metadata.sh

    +

    b. Generate root ca

    +
    docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeRootCa.sh"
    +

    c. Generate server cert

    +
    docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeCert.sh server takserver"
    +

    d. Create client cert(s)

    +
    docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeCert.sh client <user>"
    +

    e. Restart takserver to load new certificates

    +
    docker exec -d takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && ./configureInDocker.sh"
    +

    f. Tail takserver logs from the host. Once TAK server has successfully started, proceed to the next step.

    +
    tail -f tak/logs/takserver-messaging.log
    tail -f tak/logs/takserver-api.log
    +
  2. +
  3. +

    Accessing takserver +Create admin client certificate for access on secure port 8443 (https):

    +
    docker exec takserver-"$(cat tak/version.txt)" bash -c  "cd /opt/tak/ && java -jar utils/UserManager.jar certmod -A certs/files/<client cert>.pem"
    +
  4. +
+

Hardened TAK Server Setup:

+

The hardened TAK Database and Server containers provide additional security by including the use of secure Iron Bank base images, container health checks, and minimizing user privileges within the containers.

+

The hardened TAK images are available in a zip file, takserver-docker-hardened-<version>.zip. The steps for setting up the hardened containers are similar to the standard docker installation steps given above except for the following:

+

Certificate Generation:

+

The certificate generation container is only required to run once for TAK Server initialization. Run all commands in this section from the root of the unzipped hardened docker contents.

+
    +
  1. Build the Certificate Authority Setup Image:
  2. +
+
docker build -t ca-setup-hardened --build-arg ARG_CA_NAME=<CA_NAME> --build-arg ARG_STATE=<ST> --build-arg ARG_CITY=<CITY> --build-arg ARG_ORGANIZATIONAL_UNIT=<UNIT> -f docker/Dockerfile.ca .
+
    +
  1. Run the Certificate Authority Setup Container: If certificates have previously been generated and exist in the tak/cert/files path when building the ca-setup-hardened image then certificate generation will be skipped at runtime.
  2. +
+
docker run --name ca-setup-hardened -it -d ca-setup-hardened
+
    +
  1. Copy the generated certificates for TAK Server:
  2. +
+
docker cp ca-setup-hardened:/tak/certs/files files

[ -d tak/certs/files ] || mkdir tak/certs/files \
&& docker cp ca-setup-hardened:/tak/certs/files/takserver.jks tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/truststore-root.jks tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/fed-truststore.jks tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/admin.pem tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/config-takserver.cfg tak/certs/files/
+

TAK Server Database Hardened Container Setup:

+
    +
  1. Building the hardened docker images requires creating an Iron Bank/Repo1 account to access the approved base images. To create an account, follow the instructions in the IronBank Getting Started page. To download the base images via the CLI, see the instructions in the Registry Access section. After obtaining the necessary credentials, run:
  2. +
+
docker login registry1.dso.mil
+
    +
  1. Follow the instructions in the TAK Server CoreConfig Setup section and update the <connection-url> tag with the hardened TAK Database container name. For example:
  2. +
+
connection url="jdbc:postgresql://tak-database-hardened-<version>:5432/cot" username="martiuser" password=<password>/>
+
    +
  1. Create a new docker network for the current tak version:
  2. +
+
docker network create takserver-net-hardened-"$(cat tak/version.txt)"
+

Ensure in the db-utils/pg_hba.conf file that there is an entry for the subnet of the hardened takserver network. To determine the subnet of the network:

+
docker network inspect takserver-net-hardened-"$(cat tak/version.txt)"
+

Or to specify the subnet on network creation:

+
docker network create takserver-net-hardened-"$(cat tak/version.txt)" --subnet=<subnet>
+
    +
  1. Build the hardened TAK Database image:
  2. +
+
docker build -t tak-database-hardened:"$(cat tak/version.txt)" -f docker/Dockerfile.hardened-takserver-db .
+
    +
  1. Run the hardened TAK Database container:
  2. +
+
docker run --name tak-database-hardened-"$(cat tak/version.txt)" --network takserver-net-hardened-"$(cat tak/version.txt)" --network-alias tak-database -d tak-database-hardened:"$(cat tak/version.txt)" -p 5432:5432
+

TAK Server Hardened Container Setup

+
    +
  1. Build the hardened TAK Server image:
  2. +
+
docker build -t takserver-hardened:"$(cat tak/version.txt)" -f docker/Dockerfile.hardened-takserver .
+
    +
  1. Run the hardened TAK Server container:
  2. +
+
docker run --name takserver-hardened-"$(cat tak/version.txt)" --network takserver-net-hardened-"$(cat tak/version.txt)" -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -t -d takserver-hardened:"$(cat tak/version.txt)"
+

Configuring Certificates

+
    +
  1. Get the admin certificate fingerprint
  2. +
+
docker exec -it ca-setup-hardened bash -c "openssl x509 -noout -fingerprint -md5 -inform pem -in files/admin.pem | grep -oP 'MD5 Fingerprint=\K.*'"
+
    +
  1. Add the certificate fingerprint as the admin after the hardened TAK server container has started (about 60 seconds)
  2. +
+
docker exec -it takserver-hardened-"$(cat tak/version.txt)" bash -c 'java -jar /opt/tak/utils/UserManager.jar usermod -A -f <admin cert fingerprint> admin'
+

Useful Commands

+

To run these commands on the hardened containers, add the -hardened suffix to the container names.

+
    +
  • View images:
  • +
+
docker images takserver
docker images takserver-db
+
    +
  • View containers +All: 'docker ps -a' +Running: 'docker ps' +Stopped: 'docker ps -a | grep Exit'
  • +
  • Exec into container
  • +
+
docker exec -it takserver-"$(cat tak/version.txt)" bash
docker exec -it takserver-db-"$(cat tak/version.txt)" bash
+
    +
  • Exec command in container
  • +
+
docker exec -it takserver-"$(cat tak/version.txt)" bash -c  "<command>"
docker exec -it takserver-db-"$(cat tak/version.txt)" bash -c "<command>"
+
    +
  • Tail takserver logs
  • +
+
tail -f tak/logs/takserver-messaging.log
tail -f tak/logs/takserver-api.log
+
    +
  • Restart TAK server
  • +
+
docker exec -d takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && ./configureInDocker.sh"
+
    +
  • Start/Stop container:
  • +
+
docker <start/stop> takserver-"$(cat tak/version.txt)"
docker <start/stop> takserver-db-"$(cat tak/version.txt)"
+
    +
  • Remove container:
  • +
+
docker rm -f takserver-"$(cat tak/version.txt)"
docker rm -f takserver-db-"$(cat tak/version.txt)"
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/dockerinstall/ironbank/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/dockerinstall/ironbank/index.html new file mode 100644 index 00000000..dad82133 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/dockerinstall/ironbank/index.html @@ -0,0 +1,16 @@ + + + + + +IronBank | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/datapackagemissionfileblocker/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/datapackagemissionfileblocker/index.html new file mode 100644 index 00000000..1e28e6c2 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/datapackagemissionfileblocker/index.html @@ -0,0 +1,16 @@ + + + + + +Data Package and Mission File Blocker | TAK Server Documentation + + + + + +

Data Package and Mission File Blocker

+

Data packages, mission packages, and missions can be federated between servers and their respective ATAK clients. These packages may contain configuration files such as ATAK .pref files that can result in the distribution of unwanted configuration changes to ATAK devices. A filter can be enabled to block files by file-type. To enable this feature, check the Data Package and Mission File Blocker box in the Federation Configuration page. The default file extension value is 'pref'. This can be changed by entering a new file type, clicking on the 'Add' button to add the entry, and clicking on the 'Save' buton at the bottom of the Federation Configuration page.

+

Data Package and Mission File Blocker Box

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/enablefederation/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/enablefederation/index.html new file mode 100644 index 00000000..ad1e95cf --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/enablefederation/index.html @@ -0,0 +1,17 @@ + + + + + +Enable Federation | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federatedgroupmapping/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federatedgroupmapping/index.html new file mode 100644 index 00000000..267ad3f6 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federatedgroupmapping/index.html @@ -0,0 +1,18 @@ + + + + + +Federated Group Mapping | TAK Server Documentation + + + + + +

Federated Group Mapping

+

The flow of traffic between Federates may be directed using end-to-end group mapping. The Federated Group Mapping section is on the Federate Groups page.

+

Groups are exchanged during active connections between Federates. The remote groups will appear in the ‘Remote Group List’ drop down in the Federated Group Mapping section. Connected Federates must have Federated Group Mapping enabled in order for the Federates to exchange their respective remote groups. This parameter is in the Federation Configuration section in the Configuration > Manage Federates page.

+

To configure the end-to-end mapping, select a remote group and map it to a local Federate group. Remote groups may also be entered directly in the ‘Remote Group’ field. A single remote group can be mapped to many local groups. Additionally, multiple end-to-end group mappings may be defined. With a group mapping configured, traffic from the remote group will only flow to the mapped local group(s). Note: if no incoming traffic matches the remote groups configured, the federation traffic will fall back to the Federate Group scheme described previously.

+

Remote Groups

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federationdisruptiontolerance/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federationdisruptiontolerance/index.html new file mode 100644 index 00000000..c4d225e2 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federationdisruptiontolerance/index.html @@ -0,0 +1,22 @@ + + + + + +Mission Federation Disruption Tolerance | TAK Server Documentation + + + + + +

Mission Federation Disruption Tolerance

+

Traffic between federated servers may be disrupted, and updates to missions could happen during that disruption. Mission federation disruption tolerance will update each server with changes to federated missions that occured during the disruption. To enable this feature, check the box in the Federation Configuration page:

+

Enable Disruption Tolerance

+

Sending all the changes that occured between disruptions could potentially take a lot of bandwidth, so by default, we limit the changes to those that occured within the last 2 days. For example, if a disruption lasted 3 weeks, we would only send the changes from the previous 2 days. However, if the disruption only lasted a few hours, only the changes since the last disruption would be sent. If the Unlimited checkbox is checked, then all changes since the last disruption would be sent. The 2 day limit can be changed to any length with the Send Changes Newer Than setting, and the period can be selected as days, hours, or seconds.

+

It is also possible to override the global setting for a particular mission, if so desired.

+

Specific Mission Settings

+

For example, in the above image, we see that mission_2 will send updates up to over the last 10 days, mission_2 only over the last 12 hours, and mission_4 will send all updates since the last disruption. Any mission that is not listed, and any subsequently added mission, will follow the global setting of 2 days, as set above.

+

Clear Federation Events

+

The Clear Federation Events button will reset the disruption history for federation. This means that on the next reconnection, the server will send the max allowed mission changes according to the Mission Federation Disruption Tolerance settings. In the above case, that would be 10 days for mission_2, 12 hours for mission_3, and the entire change history for mission_4. For all other missions this would be 2 days worth of mission changes.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federationexample/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federationexample/index.html new file mode 100644 index 00000000..96daf11b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/federationexample/index.html @@ -0,0 +1,21 @@ + + + + + +Federation Example | TAK Server Documentation + + + + + +

Federation Example

+

The figure below shows a connectivity graph of two distinct administrative domains. Each administrative domain has multiple sub-groups (e.g. "CN=Alpha") utilizing the group-filtering. The color coding indicates the CA that is used to sign the certificate used for connections. Enclave 1's CA signs ATAK client certs and a server certificate. Enclave 2's CA also signs ATAK client certs and a server cert. The trust-store listing the allowed CAs for the "User Port" only contains a single CA (i.e. Enclave 1 CA for Enclave 1). To federate the servers, Enclave 1 and Enclave 2 send each other the "public" CA cert. Those certificates are put in a separate trust store that is used only for federation connections. The “Fed. Port” is configured with this separate trust-store. +The server cert from each administrative domain can now be used to connect to the "Fed. Port" of the other domain.

+

Federation Example

+

Alternate Configuration

+

The first example had each federate using the same CA and server certificate for local and federate connections. If you are very paranoid, or don't want to share anything about the crypto being used for local clients, you can have a wholly separate CA+server certificate chain that is used for federation. Figure 5 shows how this would work.

+

Alternate Federation Example

+

This adds some complexity, but can be used if you don't want to expose your 'internal' CA to the organizations that you are federating with.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/maketheconnection/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/maketheconnection/index.html new file mode 100644 index 00000000..8b1d5f18 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/maketheconnection/index.html @@ -0,0 +1,18 @@ + + + + + +Make the Connection | TAK Server Documentation + + + + + +

Make the Connection

+

Now that we have enabled federation and shared our CA with the other TAK server authority, we are ready to make the connection and start sharing. For this step, only ONE of the servers creates an outgoing connection to the other. If you are starting the connection, go back to the Manage Federates page where you enabled federation from step 1. You will now see three sections, Active Connections, Outgoing Connection Configuration, and Federate Configuration. To create an outgoing connection, click on the corresponding link, and enter in the address and port of the destination server. You can also pick the protocol version (make sure it is the right protocol for the port you are connecting to!), reconnection interval (how long between retries if the connection is lost), and whether or not the connection will be enabled on creation.

+

Active Connections

+

Now that you have created and started a connection, you will notice that no information is yet flowing between federates. This is because you and your fellow federate must specify which filtering groups you will allow to flow out of and into your server. To manage this, click on the Manage Groups link in the corresponding row of the Federate Configuration section. Here you can specify the groups, including the special __ANON__ group if you want. Once both servers have configured the groups, traffic will start to flow. A server restart is not necessary for these changes to take effect.

+

Federate Groups

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/overview/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/overview/index.html new file mode 100644 index 00000000..261c4e73 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/overview/index.html @@ -0,0 +1,20 @@ + + + + + +Overview | TAK Server Documentation + + + + + +

Overview

+

Federation will allow subsets of ATAK users who are connected to different servers to work together, even though each TAK server instance (hereafter refered to as 'federates') may be run by independent organizations / administrative domains. It brings some of the following benefits/restrictions:

+
    +
  1. Each administrative domain does not need to share anything about their internal structure (e.g. LDAP/Active Directory information / users) with the other administrative domain.
  2. +
  3. Each administrative domain has control over what data they share with the other domain, but has no control over what the other administrative domain does with data that is shared.
  4. +
  5. It requires no reconfiguration of ATAKs connected to either TAK Server, and the mechanism for connecting the TAK Servers does not allow direct connections of ATAK devices from the other administrative domain.
  6. +
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/uploadfederatecert/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/uploadfederatecert/index.html new file mode 100644 index 00000000..73630913 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/federation/uploadfederatecert/index.html @@ -0,0 +1,17 @@ + + + + + +Upload Federate Certificate | TAK Server Documentation + + + + + +

Upload Federate Certificate

+

In order for the federate servers to trust each other and their ATAK clients, they must share each others certificate authorities (CAs) in order to create a separate federate trust-store. One of the key components to how TAK Server satisfies all the restrictions is that we use one trust-store for local users, and one for Federates. The trust-store contains all the valid CAs that you will allow client certificates from. By having separate trust-stores, we can have the Federation channels allow connections with certificates from “outside” CAs, while not allowing ATAKs with certs from those “outside” CAs to make direct connections to our server.

+

Federate Certificate Authorities Page

+

Generally, we share the public CA, which you can find at /opt/tak/cert/files/ca.pem, via some third channel such as email or a USB drive. Once you have traded CAs, go the the Manage Federate Certifate Authorities page and upload the CA of the federate you want to connect to.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/overview/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/overview/index.html new file mode 100644 index 00000000..1da5a7ba --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/overview/index.html @@ -0,0 +1,17 @@ + + + + + +Overview | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/rhelrockycentos/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/rhelrockycentos/index.html new file mode 100644 index 00000000..3706cfa8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/rhelrockycentos/index.html @@ -0,0 +1,24 @@ + + + + + +RHEL, Rocky Linux, and CentOS | TAK Server Documentation + + + + + +

RHEL, Rocky Linux, and CentOS

+

To verify whether a firewall is running, use the command:

+
sudo systemctl status  firewalld.service
+

To see what zones are running

+
sudo firewall-cmd --get-active-zones
+

If you are working from a fresh OS install, the only active zone is 'public'.

+

For each each zone, you'll want to enable TCP (and possibly UDP) ports for the inputs in your CoreConfig.xml file, plus the web server's port. For example,

+
sudo firewall-cmd --zone=public --add-port 8089/tcp -permanent
sudo firewall-cmd --zone=public --add-port 8443/tcp --permanent
+

The ports you'll need to open for the default configuration are 8089 and 8443.

+

Finally, enable your new firewall rules:

+
sudo firewall-cmd -reload
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/ubunturaspberrypi/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/ubunturaspberrypi/index.html new file mode 100644 index 00000000..c016af44 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/firewall/ubunturaspberrypi/index.html @@ -0,0 +1,25 @@ + + + + + +Ubuntu and Raspberry Pi | TAK Server Documentation + + + + + +

Ubuntu and Raspberry Pi

+

UFW (Uncomplicated Firewall) is a utility for managing firewalls. If is not installed on your server. Install with the following:

+
sudo apt install ufw
+

IMPORTANT: Raspberry Pi installs, please reboot your device after installing ufw.

+

To check the status of the firewall service with current port rules:

+
sudo ufw status
+

Perform the following commands to set initial rules for your firewall:

+
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
+

Turn on the firewall:

+
sudo ufw enable
+

Add default configuration TAK Server ports:

+
sudo ufw allow 8089
sudo ufw allow 8443
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/groupfilteringformulticast/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/groupfilteringformulticast/index.html new file mode 100644 index 00000000..a5f81fe0 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/groupfilteringformulticast/index.html @@ -0,0 +1,20 @@ + + + + + +Group Filtering for Multicast Networks | TAK Server Documentation + + + + + +

Group Filtering for Multicast Networks

+

The proxy attribute on the CoreConfig input element ( <input … proxy="true" … /> ) was removed in TAK Server 4.1. The intent of the proxy attribute was to control bridging of multicast networks and federating multicast data. As TAK Server’s group filtering capabilities have evolved, having a dedicated proxy attribute is no longer needed. Using filtergroup on the mcast input you can achieve greater control over multicast traffic. +The default behavior in TAK Server 4.1 and higher is to put multicast traffic in the __ANON__ group. You can use a filtergroup on the mcast input to put your mcast traffic into a dedicated multicast group, for example:

+
<input auth="anonymous" _name=" SAproxy " protocol="mcast" port="6969" group="239.2.3.1">
<filtergroup>__MCAST__</filtergroup>
</input>
+

Then add the __MCAST__ group as a filtergroup on any other inputs you wanted to share multicast traffic with. For example, to share multicast traffic with the tls/8089, configure your input filtergroups as follows:

+
<input auth="anonymous" _name="stdssl1" protocol="tls" port="8089" archive="true">
<filtergroup>__ANON__</filtergroup>
<filtergroup>__MCAST__</filtergroup>
</input>
+

This same approach works for federations. You can __MCAST__ as an outboundGroup on any federates that you wanted to share multicast traffic with. Using the filtergroup approach allows for creation of input specific multicast groups, allowing control of how messages from multicast networks are routed.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/imagetest/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/imagetest/index.html new file mode 100644 index 00000000..d6104b7f --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/imagetest/index.html @@ -0,0 +1,17 @@ + + + + + +Image Test | TAK Server Documentation + + + + + +

Image Test

+

This is just a section demonstrating images.

+

Dog

+

This is some text after the image.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/prerequisite/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/prerequisite/index.html new file mode 100644 index 00000000..ef9174bb --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/prerequisite/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/takserverconfiguration/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/takserverconfiguration/index.html new file mode 100644 index 00000000..597382b2 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/takserverconfiguration/index.html @@ -0,0 +1,42 @@ + + + + + +Configure TAK Server Installation | TAK Server Documentation + + + + + +

Configure TAK Server Installation

+
sudo systemctl daemon-reload
+

On resource limited hosts, such as a Raspberry Pi, you may start/stop only essential api and messaging TAK Server services with:

+
sudo systemctl [start|stop] takserver-noplugins
+

Otherwise, to start/stop all TAK Server service:

+
sudo systemctl [start|stop] takserver
+

You can set TAK Server to start at boot by running

+
sudo systemctl enable takserver
+

or with resource limited hosts:

+
sudo systemctl enable takserver-noplugins
+

For secure operation, TAK Server requires a keystore and truststore (X.509 certificates).

+

Next, follow the instructions in Appendix B to create these certificates. TAK Server by default is TLS only, so certificate generation, including an administrative certificate is required for configuration. In addition, if you would like to configure TLS for Postgres database connection, follow additional steps in Appendix D.

+

Verify that the steps in Appendix B have been followed by checking the following items:

+

Certificates are present at:

+
/opt/tak/certs/files
+

The TAK Server was restarted, the admin cert has been generated, and an admin account in TAK Server was created with the command:

+
sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem
+

While following the instructions in Appendix B, you will have created an admin certificate. Import this certificate into your browser, so that you can access the Admin. It will be located here on your TAK Server machine:

+
/opt/tak/certs/files/admin.pem
+

Import this client certificate into your browser.

+

If you are using Firefox, go to Settings -> Preferences -> Privacy & Security -> Certificates -> View Certificates

+

Go to Import. Upload this file:

+
/opt/tak/certs/files/admin.p12
+

Enter the certificate password. The default password is atakatak

+

Browse to:

+
https://localhost:8443
+

Select the admin certificate to log in.

+

An error message similar to this indicates that the correct client certificate has not been imported into the browser:

+

Example Incorrect Client Certificate Error Message

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/takserverinstallation/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/takserverinstallation/index.html new file mode 100644 index 00000000..637fbed8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/oneserver/takserverinstallation/index.html @@ -0,0 +1,80 @@ + + + + + +TAK Server Installation | TAK Server Documentation + + + + + +

TAK Server Installation

+

Rocky Linux 8

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable PowerTools (needed for dependencies of postgis.) Install TAK server. Apply SELinux takserver-policy.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install epel-release -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql && sudo dnf update -y
sudo dnf install java-17-openjdk-devel -y
sudo dnf config-manager --set-enabled powertools
sudo dnf install takserver-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

RHEL8 FIPS mode Support. TAK Server has experimental support for RHEL8 FIPS mode. This is intended for evaluation only, for hardened environments. These steps enable TAK Server to operate with RHEL FIPS mode enabled, but does not provide full FIPS 140 compliance. See below for a new option for certs script when using FIPS mode. Client certificates generated with FIPS mode may not work with ATAK.

+
sudo rpm --import http://download.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-8
sudo rpm --import https://download.postgresql.org/pub/repos/yum/RPM-GPG-KEY-PGDG
sudo vi /etc/fapolicyd/rules.d/99-whitelist.rules
+

Add line:

+
deny perm=any all : all
+
sudo vi /etc/fapolicyd/rules.d/39-tak.rules
+

Add lines:

+
allow perm=open all : dir=/opt/tak/ ftype=application/x-sharedlib trust=0
allow perm=open exe=/usr/pgsql-15/bin/postgres : all
+
    +
  • Custom certificates with stronger algorithms will need to be generated for use on systems with FIPS enabled. To do this: follow the existing certificate instructions, but append --fips to the end of each ./makeRootCa.sh and ./makeCert.sh command. Note: ATAK may not support certificates generated with these stronger algorithms*
  • +
+

--- End FIPS Mode Commands ---

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable Repository Management and repository CodeReady Builder. Install TAK server. Apply SELinux takserver-policy.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with:

+
sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach
+
sudo dnf install takserver-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install TAK server

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -ivh takserver-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu and Raspberry Pi OS

+

Install the postgres repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install TAK server

+
sudo mkdir -p /etc/apt/keyrings
sudo curl https://www.postgresql.org/media/keys/ACCC4CF8.asc --output /etc/apt/keyrings/postgresql.asc
sudo sh -c 'echo "deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/postgresql.list'
sudo apt update
sudo apt install ./takserver-5.2-RELEASEx_all.deb
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

CentOS 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -ivh takserver-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on CentOS 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

yum Install From tak.gov

+

Check to see if a .repo file exists for tak.gov

+
ls /etc/yum.repos.d/Takserver.repo
+

If it exists, skip down to the yum install of takserver database. If it doesn't exist, create a new .repo file to point yum to the yum repo on tak.gov.

+
cd /etc/yum.repos.d
sudo vi Takserver.repo
+

You can also edit using another editor besides vi. Update the file Takserver.repo to contain:

+
[takrepo]
name=TakserverRepository
baseurl=https://<ARTIFACTORY_USER>:<ARTIFACTORY_TOKEN>@artifacts.tak.gov/artifactory/takserver-yum
enabled=1
gpgcheck=0
+

Where <ARTIFACTORY_USER> is a valid login to Artifactory that has access to the takserver-yum repo and <ARTIFACTORY_TOKEN> is a special token unique to the given Artifactory user that can be retrieved by that user using the "Set Me Up" menu option to retrieve it.

+

Note: Do NOT use your password as it the Token is more secure and cannot be used for logging in. Only for retrieving or publishing. Also note: When you change the password of the given user, you will also need to retrieve the new token which is based on it and update the baseurl in the Takserver.repo file.

+

Save the Takserver.repo file and then do the install of the takserver.

+
sudo yum install takserver-core-5.2-RELEASEx
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/overview/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/overview/index.html new file mode 100644 index 00000000..ef2313bb --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/overview/index.html @@ -0,0 +1,74 @@ + + + + + +Overview and Installer Files | TAK Server Documentation + + + + + +

Overview and Installer Files

+

TAK Server supports multiple deployment configurations:

+
    +
  • Single server install: One server running TAK Server core (messaging, API, plugins and database): recommended for fewer than 500 users.
  • +
  • Two server install: One server running TAK Server core (messaging, API, plugins and database) and a second server running PostgreSQL database: recommended for more than 500 users.
  • +
  • Containerized docker install: One container running TAK Server core (messaging, API, plugins and database) and another container running PostgreSQL database (designed for operating systems other than CentOS 7 / RHEL 7). Hardened containers are published to both tak.gov and IronBank (see https://ironbank.dso.mil/repomap?searchText=tak%20server).
  • +
+

The following installation files are provided:

+

Installer for single-server install

+
    +
  • RHEL/Rocky/CentOS: takserver-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver_5.2-RELEASE-x_all.deb
  • +
+

Database installer for two-server install

+
    +
  • RHEL/Rocky/CentOS: takserver-database-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver-database_5.2-RELEASE-x_all.deb
  • +
+

Core installer for two-server install

+
    +
  • RHEL/Rocky/CentOS: takserver-core-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver-core_5.2-RELEASE-x_all.deb
  • +
+

Containerized docker install bundle

+
    +
  • takserver-docker-5.2-RELEASE-x.zip
  • +
+

Containerized hardeneded docker install bundle

+
    +
  • takserver-docker-hardened-5.2-RELEASE-x.zip
  • +
+

Installer for federation hub (beta)

+
    +
  • RHEL/Rocky/CentOS: takserver-fed-hub-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver-fed-hub_5.2-RELEASE-x_all.deb
  • +
+

Federation hub documentation available here: +https://wiki.tak.gov/display/TPC/Federation+Hub

+

Verifying GPG signatures

+

Verifying GPG Signatures on RPM Packages

+

The GPG public key for TAK Server can be found under +https://artifacts.tak.gov/ui/repos/tree/General/TAKServer/release/

+

Select the TAK Server release version and download the file takserver-public-gpg.key

+

Import the key to the RPM key management:

+
sudo rpm --import takserver-public-gpg.key
+

Verifying signature for the rpm installer package:

+
rpm --checksig takserver-5.2-RELEASE<version>.noarch.rpm
+

Example of a successful output:

+
takserver-5.2-RELEASE28.noarch.rpm: rsa sha1 (md5) pgp md5 OK
+

Example of a failed output:

+
takserver-5.2-RELEASE28.noarch.rpm: RSA sha1 ((MD5) PGP) md5 NOT OK
(MISSING KEYS: (MD5) PGP#6851f5b5)
+

If the RPM packages were not signed with a GPG key, the output might look like:

+
takserver-5.2-RELEASE25.noarch.rpm: sha1 md5 OK
+

Verifying GPG signatures on DEB packages

+

Select the appropriate TAK Server release version and download the file takserver-public-gpg.key and deb_policy.pol

+

Install the debsig-verify utility:

+
sudo apt install debsig-verify
+

Using the ID within the deb_policy.pol file, ex. 039FCDA2D8907527, run the following command to verify signed TAK Server deb resources:

+
sudo mkdir /usr/share/debsig/keyrings/039FCDA2D8907527
sudo mkdir /etc/debsig/policies/039FCDA2D8907527
sudo touch /usr/share/debsig/keyrings/039FCDA2D8907527/debsig.gpg
sudo gpg --no-default-keyring --keyring /usr/share/debsig/keyrings/039FCDA2D8907527/debsig.gpg --import takserver-public-gpg.key
sudo cp deb_policy.pol /etc/debsig/policies/039FCDA2D8907527/debsig.pol
debsig-verify -v takserver-5.2-RELEASE_all.deb
+

Confirm signature verification by identifying statement:

+
debsig: Verified package from 'TAK Product Center' (TAK Server Release)
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/setup_wizard/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/setup_wizard/index.html new file mode 100644 index 00000000..d4ea30f1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/setup_wizard/index.html @@ -0,0 +1,22 @@ + + + + + +Use Setup Wizard to Configure TAK Server | TAK Server Documentation + + + + + +

Use Setup Wizard to Configure TAK Server

+

The TAK Server configuration wizard will help you set up common configuration options once you have installed and started TAK Server. The wizard will guide you through the setup process for a secure configuration, using the default ports that ATAK and WinTAK will connect to.

+

Once you have created your adminstrative login credentials as in the previous section, go to:

+

https://localhost:8443/setup/ (Recommended. Uses the more secure client certificate)

+

Then follow the prompts to begin configuring. The wizard will first walk you through recommended security configuration:

+

Security Configuration

+

NOTE: Insecure ports are a potential security risk and may allow attackers to gain access to the system resulting in the disclosure of personal and sensitive information. Use of unencrypted ports should be avoided to ensure a secure TAK Server deployment.

+

Followed by the recommended federation configuration, if you wish to set up your TAK Server to support federation. (For more information on federation, go to section 8):

+

Federation Configuration

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/overview/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/overview/index.html new file mode 100644 index 00000000..f0dc2c20 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/overview/index.html @@ -0,0 +1,15 @@ + + + + + +Overview | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/serverone/dependencysetup/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/serverone/dependencysetup/index.html new file mode 100644 index 00000000..70309d2d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/serverone/dependencysetup/index.html @@ -0,0 +1,67 @@ + + + + + +Dependency Setup | TAK Server Documentation + + + + + +

Dependency Setup

+

First, update firewall rules to allow communication with server two, for TCP port 5432.

+

Rocky Linux 8

+

Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Enable the 'powertools' repo for postgis dependencies. Install TAK Server RPM database and its dependencies.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install epel-release -y

sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y

sudo dnf update -y

sudo dnf module disable postgresql

sudo dnf install java-17-openjdk-devel -y

sudo dnf config-manager --set-enabled powertools

Make sure the database RPM is in the current directory

sudo dnf install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
+
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with "sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach"

+

Make sure the database RPM is in the current directory

+
sudo dnf install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Setup the extra postgres yum repo for the latest postgres and postgis. Install OpenJDK 17 and other dependencies. Install TAK Server RPM database.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y

sudo yum update -y

sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Make sure the database RPM is in the current directory

+
sudo rpm -ivh takserver-database-5.2-RELEASEx.noarch.rpm --nodeps
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu & Raspberry Pi OS

+

Install the postgres repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install TAK Server database. Use database DEB. Configure TAK Server Database installation

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo mkdir -p /etc/apt/keyrings

sudo curl https://www.postgresql.org/media/keys/ACCC4CF8.asc --output /etc/apt/keyrings/postgresql.asc

sudo sh -c 'echo "deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/postgresql.list'

sudo apt update

sudo apt install takserver-database-5.2-RELEASEx_all.deb
+

Open the file /opt/tak/CoreConfig.example.xml and look for the auto-generated password for the database. This password will be used to configure the Core Server.

+
<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" password="Database_password" />
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

CentOS 7

+

Setup the extra postgres yum repo for the latest postgres and postgis. Install OpenJDK 17 and other dependencies. Install TAK Server RPM database.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install epel-release -y

sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y

sudo yum update -y

sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Make sure the database RPM is in the current directory

+
sudo yum install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/configuretakserver/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/configuretakserver/index.html new file mode 100644 index 00000000..1a561222 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/configuretakserver/index.html @@ -0,0 +1,50 @@ + + + + + +Configuration | TAK Server Documentation + + + + + +

Configuration

+

Configure database connection by updating /opt/tak/CoreConfig.xml:

+
<repository enable="true" numDbConnections="200" primaryKeyBatchSize="500"
insertionBatchSize="500">
<connection url="jdbc:postgresql://<Database_server_IP_address>:5432/cot" username="martiuser"
password="Database_password"/>
</repository>
+
sudo systemctl daemon-reload
+

Start/stop TAK Server services with:

+
sudo systemctl [start|stop] takserver
+

Or on resource limited hosts:

+
sudo systemctl [start|stop] takserver-noplugins
+

You can set TAK Server to start at boot by running

+
sudo systemctl enable takserver
+

For secure operation, TAK Server requires a keystore and truststore (X.509 certificates).

+

Next, follow the instructions in Appendix B to create these certificates. TAK Server by default is TLS only, so certificate generation, including an administrative certificate is required for configuration.

+

Verify that the steps in Appendix B have been followed by checking the following items:

+

Certificates are present at:

+
/opt/tak/certs/files
+

The admin cert has been generated and an admin account in TAK Server was created with the command:

+
sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem
+

Import this client certificate into your browser.

+

If you are using Firefox, go to Settings -> Preferences -> Privacy & Security -> Certificates -> View Certificates

+

Go to Import. Upload this file:

+
/opt/tak/certs/files/admin.p12
+

Enter the certificate password. The default password is atakatak

+

Browse to:

+
https://localhost:8443
+

Select the admin certificate to log in.

+

An error message similar to this indicates that the correct client certificate has not been imported into the browser:

+

Example Incorrect Client Certificate Error Message

+

Once logged in with the admin certificate, configure the TAK Server with the following instructions:

+

Configure TAK Server to connect to the database. Access the Database configuration settings:

+

Example Incorrect Client Certificate Error Message

+

Edit the database connection address, specifying the hostname or IP address of the database server:

+

Example Incorrect Client Certificate Error Message

+

Restart TAK Server

+
sudo systemctl restart takserver
+

Or on resource limited hosts

+
sudo systemctl restart takserver-noplugins
+

If you would like to configure TLS for Postgres database connection, refer to Appendix D.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/installtakserver/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/installtakserver/index.html new file mode 100644 index 00000000..f706658a --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/installtakserver/index.html @@ -0,0 +1,62 @@ + + + + + +Install TAK Server | TAK Server Documentation + + + + + +

Install TAK Server

+

Rocky Linux 8

+
sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linuxx64_bin.rpm
sudo rpm -ivh takserver-core-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu & Raspberry Pi OS

+
sudo apt install takserver-core-5.2-RELEASEx_all.deb
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

CentOS 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linuxx64_bin.rpm
sudo rpm -ivh takserver-core-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

yum install From tak.gov

+

Check to see if a .repo file exists for tak.gov

+
ls /etc/yum.repos.d/Takserver.repo
+

If it exists, skip down to the yum install of takserver database. If it doesn't exist, create a new .repo file to point yum to the yum repo on tak.gov.

+
cd /etc/yum.repos.d
sudo vi Takserver.repo
+

You can also edit using another editor besides vi. Update the file Takserver.repo to contain:

+
[takrepo]
name=TakserverRepository
baseurl=https://<ARTIFACTORY_USER>:<ARTIFACTORY_TOKEN>@artifacts.tak.gov/artifactory/takserver-yum
enabled=1
gpgcheck=0
+

Where <ARTIFACTORY_USER> is a valid login to Artifactory that has access to the takserver-yum repo and <ARTIFACTORY_TOKEN> is a special token unique to the given Artifactory user that can be retrieved by that user using the "Set Me Up" menu option to retrieve it.

+

Note: Do NOT use your password as it the Token is more secure and cannot be used for logging in. Only for retrieving or publishing.

+

Also note: When you change the password of the given user, you will also need to retrieve the new token which is based on it and update the baseurl in the Takserver.repo file.

+

Save the Takserver.repo file and then do the install of the takserver.

+
sudo yum install takserver-core-5.2-RELEASEx
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/prerequisites/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/prerequisites/index.html new file mode 100644 index 00000000..d5109dc8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/installation/twoserver/servertwo/prerequisites/index.html @@ -0,0 +1,17 @@ + + + + + +Prerequisites | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/logging/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/logging/index.html new file mode 100644 index 00000000..673191f5 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/logging/index.html @@ -0,0 +1,16 @@ + + + + + +Logging | TAK Server Documentation + + + + + +

Logging

+

TAK Server provides several log files to provide information about relevant events that happen during execution. The log files are located in the /opt/tak/logs directory. This table shows the name of the log files, and their function.

+
Name of Log FilePurpose
takserver-messaging.logExecution-level information about the messaging process, including client connection events, error messages and warnings.
takserver-api.logExecution-level information about the API process, including error messages and warnings.
takserver-messaging-console.logJava Virtual Machine (JVM) informational messages and errors, for the messaging process.
takserver-api-console.logJava Virtual Machine (JVM) informational messages and errors, for the API process.
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/metrics/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/metrics/index.html new file mode 100644 index 00000000..036ad40c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/metrics/index.html @@ -0,0 +1,21 @@ + + + + + +Metrics | TAK Server Documentation + + + + + +

Metrics

+

The TAK Server Metrics Dashboard is available in the Monitoring menu. The dashboard continuously renders the following information:

+

Server Start Time and Server Up Time This tell you when the server was turned on and how long it has been operating.

+

Clients Connected This tells you how many connections your client is currently servicing. This corresponds to the number of clients that are displayed in the client dashboard.

+

Heap Usage TAK server runs inside one or more Java Virtual Machines (JVM). Heap Commited is how much heap memory in MB is allocated to the API process for TAK Server, and Heap Used is how much of that is currently being used.

+

Network I/O and Reads/Writes This tells you how much TCP and UDP traffic the server is currently handling, as well as a brief history.

+

CPU Usage How much of the CPU of the machine the server is running on is currently being used.

+

Metrics Page

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/oath2authentication/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/oath2authentication/index.html new file mode 100644 index 00000000..05ce9378 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/oath2authentication/index.html @@ -0,0 +1,15 @@ + + + + + +OAuth2 Authentication | TAK Server Documentation + + + + + +

OAuth2 Authentication

+

TAK Server provides OAuth2 Authorization and Resource server capabilities using the OAuth2 Password authentication flow. OAuth2 integration works with existing authentication back ends, allowing TAK Server to issue tokens backed by the File or LDAP authenticators. TAK Server issues JSON Web Tokens (JWT) signed by the server certificate, allowing external systems to validate tokens against the server’s trust chain. The OAuth2 token endpoint is available at https://<takserver>:8446/oauth/token.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/softwareinstallationlocation/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/softwareinstallationlocation/index.html new file mode 100644 index 00000000..0436859d --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/softwareinstallationlocation/index.html @@ -0,0 +1,16 @@ + + + + + +Software Installation Location | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/awsrequirements/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/awsrequirements/index.html new file mode 100644 index 00000000..36cf5041 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/awsrequirements/index.html @@ -0,0 +1,28 @@ + + + + + +AWS / GovCloud Recommended Instance Type | TAK Server Documentation + + + + + +

AWS / GovCloud Recommended Instance Type

+
    +
  • c5.xlarge +
      +
    • 4 vCPU
    • +
    • 8 GB RAM
    • +
    • Up to 10 Gbps network bandwidth
    • +
    +
  • +
  • For 2-server installation, use this instance type for both servers.
  • +
+

TAK Server is a TLS-enabled networking server. In order to ensure consistent performance, burstable AWS EC2 instance types such as T2 are not recommended. TLS and TCP processing requires consistent, continuous CPU performance. C4 and C5 instances are designed for predictable CPU performance, and are better-suited for TAK Server deployments.

+

More information about instance types may be found here: +https://aws.amazon.com/ec2/instance-types

+

Usage of larger instance types or physical servers is supported for scalability, to support more concurrent active users.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/serverrequirements/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/serverrequirements/index.html new file mode 100644 index 00000000..cf8e60d8 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/serverrequirements/index.html @@ -0,0 +1,21 @@ + + + + + +Server Requirements | TAK Server Documentation + + + + + +

Server Requirements

+
    +
  • 4 processor cores
  • +
  • 8 GB RAM
  • +
  • 40 GB disk storage
  • +
+

For Raspberry Pi installations, a Pi 4, Model B, Quad-Core 64-bit 8GB RAM version is recommended for a minimal TAK Server setup (TAK Server messaging and api services with local PostgreSQL database)

+

NOTE: Insecure ports are a potential security risk and may allow attackers to gain access to the system resulting in the disclosure of personal and sensitive information. Use of unencrypted ports should be avoided to ensure a secure TAK Server deployment.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/systemrequirements/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/systemrequirements/index.html new file mode 100644 index 00000000..d381fd7c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/system-requirements/systemrequirements/index.html @@ -0,0 +1,22 @@ + + + + + +Supported Operating Systems | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/overview/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/overview/index.html new file mode 100644 index 00000000..19f00df9 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/overview/index.html @@ -0,0 +1,15 @@ + + + + + +Overview | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/singleserver/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/singleserver/index.html new file mode 100644 index 00000000..5d9392b1 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/singleserver/index.html @@ -0,0 +1,57 @@ + + + + + +Single-Server Upgrade | TAK Server Documentation + + + + + +

Single-Server Upgrade

+

Rocky Linux 8

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable PowerTools (needed for dependencies of postgis.) Install TAK server.

+
sudo dnf install epel-release -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql && sudo dnf update -y
sudo dnf install java-17-openjdk-devel -y
sudo dnf config-manager --set-enabled powertools
+

Upgrade Tak server

+
sudo dnf install takserver-5.1-RELEASEx.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

Setup the extra postgres yum repo for the latest postgres and postgis. Install Java 17. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies.

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with:

+
sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach
+

Upgrade Tak server

+
sudo dnf install takserver-5.1-RELEASEx.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

If you have not previously done so, install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Upgrade TAK server

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -Uvh takserver-5.1-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on Centos 7 and RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu and Raspberry Pi OS

+

Upgrade Tak server

+
sudo apt install ./takserver-5.1-RELEASEx_all.deb
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Centos 7

+

If you have not previously done so, install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Upgrade Tak server.

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -Uvh takserver-5.1-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on Centos 7 and RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/twoserver/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/twoserver/index.html new file mode 100644 index 00000000..67492982 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/upgrade/twoserver/index.html @@ -0,0 +1,85 @@ + + + + + +Two-Server Upgrade | TAK Server Documentation + + + + + +

Two-Server Upgrade

+

Rocky Linux 8

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, on the core server, install Java 17 and upgrade the core package:

+
sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.1-RELEASEx.noarch.rpm -y
+

Next, on the database server, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Install Java 17. Enable the 'powertools' repo for postgis dependencies. Install TAK Server RPM database and its dependencies.

+
sudo dnf install epel-release -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo dnf update -y
sudo dnf module disable postgresql
sudo dnf install java-17-openjdk-devel -y
sudo dnf config-manager --set-enabled powertools
+

Make sure the database RPM is in the current directory

+
sudo dnf install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, on the core server, install Java 17 and upgrade the core package:

+
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.1-RELEASEx.noarch.rpm -y
+

Next, on the database server, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install Java 17. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies.

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with:

+
sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach
+

Make sure the database RPM is in the current directory

+
sudo dnf install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Install OpenJDK 17 and other dependencies (if you have not previously done so.)

+
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8. +Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, upgrade the core package:

+
sudo rpm -Uvh takserver-core-5.1-RELEASEx.noarch.rpm --nodeps
+

Next, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install TAK Server RPM database and its dependencies.

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum update -y
+

Make sure the database RPM is in the current directory

+
sudo rpm -Uvh takserver-database-5.1-RELEASEx.noarch.rpm --nodeps
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu and Raspberry Pi OS

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, upgrade the core package.

+
sudo apt install ./takserver-core_5.1-RELEASE-x_all.deb
+

Next, upgrade the database.

+
sudo apt install ./takserver-database_5.1-RELEASE-x_all.deb
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Centos 7

+

Install OpenJDK 17 and other dependencies (if you have not previously done so.)

+
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, upgrade the core package:

+
sudo rpm -Uvh takserver-core-5.1-RELEASEx.noarch.rpm --nodeps
+

Next, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install TAK Server RPM database and its dependencies.

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum update -y
+

Make sure the database RPM is in the current directory

+
sudo yum install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/usermanagementui/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/usermanagementui/index.html new file mode 100644 index 00000000..e577611b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/usermanagementui/index.html @@ -0,0 +1,41 @@ + + + + + +User Management UI | TAK Server Documentation + + + + + +

User Management UI

+

Overview

+

The User Management UI provides an intuitive drag-and-drop mechanisms for managing TAK user accounts. The tool is integrated within TAK Server and can be accessed from the TAK main menu, under Administrative >> Manage Users. Users need to have an admin role to access the tool. Currently the User Management UI supports only file-based users and not LDAP/AD users. +The tool allows TAK administrators to create, manage, inspect and delete TAK user accounts. More specifically, the tool allows TAK administrators to:

+
    +
  • View, filter and search for existing user accounts and groups.
  • +
  • View a list of users in each group.
  • +
  • Change password for each user account.
  • +
  • View and update groups (IN group, OUT group and both) for each user account.
  • +
  • Delete user accounts.
  • +
  • Create a new user account with a specified password and groups. Password complexity is checked to confirm compliance.
  • +
  • Create new user accounts in bulk with username following a pattern. System uses password generation mechanism to create passwords that meet TAK password complexity requirements. System produces output file with user/password combos as a one-time downloadable item, after which system forgets the un-hashed passwords.
  • +
  • Create new groups.
  • +
+

Usage

+

The below figure shows the main page of the User Management UI. The left panel lists all user accounts, which can be filtered using the Search box on the top. The right panel lists all existing groups, which can be filtered using the Search box on the top.

+

Main Page

+

To change user’s password, click on the arrow right next to the username and select "Change password".

+

Change Password

+

To view/edit groups for a user account, click on the arrow right next to the username and select "View/Edit groups". You can drag the groups from the right panel and drop to one of the three boxes in the middle panel. Click on “Reset” button to bring the UI back to showing the current groups of the user. Click on “Update” button to update the groups of the user.

+

View Edit Groups

+

To delete an account, click on the arrow right next to the username and select "Delete User". You will be prompted to either confirm or cancel the action.

+

Delete Account

+

To list all users in a group, click on the arrow right next to the group name and click on "List users".

+

List Users

+

To create a new user, click on "Add User" on the menu bar.

+

Add User

+

To create new users in bulk, click on "Add Users" on the menu bar.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docs/webtak/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/docs/webtak/index.html new file mode 100644 index 00000000..32c2e533 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docs/webtak/index.html @@ -0,0 +1,18 @@ + + + + + +WebTAK | TAK Server Documentation + + + + + +

WebTAK

+

The WebTAK front-end application is bundled with TAK Server. The WebTAK back-end WebSockets networking channel and APIs are provided by TAK Server. WebTAK must be accessed using https.

+

WebTAK can be accessed either with X.509 client certificates (default https port 8443), or by username-password access using OAuth (https port 8446).

+

In either case, TAK Server must be configured with a server certificate and truststore (see Appendix B).

+

In the Admin UI menu, use Situation Awareness -> WebTAK to access WebTAK.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static-single-file.html b/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static-single-file.html new file mode 100644 index 00000000..460d834b --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static-single-file.html @@ -0,0 +1,1263 @@ + + + +Hello from TAK Server Documentation | TAK Server Documentation + + + + + +

Markdown page example

+

You don't need React to write simple standalone pages.

TAK Server Documentation

Version 5.2

First Blog Post

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Long Blog Post

· 3 min read
Endilie Yacop Sucipto

This is the summary of a very long blog post,

+

Use a <!-- truncate --> comment to limit blog post size in the list view.

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

+

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

4 posts tagged with "docusaurus"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

Welcome to Docusaurus!

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

One post tagged with "docusaurus-static"

View All Tags

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

Welcome to Docusaurus-Static!

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

One post tagged with "hola"

View All Tags

· One min read
Gao Wei

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Pellentesque elementum dignissim ultricies. Fusce rhoncus ipsum tempor eros aliquam consequat. Lorem ipsum dolor sit amet

3 posts tagged with "hello"

View All Tags

· One min read
OctoSpacc

Docusaurus-Static is a set made out of a build postprocessing script, and several static runtime files, that can be used to make any Docusaurus site magically work without a web server.

+

Not only can the built site be navigated as a collection of static HTML pages from file:/// locations or indiscriminate domains, but is now also made to be contained in a single-file HTML application. Only if you're not already on it, try opening docusaurus-static-single-file.html...

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

About TAK Server

+

TAK Server is a situational awareness server, that provides a dynamic Common Operating Picture to users of the Team Awareness Kit, including ATAK (Android), WinTAK (Windows) and WebTAK. TAK enables sharing of geolocated information in real time for military forces, law enforcement, and emergency responders. It supports both wireless and wired networks, as well as cloud and data center deployment.

Appendix B: Certificate Generation

+

TAK Server includes scripts for generating a private security enclave, which will create a Certificate Authority (CA) as well as server and client certificates.

+

First, figure out how many client certificates you are going to need. Ideally you should have a different client cert for each ATAK device on your network.

+

Become the tak user:

+
sudo su tak
+

Edit the certificate-generation configuration file, at this location:

+
/opt/tak/certs/cert-metadata.sh
+

Set options for country, state, city, organization, and organizational_unit.

+

Change directory:

+
cd /opt/tak/certs
+

Create a certificate authority (CA):

+
./makeRootCa.sh
+

Follow the prompt to name the CA.

+

Create a server certificate:

+
./makeCert.sh server takserver
+

This command will result in a server certificate named '/opt/tak/certs/files/takserver.jks'

+

Create one or more client certificates. You should use a different client cert for each ATAK device on your network. This username will be provisioned in the certificate as the CN (Common Name). When using certs on devices that are connected to an input that is configured for group filtering without authentication messages, this username will be used by TAK Server to look up group membership information in an authentication repository, such as Active Directory (AD). This command will create a cert for the username user:

+
./makeCert.sh client user
+

Generate another cert, named admin to access the admin UI:

+
./makeCert.sh client admin
+

The generated CA truststores and certs will be located here:

+
/opt/tak/certs/files
+

Follow the instruction on "Configure TAK Server Certificate" to set up the server to use the generated certs and to authenticate users on a TLS port. If using the default configuration, TLS will be correctly set up on 8443.

+

Become a normal user:

+
exit
+

Restart the TAK Server:

+
sudo systemctl restart takserver
+

Authorize the admin cert to perform administrative functions using the UI:

+
sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem
+

Configure TAK Server Certificate

+

In /opt/tak, check the following settings in CoreConfig.xml:

+
    +
  1. In the <tls> element, the keystoreFile attribute should be set to the server keystore that was generated with makeCerts.sh, above. If you followed the instructions verbatim, the server keystore is '/opt/tak/certs/files/takserver.jks'.
  2. +
  3. Also in the <tls> element, the truststoreFile attribute should be set to the the trust store that was generated with makeCerts.sh, above. If you used the default arguments, your trust store file is '/opt/tak/certs/files/truststore-root.jks'.
  4. +
  5. In the <network> element, add a TLS input, specifying group-based filtering without requiring an authentication message:
  6. +
+
<input _name="stdssl" protocol="tls" port="8089" auth="x509"/>
+

You can change the port number if you want. +Example:

+
<network multicastTTL="5">
<!-- <input _name="stdtcp" protocol="tcp" port="8087"/> -->
<!-- <input _name="stdudp" protocol="udp" port="8087"/> -->
<input _name="stdssl" protocol="tls" port="8089" auth="x509"/>
<!-- <input _name="streamtcp" protocol="stcp" port="8088"/> -->
<!-- <input _name="SAproxy" protocol="mcast" group="239.2.3.1" port="6969" proxy="true"/> -->
<!-- <input _name="GeoChatproxy" protocol="mcast" group="224.10.10.1" port="17012" proxy="true"/> -->
<!--<announce enable="true" uid="Marti1" group="239.2.3.1" port="6969" interval="1" ip="192.168.1.137" />-->
</network>
...
<security>
<tls context="TLSv1" keymanager="SunX509" keystore="JKS" keystoreFile="certs/files/takserver.jks" keystorePass="atakatak" truststore="JKS" truststoreFile="certs/files/truststore-root.jks" truststorePass="atakatak">
(Uncomment the following if you are using a CRL)
<!-- <crl _name="Marti CA" crlFile="certs/ca.crl"/> -->
</tls>
</security>
+

Then (re)start the TAK Server as normal.

+

Installing Client Certificates

+

Take the truststore-root.p12 and user.p12 files and copy them to your Android device. In ATAK, open 'Settings -> General Settings -> Network Settings' and set the SSL/TLS Truststore and Client Certificate preferences to point to those .p12 files.

+

Repeat the procedure described above for creating a new server connection, but be sure to select SSL as the protocol.

+

These same .p12 files can be installed in a browser, and used to access the Web UI (for admin use) and WebTAK (for normal users or admins). The process to install these files varies by browser and operating system, but can generally be configured by going to the browser preferences, and the security or certificates section.

Appendix A: Acronyms and Abbreviations

+
    +
  • ATAK Android Team Awareness Kit
  • +
  • CA Certificate Authority (for digital certificates)
  • +
  • CN Common Name (of a digital certificate)
  • +
  • CoT Cursor-on-Target, an XML-based data interchange format
  • +
  • CRL Certificate Revocation List
  • +
  • DoD Department of Defense (United States)
  • +
  • DISA Defense Information Systems Agency
  • +
  • ESAPI Enterprise Security Application Programming Interface
  • +
  • EPL Evaluated Products List
  • +
  • HTTP Hypertext Transfer Protocol
  • +
  • IA Information Assurance
  • +
  • IP Internet Protocol
  • +
  • IPv4 Internet Protocol, version 4. The commonly-used version of IP, in which addresses consist of four integers from zero to 255 (inclusive), separated by periods, such as 192.168.123.4
  • +
  • JCE Java Cryptography Extensions
  • +
  • JDK Java Development Kit, a JRE with additional tools and libraries.
  • +
  • JKS Java Key Store
  • +
  • JRE Java Runtime Environment
  • +
  • KML Keyhole Markup Language, the XML-based data format used by Google Earth
  • +
  • OS Operating System
  • +
  • OWASP Open Web Application Security Project
  • +
  • NIAP National Information Assurance Partnership
  • +
  • PKCS12 Public-Key Cryptography Standard #12
  • +
  • TCP Transmission Control Protocol
  • +
  • RHEL Red Hat Enterprise Linux
  • +
  • UDP User Datagram Protocol
  • +
  • SSL Secure Sockets Layer
  • +
  • TAK Team Awareness Kit, a mobile or desktop application that sends and receives real-time information through TAK Server.
  • +
  • TLS Transport Layer Security, a newer and more-secure protocol derived from SSL. The terms SSL and TLS are often used interchangeably. Technically, TLS provides a superset of SSL's capabilities and should always be preferred.
  • +
  • XML Extensible Markup Language
  • +

Appendix C: Certificate Signing

+

TAK Clients can enroll for new client certificates by submitting a Certificate Signing Request (CSR) to TAK Server. The Certificate Signing endpoint resides on port 8446 and requires HTTP Basic Authentication backed by either File or LDAP authentication. Ensure that the tomcat connector for port 8446 is active within tomcat-home/conf/server.xml.

+

The CertificateSigning section in CoreConfig.xml specifies how CSRs are processed. TAK Server can be configured to sign certificates directly, or proxy CSRs to a Microsoft CA instance running Certificate Enrollment Services. To configure TAK Server to sign certificates, set the CA attribute to "TAKServer". To configure TAK Server to proxy the CSR to MS CA, set the CA attribute to "MicrosoftCA".

+
<certificateSigning CA="{TAKServer | MicrosoftCA}">
<certificateConfig>
<nameEntries>
<nameEntry name="O" value="Test Org Name"/>
<nameEntry name="OU" value="Test Org Unit Name"/>
</nameEntries>
</certificateConfig>
<TAKServerCAConfig
keystore="JKS"
keystoreFile="../certs/files_intCA/intermediate-ca-signing.jks"
keystorePass="atakatak"
validityDays="30"
signatureAlg="SHA256WithRSA" />
<MicrosoftCAConfig
username="{MS CA Username}"
password="{MS CA Password}"
truststore="/opt/tak/certs/files_MSCA/keystore.jks"
truststorePass="atakatak"
svcUrl="https://win-kbtud3n1hjl.tak.net/tak-WIN-KBTUD3N1HJL-CA_CES_UsernamePassword/service.svc"
templateName="Copy of User"/>
</certificateSigning>
+

Prior to submitting a CSR, Clients query TAK Server for Relative Distinguished Names (RDNs) that need to go into the CSR. The nameEntries element in CoreConfig.xml specifies the required RDNs, giving the administrator control over generated certificates. The CN value in the CSR will be equal to the HTTP username. TAK Server validates all required fields in the CSR prior to signing.

+

The extra step of having client query TAK Server for RDNs wouldn't be required if TAK Server were signing certificates exclusively, since TAK Server could just add these names to the certificate. However, when proxying the CSR to an external CA, this allows additional flexibility in controlling the subject name within the certificate.

+

The TAKServerCAConfig element specifies the keystore that TAK Server will use for signing certificates. The keystore must hold the CA's private key along with it's full trust chain. The makeCert.sh script will produce a signing keystore when generating an intermediate CA certificate. Certificates signed by TAK Server will be valid for the specified validityDays, and will be signed using the algorithm specified by signatureAlg.

+

The MicrosoftCAConfig element defines how TAK Server will connect to the Certificate Enrollment Services (CES) endpoint. The CES endpoint is defined by the svcUrl attribute. The CES endpoint must be configured to use Username/Password authentication, and by default will include 'UsernamePassword' in the URL. The username and password attributes refer to an account configured on the MS CA Server to access the the CES endpoint. The truststore and truststorePass attrbitues point to a Java keystore (.jks) file that contains the trust chain for the svcUrl endpoint. Lastly, the templateName defines the certificate template that will be used to sign CSRs sent from TAK Server.

Appendix D: PostgreSQL TLS Configuration

+

Configure PostgreSQL server to use TLS

+
    +
  • +

    Follow the steps in Appendix B (Certificate Generation) to generate CA keys and certificates if not already done so.

    +
  • +
  • +

    Generate PostgreSQL server keys and certificates:

    +
  • +
+
cd /opt/tak/certs
sudo su tak
./makeCert.sh server takdb
+

Become a normal user

+
exit
sudo chown postgres /opt/tak/certs/files/takdb.key
+
    +
  • Update postgresql.conf. The file location can be different depending on your PostgreSQL installation: +
      +
    • RHEL/Rocky/CentOS: /var/lib/pgsql/15/data/postgresql.conf
    • +
    • Ubuntu/RaspPi: /etc/postgresql/15/main/postgresql.conf
    • +
    +
  • +
+
sudo vim /var/lib/pgsql/15/data/postgresql.conf
ssl = on
ssl_ca_file = '/opt/tak/certs/files/ca.pem'
ssl_cert_file = '/opt/tak/certs/files/takdb.pem'
ssl_key_file = '/opt/tak/certs/files/takdb.key'
# Make sure to update the next line to use the correct passphrase as configured in cert-metadata.sh.
ssl_passphrase_command = 'echo "atakatak"'
ssl_passphrase_command_supports_reload = on
+
    +
  • Update pg_hba.conf. The file location can be different depending on your PostgreSQL installation: +
      +
    • RHEL/Rocky/CentOS: /var/lib/pgsql/15/data/pg_hba.conf
    • +
    • Ubuntu/RaspPi: /etc/postgresql/15/main/pg_hba.conf
    • +
    +
  • +
+
sudo vim /var/lib/pgsql/15/data/pg_hba.conf
+

Add this new line:

+
hostssl	 all             all             all                     cert
Comment out the following lines if you also require SSL authentication for IPv4/IPv6 local connections
# host all all 127.0.0.1/32 trust
# host all all ::1/128 trust
+
    +
  • Restart PostgreSQL server. Make sure it starts successfully. +
      +
    • RHEL, Rocky Linux, and CentOS installations: +
      sudo systemctl restart postgresql-15
      sudo systemctl status postgresql-15
      +
    • +
    • Ubuntu and Raspberry Pi installations: +
      sudo systemctl restart postgresql
      sudo systemctl status postgresql
      +
    • +
    +
  • +
+

Generate Client keys and certificates

+
    +
  • Generate client keys and certificates:
  • +
+
cd /opt/tak/certs
sudo su tak
./makeCert.sh dbclient
+

Client keys and certificates named "martiuser" (by default) will be created in the "files" directory.

+
    +
  • Test SSL connection using the generated client certificate:
  • +
+
psql "host=127.0.0.1 port=5432 user=martiuser dbname=cot sslmode=verify-ca sslcert=files/martiuser.pem sslkey=files/martiuser.key sslrootcert=files/ca.pem"
+

If you don’t want to verify the server’s credential:

+
psql "host=127.0.0.1 port=5432 user=martiuser dbname=cot sslmode=require sslcert=files/martiuser.pem sslkey=files/martiuser.key"
+

The sslmode "verify-ca" means "I want to be sure that I connect to a server that I trust." The sslmode "require" means "I trust that the network will make sure I always connect to the server I want."

+

More information on the sslmode can be found here: https://www.postgresql.org/docs/current/libpq-ssl.html

+
    +
  • Test database permission from the psql prompt:
  • +
+
select count(*) from cot_router;
+

NOTE: If you want to use a different name for certificates, you would also need to add a new user to the PostgreSQL database and grant permissions for the user. For example, following these steps to create a certificate named "takdbuser"

+
./makeCert.sh dbclient takdbuser
sudo su - postgres
+

Connect to Postgres:

+
psql -d cot
# List all users/roles:
\du
SELECT * FROM pg_roles;
# Create a new user ("takdbuser") and grant the user necessary roles ("martiuser"). The name of the user must match the CN in the client certificate.
CREATE USER takdbuser;
grant martiuser to takdbuser;
# Optional: Double check using \du and "SELECT * FROM pg_roles;"
+

Configure TAK Server to use SSL

+
    +
  • +

    Note that when you created a database client certificate (./makeCert.sh dbclient), an additional private key file in PKCS#8 format was created. Use this file for the param sslkey in CoreConfig.xml instead of using the files with .key extension.

    +
  • +
  • +

    Update CoreConfig.xml: +Update the <connection> tag in <repository> (Remember to use a correct hostname/IP)

    +
  • +
+
<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" sslEnabled="true" sslMode="verify-ca" sslCert="certs/files/martiuser.pem" sslKey="certs/files/martiuser.key.pk8" sslRootCert="certs/files/ca.pem"/>
+

If you don’t want to verify the server’s credential (not recommended in production):

+
<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" sslEnabled="true" sslMode="require" sslCert="certs/files/martiuser.pem" sslKey="certs/files/martiuser.key.pk8" />
+
    +
  • Start/Restart TAK server.
  • +
+
 sudo systemctl restart takserver

Appendix E: Proper Use of Trusted CAs

+

TAK uses Mutual TLS (MTLS) authentication to establish secure communications channels between TAK clients and TAK Server. It's critical that deployments use a CA created by the TAK server scripts, or another private CA, to establish the root of trust. Failure to follow this guidance could result in exposing your deployment to a Man-In-The-Middle (MITM) attack.

+
    +
  • To ensure secure communications, it's critical that truststores deployed to TAK clients only contain CAs created by the TAK Server scripts or another private CA.
  • +
  • There is never a need to add a LetsEncrypt, Digicert or any other public CA certificate to a truststore on a TAK client or TAK Server.
  • +
  • When using Quick Connect, the LetsEncrypt or DigiCert server certificate should only be configured within your 8446 connector, and never within your <tls> configuration.
  • +
+

Appendix B of the TAK Server Configuration Guide (see Downloadable Resources section here https://tak.gov/products/tak-server) contains steps for creating a root CA to use in your TAK deployment. The makeRootCa.sh script creates a private key and self-signed certificate for the CA, and packages up the CA certificate within truststores that can be configured on TAK clients and on TAK Server.

+

Appendix B contains additional steps for creating a client and server certificates, signed by the root CA. Appendix C describes TAK Server's Certificate Enrollment capability (https://wiki.tak.gov/display/DEV/Certificate+Enrollment) that automates the provisioning of client certificates. Users authenticate to the Certificate Enrollment endpoints with username/password, provide a CSR and receive a signed client certificate in return.

+

When enrolling using a server certificate from a self-signed TAK CA, clients must be bootstrapped with server's CA certificate prior to enrollment. To streamline provisioning of clients, TAK provides the Quick Connect feature that performs Certificate Enrollment using trust provided by LetsEncrypt or DigiCert CAs. TAK clients have embedded the LetsEncrypt and DigiCert CAs and will use these CAs when validating connections to the enrollment port. In this configuration, the TAK client will be able to leverage the embedded LetsEncrypt CA certificate, along with TLS hostname verification, to ensure the integrity of the connection.

+

To configure Quick Connect, the TAK server admin adds the keystore, keystoreFile, and keystorePass attributes on the 8446 connector to use the trusted server cert for enrollment, as shown below.

+
	    <connector port="8446" clientAuth="false" _name="cert_https" keystore="JKS" keystoreFile="certs/files/letsencrypt-server-cert.jks" keystorePass="example-pass"/>
+

When using Quick Connect, you never have to do any configuration with LetsEncrypt or DigiCert CA itself. That is handled by embedding the CAs within the clients. The only reference to any key material from LetsEncrypt or DigiCert within your environment will be the server certificate contained in keystoreFile that's referenced by your 8446 connector.

Group Filtering for Multicast Networks

+

The proxy attribute on the CoreConfig input element ( <input … proxy="true" … /> ) was removed in TAK Server 4.1. The intent of the proxy attribute was to control bridging of multicast networks and federating multicast data. As TAK Server’s group filtering capabilities have evolved, having a dedicated proxy attribute is no longer needed. Using filtergroup on the mcast input you can achieve greater control over multicast traffic. +The default behavior in TAK Server 4.1 and higher is to put multicast traffic in the __ANON__ group. You can use a filtergroup on the mcast input to put your mcast traffic into a dedicated multicast group, for example:

+
<input auth="anonymous" _name=" SAproxy " protocol="mcast" port="6969" group="239.2.3.1">
<filtergroup>__MCAST__</filtergroup>
</input>
+

Then add the __MCAST__ group as a filtergroup on any other inputs you wanted to share multicast traffic with. For example, to share multicast traffic with the tls/8089, configure your input filtergroups as follows:

+
<input auth="anonymous" _name="stdssl1" protocol="tls" port="8089" archive="true">
<filtergroup>__ANON__</filtergroup>
<filtergroup>__MCAST__</filtergroup>
</input>
+

This same approach works for federations. You can __MCAST__ as an outboundGroup on any federates that you wanted to share multicast traffic with. Using the filtergroup approach allows for creation of input specific multicast groups, allowing control of how messages from multicast networks are routed.

Device Profiles

+

TAK Server can now assist in provisioning ATAK devices through Device Profiles. The Device Profile Manager (under Administrative Menu, Device Profiles) allows administrators to build profiles that can be applied to clients when enrolling for certificates, and when connecting to TAK server. The Profile consists of a sets of files, which can include settings and data in any file format that is supported by TAK Mission Packages. Profiles can be made public or restricted to individual Groups.

+

When an ATAK device enrolls for client certificate, or optionally after connecting to TAK server, TAK server will return all profiles that need to be applied to the device. The TAK server administrator can also push a profile to a connected user by clicking the Send link within the Device Profile Manager.

One post tagged with "facebook"

View All Tags

· One min read
Sébastien Lorber
Yangshun Tay

Docusaurus blogging features are powered by the blog plugin.

+

Simply add Markdown files (or folders) to the blog directory.

+

Regular blog authors can be added to authors.yml.

+

The blog post date can be extracted from filenames, such as:

+
    +
  • 2019-05-30-welcome.md
  • +
  • 2019-05-30-welcome/index.md
  • +
+

A blog post folder can be convenient to co-locate blog post images:

+

Docusaurus Plushie

+

The blog supports tags as well!

+

And if you don't want a blog: just delete this directory, and use blog: false in your Docusaurus config.

Image Test

+

This is just a section demonstrating images.

+

Dog

+

This is some text after the image.

OAuth2 Authentication

+

TAK Server provides OAuth2 Authorization and Resource server capabilities using the OAuth2 Password authentication flow. OAuth2 integration works with existing authentication back ends, allowing TAK Server to issue tokens backed by the File or LDAP authenticators. TAK Server issues JSON Web Tokens (JWT) signed by the server certificate, allowing external systems to validate tokens against the server’s trust chain. The OAuth2 token endpoint is available at https://<takserver>:8446/oauth/token.

Logging

+

TAK Server provides several log files to provide information about relevant events that happen during execution. The log files are located in the /opt/tak/logs directory. This table shows the name of the log files, and their function.

+
Name of Log FilePurpose
takserver-messaging.logExecution-level information about the messaging process, including client connection events, error messages and warnings.
takserver-api.logExecution-level information about the API process, including error messages and warnings.
takserver-messaging-console.logJava Virtual Machine (JVM) informational messages and errors, for the messaging process.
takserver-api-console.logJava Virtual Machine (JVM) informational messages and errors, for the API process.

Metrics

+

The TAK Server Metrics Dashboard is available in the Monitoring menu. The dashboard continuously renders the following information:

+

Server Start Time and Server Up Time This tell you when the server was turned on and how long it has been operating.

+

Clients Connected This tells you how many connections your client is currently servicing. This corresponds to the number of clients that are displayed in the client dashboard.

+

Heap Usage TAK server runs inside one or more Java Virtual Machines (JVM). Heap Commited is how much heap memory in MB is allocated to the API process for TAK Server, and Heap Used is how much of that is currently being used.

+

Network I/O and Reads/Writes This tells you how much TCP and UDP traffic the server is currently handling, as well as a brief history.

+

CPU Usage How much of the CPU of the machine the server is running on is currently being used.

+

Metrics Page

User Management UI

+

Overview

+

The User Management UI provides an intuitive drag-and-drop mechanisms for managing TAK user accounts. The tool is integrated within TAK Server and can be accessed from the TAK main menu, under Administrative >> Manage Users. Users need to have an admin role to access the tool. Currently the User Management UI supports only file-based users and not LDAP/AD users. +The tool allows TAK administrators to create, manage, inspect and delete TAK user accounts. More specifically, the tool allows TAK administrators to:

+
    +
  • View, filter and search for existing user accounts and groups.
  • +
  • View a list of users in each group.
  • +
  • Change password for each user account.
  • +
  • View and update groups (IN group, OUT group and both) for each user account.
  • +
  • Delete user accounts.
  • +
  • Create a new user account with a specified password and groups. Password complexity is checked to confirm compliance.
  • +
  • Create new user accounts in bulk with username following a pattern. System uses password generation mechanism to create passwords that meet TAK password complexity requirements. System produces output file with user/password combos as a one-time downloadable item, after which system forgets the un-hashed passwords.
  • +
  • Create new groups.
  • +
+

Usage

+

The below figure shows the main page of the User Management UI. The left panel lists all user accounts, which can be filtered using the Search box on the top. The right panel lists all existing groups, which can be filtered using the Search box on the top.

+

Main Page

+

To change user’s password, click on the arrow right next to the username and select "Change password".

+

Change Password

+

To view/edit groups for a user account, click on the arrow right next to the username and select "View/Edit groups". You can drag the groups from the right panel and drop to one of the three boxes in the middle panel. Click on “Reset” button to bring the UI back to showing the current groups of the user. Click on “Update” button to update the groups of the user.

+

View Edit Groups

+

To delete an account, click on the arrow right next to the username and select "Delete User". You will be prompted to either confirm or cancel the action.

+

Delete Account

+

To list all users in a group, click on the arrow right next to the group name and click on "List users".

+

List Users

+

To create a new user, click on "Add User" on the menu bar.

+

Add User

+

To create new users in bulk, click on "Add Users" on the menu bar.

WebTAK

+

The WebTAK front-end application is bundled with TAK Server. The WebTAK back-end WebSockets networking channel and APIs are provided by TAK Server. WebTAK must be accessed using https.

+

WebTAK can be accessed either with X.509 client certificates (default https port 8443), or by username-password access using OAuth (https port 8446).

+

In either case, TAK Server must be configured with a server certificate and truststore (see Appendix B).

+

In the Admin UI menu, use Situation Awareness -> WebTAK to access WebTAK.

Configuring Messaging and Repository Settings through Web UI

+

Messaging Configuration Web Interface

+

Messaging/Repository settings configuration can be done through the input definitions page. To get there go to Configuration > Input Definitions in the menu bar. This page displays the current input definitions at the top and at the bottom the current configuration of Messaging and Repository settings are displayed. To edit these setting click "Edit Configuration". Note: Changes made here will only take effect after a server restart.

Authentication Backends

+

File-Based

+

There is now a flat-file option available to inputs. Previously the only valid value for the <input> “auth” attribute was “ldap”. “file” is now another valid value. The example configuration file (CoreConfig.example.xml) contains an example of how to configure the File-based backend.

+

A utility for creating and maintaining that flat file is included in the release. Run

+
sudo java -jar /opt/tak/utils/UserManager.jar
+

and look at the various options for the 'offlineFileAuth'

+

Active Directory (AD) / LDAP

+

TAK Server can be configured to use an Active Directory or LDAP server to authenticate users, and assign groups. LDAP configuration for TAK Server varies depending on the configuration of the AD or LDAP server. Here is an example. Note that it contains credentials for a service account. This is required for group membership lookup using a client cert:

+
<auth>
<ldap url="ldap://a.b.com/ou=MyUserOU,DC=a,DC=b,DC=com" userstring="{username}@MYDOMAIN" updateinterval="60000" style="AD" serviceAccountDN="mysearchuser@MYDOMAIN" serviceAccountCredential="password" />
</auth>
+

Configuring LDAP Through Web Interface

+

Authentication Configuration Web Interface

+

The LDAP configuration can be changed through an easy to use web page. To access this go to Configuration > Manage Security and Authentication. Under the Authentication heading will be the current LDAP configuration (the values will be empty if LDAP is not configured yet). Click on "Edit Authentication" to be directed to a form to enter desired LDAP settings. Note: Changes made here will only take effect after a server restart.

Configuring Security Through Web UI (Certificates/TLS)

+

Security Configuration

+

Security Configuration Web interface

+

Security and authentication options for TAK Server can be set up using a web interface. To access this page in the menu bar go to Configuration > Manage Security and Authentication Configuration. This page will contain both Security and Authentication configuration current values. To modify the Security Configuration click "Edit Security". This will allow changes to the server's certificates, the version of TLS used, x509 Groups settings and x509 Add Anonymous settings.

+

Note: Changes made here will only take effect after a server restart.

Group Assignment Using Authentication Messages

+

If ATAK or another TAK client is configured to send an authentication message after establishing a connection to TAK Server, the username and password credentials contained in that message will be used, in conjunction with an authentication backend, to determine the group membership of a user. TAK Server will then filter messages according to common group membership, in a similar fashion to filtering configured by <filtergroups> for a given <input>.

+

TAK Server can be configured at the input level to expect authentication messages with each new client connection. If the authentication message is not sent, or is invalid, the client will be disconnected. The “auth” attribute on the input indicates which authentication strategy will be used when processing authentication messages. A value of “file” tells TAK Server to validate authentication credentials using the flat-file backend. A value of “ldap” indicates that an Active Directory or LDAP server should be used to validate the credentials.

+

For example, this input definition specifies streaming TCP, encrypted with TLS, authenticating the user with a client certificate and also requiring an authentication message, and using the LDAP authentication backend:

+
  <input _name="ldapssl" protocol="tls" port="8091"  auth="ldap"/>

Group Assignment by Input

+

<inputs> can drive group filtering, even without authentication messages. Version 1.3.0 added group filtering based on LDAP groups. This necessitated a new authentication message from ATAK. This worked for the streaming connections, but wouldn't work for the connection-less UDP traffic.

+

We added an additional configuration option for inputs to allow the connection-less traffic to be routed according to the group filtering. An input definition like this:

+
   <input _name="stdudp" protocol="udp" port="8087">
<filtergroup>TEST1</filtergroup>
</input>

+

would have the effect of making every CoT event that came into the 'stdudp' input be associated with the “TEST1” group instead of the anonymous group. If there is no filtergroup specified, the default is the old behavior, which is a special anonymous group. The anonymous group has a name “__ANON__” that can be used to explicitly add it back in if needed. The filtergroup option can be used with the streaming input protocols as well (stcp, tls), the effect of which is that any subscriptions made by connecting to that port inherit the filter group from the input. <filtergroup> cannot be used in conjunction with the “auth” attribute on the same input. You can however use them on separate inputs, for example:

+
   <input _name="stdudp" protocol="udp" port="8087">
<filtergroup>CN=TAK1,DC=...</filtergroup>
</input>
<input _name="sec" protocol="tls" port="8089" auth="ldap" />
+

Note that when trying to interact with LDAP groups, you need to use the fully qualified group name that LDAP/ActiveDirectory reports.

+

Input Configuration UI

+

Inputs can be dynamically added, modified and deleted in the TAK Server user interface, under the menu heading Configuration → Input Definitions. The UI also shows activity for each input, in terms of number of reads and messages. For the streaming protocols (stcp, tls), the activity is the sum for all connections made using that particular input port.

Group Assignment using Client Certificates

+

TAK Server can be configured to use only the information contained in a client certificate, when looking up group membership for a user. In this case, the TAK client is configured to use TLS and a client certificate when connecting to TAK server, but does not send an authentication message. This eliminates the requirement to cache credentials on the device, or enter credentials prior to establishment of each new connection. TAK Server will then filter messages according to common group membership, in a similar fashion to input-level filtering with filter groups. When analyzing the X.509 client certificate presented by the TAK client, TAK Server will look at the DN attribute in the certificate, extract the CN value from the DN (if present). The CN is regarded as the username, and is used to look up group membership in authentication backends. For example, consider this DN in a client certificate:

+
CN=jkirk, O=TAK, C=US
+

The CN value jkirk will be used as the username. The process for deciding which authentication backend to use depends on whether or not an Active Directory (AD) LDAP configuration is present in CoreConfig.xml. Valid service account credentials must be configured in CoreConfig.. If AD authentication is configured, the user account is matched by the sAMAccountName LDAP attribute. At client authentication time, if groups are found in AD for the user, those groups are used by TAK Server. If no groups are found, the flat-file authentication backend is searched for a match on the username. If no groups are found for the user in either repository, the user is assigned to the __ANON__ group.

+

When configuring the input, a TLS input with an auth type of x509 directs TAK Server to use the client certificate for both authentication and group assignment. On the input configuration, the on or example, this input definition specifies streaming TCP encrypted with TLS, authenticating the user with a client certificate and also requiring an authentication message, and using the LDAP authentication backend:

+
  <input _name="ldapssl" protocol="tls" port="8091" auth="x509"/>

Optionally Disabling UI and WebTAK on HTTPS Ports

+

TAK Server can be configured to optionally disable the Admin UI, non-admin UI or WebTAK on any HTTPS connector (port). These options can be used to fine-tune the security profile for each HTTPS connector. For example, the admin web interface can be moved to an alternate port that is protected by a firewall from access on the public Internet.

+

In the CoreConfig.xml, the enableAdminUI, enableWebtak, and enableNonAdminUI attributes on each <connector> can be used to optionally disable access to any of these three functions for a given HTTPS connector port. The default value for each of these attributes is true, so by default these functions are available.

+

Usage Examples: +Disable webtak on port 8443:

+
<connector port="8443" _name="https" enableWebtak="false" />
+

On port 8452, disable admin UI, but enable WebTAK and non-admin UI:

+
<connector port="8452" _name="https" enableAdminUI="false" />
+

Disable WebTAK on OAuth port 8446:

+
<connector port="8446" clientAuth="false" _name="cert_https" enableWebtak="false"/>

Group Filtering

+

TAK Server has the ability to segment users so they only see a subset of the other users on the system. This is achieved by assigning groups to individual connections. If ATAK-A shares common group membership in at least one group with ATAK-B, they share data with each other. If not otherwise specified, all connections default to being in the special “ANON” group (note 2 underscores as prefix and postfix). There are three ways to assign groups to a connection:

+
    +
  • Assigning <filtergroup> elements to <inputs>: this is simple, but provides no access control if you have multiple ports configured on the same server.
  • +
  • Active Directory / LDAP / Flat file with additional authentication message
  • +
  • Active Directory / LDAP / Flat file without additional authentication messages (uses certificate-based identification)
  • +
+

Details on the three options:

VBM Admin Configuration

+

TAK Server can allow for additional filtering of cot messages recieved from inputs (server ports) and data feeds by using the VBM Configuration page in the Admin UI. To navigate there, go to Administative > VBM Configuration as shown below.

+

VBM Admin Configuration

+

You will then see the following options.

+

VBM Admin Configuration

+

To modify the VBM configurations select the checkbox next to the desired option and when you're finished click "Save changes".

+

NOTE: "Disable SA Sharing" and "Disable Chat Sharing" will only be used if "Enable VBM" is selected.

+

The VBM options have the following impact:

+

If "Enable VBM" is selected, messages recieved on a data feed are only brokered to clients which are subscribed to the mission if the message falls within the bounding box specified by the mission. For example, the message represented by the blue dot would be passed on while the message represented by the green dot would be filtered out for the mission with the displayed bounding box only if "Enable VBM" is turned on.

+

VBM Admin Configuration

+

The second two options are only activated if "Enable VBM" is on and they refer to messages recieved from inputs (server ports). These options filter messages based on whether they are chat messages. Chat messages in this context are cot messages which have a type set to "b-t-f".

+

If "Disable SA Sharing" is selected, messages recieved from inputs are passed on if the message is a chat message as defined above.

+

If "Disable Chat Sharing" is selected, messages recieved from inputs are passed on if the messages is NOT a chat message as defined above.

+

These options are not mutually exclusive. Therefore, having both selected will filter out all messages recieved on inputs.

Overview

+

Configuration is primarily done through the web interface. Changes made in the interface will be reflected in the /opt/tak/CoreConfig.xml file. If that file does not exist (e.g. on a fresh install), then when TAK Server starts up it will copy /opt/tak/CoreConfig.example.xml. The example has many commented out options. Notable configuration options:

+
    +
  • inputs: In the <network> section there are a series of <input> elements. These define ports the server will listen on. Protocol options are as follows: +
      +
    • udp: standard CoT udp protocol; unencrypted
    • +
    • mcast: like udp, but has additional configuration option for multicast group
    • +
    • tcp: publish-only port; standard CoT tcp protocol; unencrypted
    • +
    • stcp: streaming/bi-directional; this is for ATAK to connect to. Unencrypted, for testing only
    • +
    • tls: TCP+TLS streaming/bi-directional for encrypted communication with TAK clients
    • +
    +
  • +
  • <auth> : you can use either a flat file or an LDAP backend for group filtering support
  • +
  • <security>: here you specify the keystore files to use for the secure port(s)
  • +

Data Package and Mission File Blocker

+

Data packages, mission packages, and missions can be federated between servers and their respective ATAK clients. These packages may contain configuration files such as ATAK .pref files that can result in the distribution of unwanted configuration changes to ATAK devices. A filter can be enabled to block files by file-type. To enable this feature, check the Data Package and Mission File Blocker box in the Federation Configuration page. The default file extension value is 'pref'. This can be changed by entering a new file type, clicking on the 'Add' button to add the entry, and clicking on the 'Save' buton at the bottom of the Federation Configuration page.

+

Data Package and Mission File Blocker Box

Federated Group Mapping

+

The flow of traffic between Federates may be directed using end-to-end group mapping. The Federated Group Mapping section is on the Federate Groups page.

+

Groups are exchanged during active connections between Federates. The remote groups will appear in the ‘Remote Group List’ drop down in the Federated Group Mapping section. Connected Federates must have Federated Group Mapping enabled in order for the Federates to exchange their respective remote groups. This parameter is in the Federation Configuration section in the Configuration > Manage Federates page.

+

To configure the end-to-end mapping, select a remote group and map it to a local Federate group. Remote groups may also be entered directly in the ‘Remote Group’ field. A single remote group can be mapped to many local groups. Additionally, multiple end-to-end group mappings may be defined. With a group mapping configured, traffic from the remote group will only flow to the mapped local group(s). Note: if no incoming traffic matches the remote groups configured, the federation traffic will fall back to the Federate Group scheme described previously.

+

Remote Groups

Mission Federation Disruption Tolerance

+

Traffic between federated servers may be disrupted, and updates to missions could happen during that disruption. Mission federation disruption tolerance will update each server with changes to federated missions that occured during the disruption. To enable this feature, check the box in the Federation Configuration page:

+

Enable Disruption Tolerance

+

Sending all the changes that occured between disruptions could potentially take a lot of bandwidth, so by default, we limit the changes to those that occured within the last 2 days. For example, if a disruption lasted 3 weeks, we would only send the changes from the previous 2 days. However, if the disruption only lasted a few hours, only the changes since the last disruption would be sent. If the Unlimited checkbox is checked, then all changes since the last disruption would be sent. The 2 day limit can be changed to any length with the Send Changes Newer Than setting, and the period can be selected as days, hours, or seconds.

+

It is also possible to override the global setting for a particular mission, if so desired.

+

Specific Mission Settings

+

For example, in the above image, we see that mission_2 will send updates up to over the last 10 days, mission_2 only over the last 12 hours, and mission_4 will send all updates since the last disruption. Any mission that is not listed, and any subsequently added mission, will follow the global setting of 2 days, as set above.

+

Clear Federation Events

+

The Clear Federation Events button will reset the disruption history for federation. This means that on the next reconnection, the server will send the max allowed mission changes according to the Mission Federation Disruption Tolerance settings. In the above case, that would be 10 days for mission_2, 12 hours for mission_3, and the entire change history for mission_4. For all other missions this would be 2 days worth of mission changes.

Upload Federate Certificate

+

In order for the federate servers to trust each other and their ATAK clients, they must share each others certificate authorities (CAs) in order to create a separate federate trust-store. One of the key components to how TAK Server satisfies all the restrictions is that we use one trust-store for local users, and one for Federates. The trust-store contains all the valid CAs that you will allow client certificates from. By having separate trust-stores, we can have the Federation channels allow connections with certificates from “outside” CAs, while not allowing ATAKs with certs from those “outside” CAs to make direct connections to our server.

+

Federate Certificate Authorities Page

+

Generally, we share the public CA, which you can find at /opt/tak/cert/files/ca.pem, via some third channel such as email or a USB drive. Once you have traded CAs, go the the Manage Federate Certifate Authorities page and upload the CA of the federate you want to connect to.

Federation Example

+

The figure below shows a connectivity graph of two distinct administrative domains. Each administrative domain has multiple sub-groups (e.g. "CN=Alpha") utilizing the group-filtering. The color coding indicates the CA that is used to sign the certificate used for connections. Enclave 1's CA signs ATAK client certs and a server certificate. Enclave 2's CA also signs ATAK client certs and a server cert. The trust-store listing the allowed CAs for the "User Port" only contains a single CA (i.e. Enclave 1 CA for Enclave 1). To federate the servers, Enclave 1 and Enclave 2 send each other the "public" CA cert. Those certificates are put in a separate trust store that is used only for federation connections. The “Fed. Port” is configured with this separate trust-store. +The server cert from each administrative domain can now be used to connect to the "Fed. Port" of the other domain.

+

Federation Example

+

Alternate Configuration

+

The first example had each federate using the same CA and server certificate for local and federate connections. If you are very paranoid, or don't want to share anything about the crypto being used for local clients, you can have a wholly separate CA+server certificate chain that is used for federation. Figure 5 shows how this would work.

+

Alternate Federation Example

+

This adds some complexity, but can be used if you don't want to expose your 'internal' CA to the organizations that you are federating with.

Make the Connection

+

Now that we have enabled federation and shared our CA with the other TAK server authority, we are ready to make the connection and start sharing. For this step, only ONE of the servers creates an outgoing connection to the other. If you are starting the connection, go back to the Manage Federates page where you enabled federation from step 1. You will now see three sections, Active Connections, Outgoing Connection Configuration, and Federate Configuration. To create an outgoing connection, click on the corresponding link, and enter in the address and port of the destination server. You can also pick the protocol version (make sure it is the right protocol for the port you are connecting to!), reconnection interval (how long between retries if the connection is lost), and whether or not the connection will be enabled on creation.

+

Active Connections

+

Now that you have created and started a connection, you will notice that no information is yet flowing between federates. This is because you and your fellow federate must specify which filtering groups you will allow to flow out of and into your server. To manage this, click on the Manage Groups link in the corresponding row of the Federate Configuration section. Here you can specify the groups, including the special __ANON__ group if you want. Once both servers have configured the groups, traffic will start to flow. A server restart is not necessary for these changes to take effect.

+

Federate Groups

Overview

+

Federation will allow subsets of ATAK users who are connected to different servers to work together, even though each TAK server instance (hereafter refered to as 'federates') may be run by independent organizations / administrative domains. It brings some of the following benefits/restrictions:

+
    +
  1. Each administrative domain does not need to share anything about their internal structure (e.g. LDAP/Active Directory information / users) with the other administrative domain.
  2. +
  3. Each administrative domain has control over what data they share with the other domain, but has no control over what the other administrative domain does with data that is shared.
  4. +
  5. It requires no reconfiguration of ATAKs connected to either TAK Server, and the mechanism for connecting the TAK Servers does not allow direct connections of ATAK devices from the other administrative domain.
  6. +

Building and Installing Container Images Using Docker

+

TAK Server can be installed using docker. Start by downloading container images from tak.gov. You will need the docker release which comes as a zip file called 'takserver-docker-<version>.zip'.

+

If you using CentOS 7, follow these instructions first to install docker, start the docker daemon and use it as a regular user: +https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-centos-7

+

Next, unzip the docker zip file. All further commands should be run from this top level 'takserver-docker-<version>' directory. If you are familiar with the rpm install, the 'tak' folder within the 'takserver-docker-<version>' directory represents the '/opt/tak' directory installed by the rpm. When the takserver containers are built, the 'tak' directory will be mounted to '/opt/tak' within the containers. Therefore, any references to '/opt/tak' outside this section of the guide will be equivalent to the 'tak' directory you have on the host, or '/opt/tak' if you are working from inside the container. The 'tak' directory is where the coreconfig, certificates, logs and other TAK configuration/tools will live. This folder is shared between the host, the takserver container and the takserver database container. This means you can tail the logs or manually edit the coreconfig from the host without being inside the container.

+

Notes for running in Windows Subsystem for Linux (WSL) 2:

+

When running TAK Server in the WSL2 environment, follow the steps outlined in the Best Practices section here https://docs.docker.com/desktop/windows/wsl/ to maximize TAK Server performance. Specifically, it’s recommended that you copy the 'takserver-docker-<version>.zip' file into your WSL user’s home directory and execute all docker commands from there (vs accessing your Windows filesystem from /mnt). From there, unzip the file and run the docker commands below for TAK Server. It’s important to unzip the file from within WSL to ensure permissions are setup correctly.

+

TAK Server CoreConfig Setup:

+
    +
  1. Open tak/CoreConfig.example.xml and set a database password
  2. +
  3. Make any other configuration changes you need
  4. +
+

TAK Server Database Container Setup:

+
    +
  1. Build TAK server database image:
  2. +
+
docker build -t takserver-db:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver-db .
+
    +
  1. Create a new docker network for the current tak version:
  2. +
+
docker network create takserver-"$(cat tak/version.txt)"
+
    +
  1. The TAK Server database container can be configured to persist data directly to the host or only within the container.
  2. +
+

a. To persist to the host, create an empty host directory (unless you have a directory from a previous docker install you want to reuse). For upgrading purposes, we recommend that you keep the takserver database directory outside of the 'takserver-docker-<version>' directory structure.

+
docker run -d -v <absolute path to takserver database directory>:/var/lib/postgresql/data:z -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)"
+

b. To run TAK server database with container only persistence

+
docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)"
+

TAK Server Container Setup:

+
    +
  1. Build TAK Server image:
  2. +
+
docker build -t takserver:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver .
+
    +
  1. Running TAK Server container: use -p <host port>:<container port> to map any additional ports you have configured. Adding new inputs or changing ports while the container is running will require the container to be recreated so that the new port mapping can be added.
  2. +
+
docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -p 9000:9000 -p 9001:9001 --network takserver-"$(cat tak/version.txt)" --name takserver-"$(cat tak/version.txt)" takserver:"$(cat tak/version.txt)"
+
    +
  1. +

    Before using the TAK Server, you must setup the certificates for secure operation. If you have already configured certificates you can skip this step. You can also copy existing certificates into 'tak/certs/files' and a UserAuthetication.xml file into 'tak/' to reuse existing certificate authentication settings. Any change to certificates while the container is running will require either a TAK server restart or container restart. Additional certificate details can be found in Appendix B.

    +

    a. Edit tak/certs/cert-metadata.sh

    +

    b. Generate root ca

    +
    docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeRootCa.sh"
    +

    c. Generate server cert

    +
    docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeCert.sh server takserver"
    +

    d. Create client cert(s)

    +
    docker exec -it takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/certs && ./makeCert.sh client <user>"
    +

    e. Restart takserver to load new certificates

    +
    docker exec -d takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && ./configureInDocker.sh"
    +

    f. Tail takserver logs from the host. Once TAK server has successfully started, proceed to the next step.

    +
    tail -f tak/logs/takserver-messaging.log
    tail -f tak/logs/takserver-api.log
    +
  2. +
  3. +

    Accessing takserver +Create admin client certificate for access on secure port 8443 (https):

    +
    docker exec takserver-"$(cat tak/version.txt)" bash -c  "cd /opt/tak/ && java -jar utils/UserManager.jar certmod -A certs/files/<client cert>.pem"
    +
  4. +
+

Hardened TAK Server Setup:

+

The hardened TAK Database and Server containers provide additional security by including the use of secure Iron Bank base images, container health checks, and minimizing user privileges within the containers.

+

The hardened TAK images are available in a zip file, takserver-docker-hardened-<version>.zip. The steps for setting up the hardened containers are similar to the standard docker installation steps given above except for the following:

+

Certificate Generation:

+

The certificate generation container is only required to run once for TAK Server initialization. Run all commands in this section from the root of the unzipped hardened docker contents.

+
    +
  1. Build the Certificate Authority Setup Image:
  2. +
+
docker build -t ca-setup-hardened --build-arg ARG_CA_NAME=<CA_NAME> --build-arg ARG_STATE=<ST> --build-arg ARG_CITY=<CITY> --build-arg ARG_ORGANIZATIONAL_UNIT=<UNIT> -f docker/Dockerfile.ca .
+
    +
  1. Run the Certificate Authority Setup Container: If certificates have previously been generated and exist in the tak/cert/files path when building the ca-setup-hardened image then certificate generation will be skipped at runtime.
  2. +
+
docker run --name ca-setup-hardened -it -d ca-setup-hardened
+
    +
  1. Copy the generated certificates for TAK Server:
  2. +
+
docker cp ca-setup-hardened:/tak/certs/files files

[ -d tak/certs/files ] || mkdir tak/certs/files \
&& docker cp ca-setup-hardened:/tak/certs/files/takserver.jks tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/truststore-root.jks tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/fed-truststore.jks tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/admin.pem tak/certs/files/ \
&& docker cp ca-setup-hardened:/tak/certs/files/config-takserver.cfg tak/certs/files/
+

TAK Server Database Hardened Container Setup:

+
    +
  1. Building the hardened docker images requires creating an Iron Bank/Repo1 account to access the approved base images. To create an account, follow the instructions in the IronBank Getting Started page. To download the base images via the CLI, see the instructions in the Registry Access section. After obtaining the necessary credentials, run:
  2. +
+
docker login registry1.dso.mil
+
    +
  1. Follow the instructions in the TAK Server CoreConfig Setup section and update the <connection-url> tag with the hardened TAK Database container name. For example:
  2. +
+
connection url="jdbc:postgresql://tak-database-hardened-<version>:5432/cot" username="martiuser" password=<password>/>
+
    +
  1. Create a new docker network for the current tak version:
  2. +
+
docker network create takserver-net-hardened-"$(cat tak/version.txt)"
+

Ensure in the db-utils/pg_hba.conf file that there is an entry for the subnet of the hardened takserver network. To determine the subnet of the network:

+
docker network inspect takserver-net-hardened-"$(cat tak/version.txt)"
+

Or to specify the subnet on network creation:

+
docker network create takserver-net-hardened-"$(cat tak/version.txt)" --subnet=<subnet>
+
    +
  1. Build the hardened TAK Database image:
  2. +
+
docker build -t tak-database-hardened:"$(cat tak/version.txt)" -f docker/Dockerfile.hardened-takserver-db .
+
    +
  1. Run the hardened TAK Database container:
  2. +
+
docker run --name tak-database-hardened-"$(cat tak/version.txt)" --network takserver-net-hardened-"$(cat tak/version.txt)" --network-alias tak-database -d tak-database-hardened:"$(cat tak/version.txt)" -p 5432:5432
+

TAK Server Hardened Container Setup

+
    +
  1. Build the hardened TAK Server image:
  2. +
+
docker build -t takserver-hardened:"$(cat tak/version.txt)" -f docker/Dockerfile.hardened-takserver .
+
    +
  1. Run the hardened TAK Server container:
  2. +
+
docker run --name takserver-hardened-"$(cat tak/version.txt)" --network takserver-net-hardened-"$(cat tak/version.txt)" -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -t -d takserver-hardened:"$(cat tak/version.txt)"
+

Configuring Certificates

+
    +
  1. Get the admin certificate fingerprint
  2. +
+
docker exec -it ca-setup-hardened bash -c "openssl x509 -noout -fingerprint -md5 -inform pem -in files/admin.pem | grep -oP 'MD5 Fingerprint=\K.*'"
+
    +
  1. Add the certificate fingerprint as the admin after the hardened TAK server container has started (about 60 seconds)
  2. +
+
docker exec -it takserver-hardened-"$(cat tak/version.txt)" bash -c 'java -jar /opt/tak/utils/UserManager.jar usermod -A -f <admin cert fingerprint> admin'
+

Useful Commands

+

To run these commands on the hardened containers, add the -hardened suffix to the container names.

+
    +
  • View images:
  • +
+
docker images takserver
docker images takserver-db
+
    +
  • View containers +All: 'docker ps -a' +Running: 'docker ps' +Stopped: 'docker ps -a | grep Exit'
  • +
  • Exec into container
  • +
+
docker exec -it takserver-"$(cat tak/version.txt)" bash
docker exec -it takserver-db-"$(cat tak/version.txt)" bash
+
    +
  • Exec command in container
  • +
+
docker exec -it takserver-"$(cat tak/version.txt)" bash -c  "<command>"
docker exec -it takserver-db-"$(cat tak/version.txt)" bash -c "<command>"
+
    +
  • Tail takserver logs
  • +
+
tail -f tak/logs/takserver-messaging.log
tail -f tak/logs/takserver-api.log
+
    +
  • Restart TAK server
  • +
+
docker exec -d takserver-"$(cat tak/version.txt)" bash -c "cd /opt/tak/ && ./configureInDocker.sh"
+
    +
  • Start/Stop container:
  • +
+
docker <start/stop> takserver-"$(cat tak/version.txt)"
docker <start/stop> takserver-db-"$(cat tak/version.txt)"
+
    +
  • Remove container:
  • +
+
docker rm -f takserver-"$(cat tak/version.txt)"
docker rm -f takserver-db-"$(cat tak/version.txt)"

Ubuntu and Raspberry Pi

+

UFW (Uncomplicated Firewall) is a utility for managing firewalls. If is not installed on your server. Install with the following:

+
sudo apt install ufw
+

IMPORTANT: Raspberry Pi installs, please reboot your device after installing ufw.

+

To check the status of the firewall service with current port rules:

+
sudo ufw status
+

Perform the following commands to set initial rules for your firewall:

+
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow ssh
+

Turn on the firewall:

+
sudo ufw enable
+

Add default configuration TAK Server ports:

+
sudo ufw allow 8089
sudo ufw allow 8443

AWS / GovCloud Recommended Instance Type

+
    +
  • c5.xlarge +
      +
    • 4 vCPU
    • +
    • 8 GB RAM
    • +
    • Up to 10 Gbps network bandwidth
    • +
    +
  • +
  • For 2-server installation, use this instance type for both servers.
  • +
+

TAK Server is a TLS-enabled networking server. In order to ensure consistent performance, burstable AWS EC2 instance types such as T2 are not recommended. TLS and TCP processing requires consistent, continuous CPU performance. C4 and C5 instances are designed for predictable CPU performance, and are better-suited for TAK Server deployments.

+

More information about instance types may be found here: +https://aws.amazon.com/ec2/instance-types

+

Usage of larger instance types or physical servers is supported for scalability, to support more concurrent active users.

Two-Server Upgrade

+

Rocky Linux 8

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, on the core server, install Java 17 and upgrade the core package:

+
sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.1-RELEASEx.noarch.rpm -y
+

Next, on the database server, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Install Java 17. Enable the 'powertools' repo for postgis dependencies. Install TAK Server RPM database and its dependencies.

+
sudo dnf install epel-release -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo dnf update -y
sudo dnf module disable postgresql
sudo dnf install java-17-openjdk-devel -y
sudo dnf config-manager --set-enabled powertools
+

Make sure the database RPM is in the current directory

+
sudo dnf install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, on the core server, install Java 17 and upgrade the core package:

+
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.1-RELEASEx.noarch.rpm -y
+

Next, on the database server, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install Java 17. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies.

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with:

+
sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach
+

Make sure the database RPM is in the current directory

+
sudo dnf install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Install OpenJDK 17 and other dependencies (if you have not previously done so.)

+
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8. +Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, upgrade the core package:

+
sudo rpm -Uvh takserver-core-5.1-RELEASEx.noarch.rpm --nodeps
+

Next, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install TAK Server RPM database and its dependencies.

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum update -y
+

Make sure the database RPM is in the current directory

+
sudo rpm -Uvh takserver-database-5.1-RELEASEx.noarch.rpm --nodeps
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu and Raspberry Pi OS

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, upgrade the core package.

+
sudo apt install ./takserver-core_5.1-RELEASE-x_all.deb
+

Next, upgrade the database.

+
sudo apt install ./takserver-database_5.1-RELEASE-x_all.deb
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Centos 7

+

Install OpenJDK 17 and other dependencies (if you have not previously done so.)

+
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Upgrade the two TAK Server packages on the servers on which they are installed.

+

First, upgrade the core package:

+
sudo rpm -Uvh takserver-core-5.1-RELEASEx.noarch.rpm --nodeps
+

Next, upgrade the database. Setup the extra postgres yum repo for the latest postgres and postgis. Install TAK Server RPM database and its dependencies.

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum update -y
+

Make sure the database RPM is in the current directory

+
sudo yum install takserver-database-5.1-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

This command will make a copy of your existing Postgresql database and update it to version 15. If there is an issue with the upgraded database, you can fall back to the copy of the previous version. If the upgrade succeeds, there will be a delete_old_cluster.sh script automatically created that you can run to safely remove the previous version's data copy.

+

Check Java version in both servers

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java

Server Requirements

+
    +
  • 4 processor cores
  • +
  • 8 GB RAM
  • +
  • 40 GB disk storage
  • +
+

For Raspberry Pi installations, a Pi 4, Model B, Quad-Core 64-bit 8GB RAM version is recommended for a minimal TAK Server setup (TAK Server messaging and api services with local PostgreSQL database)

+

NOTE: Insecure ports are a potential security risk and may allow attackers to gain access to the system resulting in the disclosure of personal and sensitive information. Use of unencrypted ports should be avoided to ensure a secure TAK Server deployment.

Single-Server Upgrade

+

Rocky Linux 8

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable PowerTools (needed for dependencies of postgis.) Install TAK server.

+
sudo dnf install epel-release -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql && sudo dnf update -y
sudo dnf install java-17-openjdk-devel -y
sudo dnf config-manager --set-enabled powertools
+

Upgrade Tak server

+
sudo dnf install takserver-5.1-RELEASEx.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

Setup the extra postgres yum repo for the latest postgres and postgis. Install Java 17. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies.

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with:

+
sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach
+

Upgrade Tak server

+
sudo dnf install takserver-5.1-RELEASEx.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

If you have not previously done so, install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Upgrade TAK server

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -Uvh takserver-5.1-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on Centos 7 and RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu and Raspberry Pi OS

+

Upgrade Tak server

+
sudo apt install ./takserver-5.1-RELEASEx_all.deb
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Centos 7

+

If you have not previously done so, install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Upgrade Tak server.

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -Uvh takserver-5.1-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on Centos 7 and RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java

Supported Operating Systems

+
    +
  • Rocky Linux 8 (Replacement for CentOS 8, which is EOL)
  • +
  • Red Hat Enterprise Linux (RHEL) 8 or 7
  • +
  • Ubuntu 22
  • +
  • Raspberry Pi OS (64-bit)
  • +
  • CentOS 7 (not CentOS 8 Stream)
  • +
+

Java 17 is required. Java 17 is installed by default via package dependencies, but if your system has a different Java version installed in addition to Java 17, ensure that TAK Server is using Java 17.

Overview and Installer Files

+

TAK Server supports multiple deployment configurations:

+
    +
  • Single server install: One server running TAK Server core (messaging, API, plugins and database): recommended for fewer than 500 users.
  • +
  • Two server install: One server running TAK Server core (messaging, API, plugins and database) and a second server running PostgreSQL database: recommended for more than 500 users.
  • +
  • Containerized docker install: One container running TAK Server core (messaging, API, plugins and database) and another container running PostgreSQL database (designed for operating systems other than CentOS 7 / RHEL 7). Hardened containers are published to both tak.gov and IronBank (see https://ironbank.dso.mil/repomap?searchText=tak%20server).
  • +
+

The following installation files are provided:

+

Installer for single-server install

+
    +
  • RHEL/Rocky/CentOS: takserver-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver_5.2-RELEASE-x_all.deb
  • +
+

Database installer for two-server install

+
    +
  • RHEL/Rocky/CentOS: takserver-database-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver-database_5.2-RELEASE-x_all.deb
  • +
+

Core installer for two-server install

+
    +
  • RHEL/Rocky/CentOS: takserver-core-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver-core_5.2-RELEASE-x_all.deb
  • +
+

Containerized docker install bundle

+
    +
  • takserver-docker-5.2-RELEASE-x.zip
  • +
+

Containerized hardeneded docker install bundle

+
    +
  • takserver-docker-hardened-5.2-RELEASE-x.zip
  • +
+

Installer for federation hub (beta)

+
    +
  • RHEL/Rocky/CentOS: takserver-fed-hub-5.2-RELEASE-x.noarch.rpm
  • +
  • Ubuntu/RaspPi: takserver-fed-hub_5.2-RELEASE-x_all.deb
  • +
+

Federation hub documentation available here: +https://wiki.tak.gov/display/TPC/Federation+Hub

+

Verifying GPG signatures

+

Verifying GPG Signatures on RPM Packages

+

The GPG public key for TAK Server can be found under +https://artifacts.tak.gov/ui/repos/tree/General/TAKServer/release/

+

Select the TAK Server release version and download the file takserver-public-gpg.key

+

Import the key to the RPM key management:

+
sudo rpm --import takserver-public-gpg.key
+

Verifying signature for the rpm installer package:

+
rpm --checksig takserver-5.2-RELEASE<version>.noarch.rpm
+

Example of a successful output:

+
takserver-5.2-RELEASE28.noarch.rpm: rsa sha1 (md5) pgp md5 OK
+

Example of a failed output:

+
takserver-5.2-RELEASE28.noarch.rpm: RSA sha1 ((MD5) PGP) md5 NOT OK
(MISSING KEYS: (MD5) PGP#6851f5b5)
+

If the RPM packages were not signed with a GPG key, the output might look like:

+
takserver-5.2-RELEASE25.noarch.rpm: sha1 md5 OK
+

Verifying GPG signatures on DEB packages

+

Select the appropriate TAK Server release version and download the file takserver-public-gpg.key and deb_policy.pol

+

Install the debsig-verify utility:

+
sudo apt install debsig-verify
+

Using the ID within the deb_policy.pol file, ex. 039FCDA2D8907527, run the following command to verify signed TAK Server deb resources:

+
sudo mkdir /usr/share/debsig/keyrings/039FCDA2D8907527
sudo mkdir /etc/debsig/policies/039FCDA2D8907527
sudo touch /usr/share/debsig/keyrings/039FCDA2D8907527/debsig.gpg
sudo gpg --no-default-keyring --keyring /usr/share/debsig/keyrings/039FCDA2D8907527/debsig.gpg --import takserver-public-gpg.key
sudo cp deb_policy.pol /etc/debsig/policies/039FCDA2D8907527/debsig.pol
debsig-verify -v takserver-5.2-RELEASE_all.deb
+

Confirm signature verification by identifying statement:

+
debsig: Verified package from 'TAK Product Center' (TAK Server Release)

Use Setup Wizard to Configure TAK Server

+

The TAK Server configuration wizard will help you set up common configuration options once you have installed and started TAK Server. The wizard will guide you through the setup process for a secure configuration, using the default ports that ATAK and WinTAK will connect to.

+

Once you have created your adminstrative login credentials as in the previous section, go to:

+

https://localhost:8443/setup/ (Recommended. Uses the more secure client certificate)

+

Then follow the prompts to begin configuring. The wizard will first walk you through recommended security configuration:

+

Security Configuration

+

NOTE: Insecure ports are a potential security risk and may allow attackers to gain access to the system resulting in the disclosure of personal and sensitive information. Use of unencrypted ports should be avoided to ensure a secure TAK Server deployment.

+

Followed by the recommended federation configuration, if you wish to set up your TAK Server to support federation. (For more information on federation, go to section 8):

+

Federation Configuration

Configure TAK Server Installation

+
sudo systemctl daemon-reload
+

On resource limited hosts, such as a Raspberry Pi, you may start/stop only essential api and messaging TAK Server services with:

+
sudo systemctl [start|stop] takserver-noplugins
+

Otherwise, to start/stop all TAK Server service:

+
sudo systemctl [start|stop] takserver
+

You can set TAK Server to start at boot by running

+
sudo systemctl enable takserver
+

or with resource limited hosts:

+
sudo systemctl enable takserver-noplugins
+

For secure operation, TAK Server requires a keystore and truststore (X.509 certificates).

+

Next, follow the instructions in Appendix B to create these certificates. TAK Server by default is TLS only, so certificate generation, including an administrative certificate is required for configuration. In addition, if you would like to configure TLS for Postgres database connection, follow additional steps in Appendix D.

+

Verify that the steps in Appendix B have been followed by checking the following items:

+

Certificates are present at:

+
/opt/tak/certs/files
+

The TAK Server was restarted, the admin cert has been generated, and an admin account in TAK Server was created with the command:

+
sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem
+

While following the instructions in Appendix B, you will have created an admin certificate. Import this certificate into your browser, so that you can access the Admin. It will be located here on your TAK Server machine:

+
/opt/tak/certs/files/admin.pem
+

Import this client certificate into your browser.

+

If you are using Firefox, go to Settings -> Preferences -> Privacy & Security -> Certificates -> View Certificates

+

Go to Import. Upload this file:

+
/opt/tak/certs/files/admin.p12
+

Enter the certificate password. The default password is atakatak

+

Browse to:

+
https://localhost:8443
+

Select the admin certificate to log in.

+

An error message similar to this indicates that the correct client certificate has not been imported into the browser:

+

Example Incorrect Client Certificate Error Message

TAK Server Installation

+

Rocky Linux 8

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable PowerTools (needed for dependencies of postgis.) Install TAK server. Apply SELinux takserver-policy.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install epel-release -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf -qy module disable postgresql && sudo dnf update -y
sudo dnf install java-17-openjdk-devel -y
sudo dnf config-manager --set-enabled powertools
sudo dnf install takserver-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

RHEL8 FIPS mode Support. TAK Server has experimental support for RHEL8 FIPS mode. This is intended for evaluation only, for hardened environments. These steps enable TAK Server to operate with RHEL FIPS mode enabled, but does not provide full FIPS 140 compliance. See below for a new option for certs script when using FIPS mode. Client certificates generated with FIPS mode may not work with ATAK.

+
sudo rpm --import http://download.fedoraproject.org/pub/epel/RPM-GPG-KEY-EPEL-8
sudo rpm --import https://download.postgresql.org/pub/repos/yum/RPM-GPG-KEY-PGDG
sudo vi /etc/fapolicyd/rules.d/99-whitelist.rules
+

Add line:

+
deny perm=any all : all
+
sudo vi /etc/fapolicyd/rules.d/39-tak.rules
+

Add lines:

+
allow perm=open all : dir=/opt/tak/ ftype=application/x-sharedlib trust=0
allow perm=open exe=/usr/pgsql-15/bin/postgres : all
+
    +
  • Custom certificates with stronger algorithms will need to be generated for use on systems with FIPS enabled. To do this: follow the existing certificate instructions, but append --fips to the end of each ./makeRootCa.sh and ./makeCert.sh command. Note: ATAK may not support certificates generated with these stronger algorithms*
  • +
+

--- End FIPS Mode Commands ---

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository. Install java 17. Disable the postgresql module (so the later postgresql and postgis specific versions aren't inaccessible due to 'modular filtering'). Enable Repository Management and repository CodeReady Builder. Install TAK server. Apply SELinux takserver-policy.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install -y https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with:

+
sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach
+
sudo dnf install takserver-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install TAK server

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -ivh takserver-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu and Raspberry Pi OS

+

Install the postgres repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install TAK server

+
sudo mkdir -p /etc/apt/keyrings
sudo curl https://www.postgresql.org/media/keys/ACCC4CF8.asc --output /etc/apt/keyrings/postgresql.asc
sudo sh -c 'echo "deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/postgresql.list'
sudo apt update
sudo apt install ./takserver-5.2-RELEASEx_all.deb
+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

CentOS 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
sudo rpm -ivh takserver-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on CentOS 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version:

+
java -version
+

This should tell you have 17.x.y. If the command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

yum Install From tak.gov

+

Check to see if a .repo file exists for tak.gov

+
ls /etc/yum.repos.d/Takserver.repo
+

If it exists, skip down to the yum install of takserver database. If it doesn't exist, create a new .repo file to point yum to the yum repo on tak.gov.

+
cd /etc/yum.repos.d
sudo vi Takserver.repo
+

You can also edit using another editor besides vi. Update the file Takserver.repo to contain:

+
[takrepo]
name=TakserverRepository
baseurl=https://<ARTIFACTORY_USER>:<ARTIFACTORY_TOKEN>@artifacts.tak.gov/artifactory/takserver-yum
enabled=1
gpgcheck=0
+

Where <ARTIFACTORY_USER> is a valid login to Artifactory that has access to the takserver-yum repo and <ARTIFACTORY_TOKEN> is a special token unique to the given Artifactory user that can be retrieved by that user using the "Set Me Up" menu option to retrieve it.

+

Note: Do NOT use your password as it the Token is more secure and cannot be used for logging in. Only for retrieving or publishing. Also note: When you change the password of the given user, you will also need to retrieve the new token which is based on it and update the baseurl in the Takserver.repo file.

+

Save the Takserver.repo file and then do the install of the takserver.

+
sudo yum install takserver-core-5.2-RELEASEx

Install TAK Server

+

Rocky Linux 8

+
sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf install takserver-core-5.2-RELEASEx.noarch.rpm -y
sudo dnf install checkpolicy
cd /opt/tak && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linuxx64_bin.rpm
sudo rpm -ivh takserver-core-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu & Raspberry Pi OS

+
sudo apt install takserver-core-5.2-RELEASEx_all.deb
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

CentOS 7

+

Install EPEL (EPEL provides certain dependencies required by PostgreSQL.) Install postgres yum repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install OpenJDK 17 and other dependencies. Install Tak server.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install epel-release -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linuxx64_bin.rpm
sudo rpm -ivh takserver-core-5.2-RELEASEx.noarch.rpm --nodeps
+

Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

yum install From tak.gov

+

Check to see if a .repo file exists for tak.gov

+
ls /etc/yum.repos.d/Takserver.repo
+

If it exists, skip down to the yum install of takserver database. If it doesn't exist, create a new .repo file to point yum to the yum repo on tak.gov.

+
cd /etc/yum.repos.d
sudo vi Takserver.repo
+

You can also edit using another editor besides vi. Update the file Takserver.repo to contain:

+
[takrepo]
name=TakserverRepository
baseurl=https://<ARTIFACTORY_USER>:<ARTIFACTORY_TOKEN>@artifacts.tak.gov/artifactory/takserver-yum
enabled=1
gpgcheck=0
+

Where <ARTIFACTORY_USER> is a valid login to Artifactory that has access to the takserver-yum repo and <ARTIFACTORY_TOKEN> is a special token unique to the given Artifactory user that can be retrieved by that user using the "Set Me Up" menu option to retrieve it.

+

Note: Do NOT use your password as it the Token is more secure and cannot be used for logging in. Only for retrieving or publishing.

+

Also note: When you change the password of the given user, you will also need to retrieve the new token which is based on it and update the baseurl in the Takserver.repo file.

+

Save the Takserver.repo file and then do the install of the takserver.

+
sudo yum install takserver-core-5.2-RELEASEx

Dependency Setup

+

First, update firewall rules to allow communication with server two, for TCP port 5432.

+

Rocky Linux 8

+

Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Enable the 'powertools' repo for postgis dependencies. Install TAK Server RPM database and its dependencies.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install epel-release -y

sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y

sudo dnf update -y

sudo dnf module disable postgresql

sudo dnf install java-17-openjdk-devel -y

sudo dnf config-manager --set-enabled powertools

Make sure the database RPM is in the current directory

sudo dnf install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 8

+

Setup the extra postgres yum repo for the latest postgres and postgis. Disable the postgresql stream to install the specific postgres version we depend on. Install TAK Server RPM database and its dependencies.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo dnf install https://dl.fedoraproject.org/pub/epel/epel-release-latest-8.noarch.rpm -y
sudo dnf install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y
+
sudo dnf update -y && sudo dnf install java-17-openjdk-devel -y
sudo dnf module disable postgresql
sudo subscription-manager config --rhsm.manage_repos=1
sudo subscription-manager repos --enable codeready-builder-for-rhel-8-x86_64-rpms
+

Note: If you get the error ‘This system has no repositories available through subscriptions’, you need to subscribe your system with "sudo subscription-manager register --username <your_username> --password <your_password> --auto-attach"

+

Make sure the database RPM is in the current directory

+
sudo dnf install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

RHEL 7

+

Setup the extra postgres yum repo for the latest postgres and postgis. Install OpenJDK 17 and other dependencies. Install TAK Server RPM database.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm -y
sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-7-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y

sudo yum update -y

sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on RHEL 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Make sure the database RPM is in the current directory

+
sudo rpm -ivh takserver-database-5.2-RELEASEx.noarch.rpm --nodeps
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

Ubuntu & Raspberry Pi OS

+

Install the postgres repository (required in order to install up-to-date Postgresql and PostGIS packages.) Install TAK Server database. Use database DEB. Configure TAK Server Database installation

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo mkdir -p /etc/apt/keyrings

sudo curl https://www.postgresql.org/media/keys/ACCC4CF8.asc --output /etc/apt/keyrings/postgresql.asc

sudo sh -c 'echo "deb [signed-by=/etc/apt/keyrings/postgresql.asc] http://apt.postgresql.org/pub/repos/apt/ $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/postgresql.list'

sudo apt update

sudo apt install takserver-database-5.2-RELEASEx_all.deb
+

Open the file /opt/tak/CoreConfig.example.xml and look for the auto-generated password for the database. This password will be used to configure the Core Server.

+
<connection url="jdbc:postgresql://127.0.0.1:5432/cot" username="martiuser" password="Database_password" />
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java
+

CentOS 7

+

Setup the extra postgres yum repo for the latest postgres and postgis. Install OpenJDK 17 and other dependencies. Install TAK Server RPM database.

+

Note that when installing postgres, you may run into issues related to the GPG key – if you need to update the key, you can modify the postgres installation command based on your operating system according to the guidelines here: https://yum.postgresql.org/news/pgdg-rpm-repo-gpg-key-update/

+
sudo yum install epel-release -y

sudo yum install https://download.postgresql.org/pub/repos/yum/reporpms/EL-8-x86_64/pgdg-redhat-repo-latest.noarch.rpm -y

sudo yum update -y

sudo yum install -y postgis33_15 postgis33_15-utils
sudo yum install -y postgresql15-server postgresql15-contrib
sudo yum install -y https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.rpm
+

Note that the yum package manager does not currently support JDK 17 on Centos 7. By installing the package manually, you will be responsible for future security updates. For a safer long-term solution, we recommend that you update your OS to RHEL 8 or Rocky Linux 8.

+

Make sure the database RPM is in the current directory

+
sudo yum install takserver-database-5.2-RELEASE-x.noarch.rpm --setopt=clean_requirements_on_remove=false -y
+

Check Java version

+
java -version
+

This should tell you you have 17.x.y. If the "java -version" command tells you your Java version is not 17.x.y, then you can use the alternatives command to change it:

+
sudo alternatives --config java

Configuration

+

Configure database connection by updating /opt/tak/CoreConfig.xml:

+
<repository enable="true" numDbConnections="200" primaryKeyBatchSize="500"
insertionBatchSize="500">
<connection url="jdbc:postgresql://<Database_server_IP_address>:5432/cot" username="martiuser"
password="Database_password"/>
</repository>
+
sudo systemctl daemon-reload
+

Start/stop TAK Server services with:

+
sudo systemctl [start|stop] takserver
+

Or on resource limited hosts:

+
sudo systemctl [start|stop] takserver-noplugins
+

You can set TAK Server to start at boot by running

+
sudo systemctl enable takserver
+

For secure operation, TAK Server requires a keystore and truststore (X.509 certificates).

+

Next, follow the instructions in Appendix B to create these certificates. TAK Server by default is TLS only, so certificate generation, including an administrative certificate is required for configuration.

+

Verify that the steps in Appendix B have been followed by checking the following items:

+

Certificates are present at:

+
/opt/tak/certs/files
+

The admin cert has been generated and an admin account in TAK Server was created with the command:

+
sudo java -jar /opt/tak/utils/UserManager.jar certmod -A /opt/tak/certs/files/admin.pem
+

Import this client certificate into your browser.

+

If you are using Firefox, go to Settings -> Preferences -> Privacy & Security -> Certificates -> View Certificates

+

Go to Import. Upload this file:

+
/opt/tak/certs/files/admin.p12
+

Enter the certificate password. The default password is atakatak

+

Browse to:

+
https://localhost:8443
+

Select the admin certificate to log in.

+

An error message similar to this indicates that the correct client certificate has not been imported into the browser:

+

Example Incorrect Client Certificate Error Message

+

Once logged in with the admin certificate, configure the TAK Server with the following instructions:

+

Configure TAK Server to connect to the database. Access the Database configuration settings:

+

Example Incorrect Client Certificate Error Message

+

Edit the database connection address, specifying the hostname or IP address of the database server:

+

Example Incorrect Client Certificate Error Message

+

Restart TAK Server

+
sudo systemctl restart takserver
+

Or on resource limited hosts

+
sudo systemctl restart takserver-noplugins
+

If you would like to configure TLS for Postgres database connection, refer to Appendix D.

RHEL, Rocky Linux, and CentOS

+

To verify whether a firewall is running, use the command:

+
sudo systemctl status  firewalld.service
+

To see what zones are running

+
sudo firewall-cmd --get-active-zones
+

If you are working from a fresh OS install, the only active zone is 'public'.

+

For each each zone, you'll want to enable TCP (and possibly UDP) ports for the inputs in your CoreConfig.xml file, plus the web server's port. For example,

+
sudo firewall-cmd --zone=public --add-port 8089/tcp -permanent
sudo firewall-cmd --zone=public --add-port 8443/tcp --permanent
+

The ports you'll need to open for the default configuration are 8089 and 8443.

+

Finally, enable your new firewall rules:

+
sudo firewall-cmd -reload
+
+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static.css b/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static.css new file mode 100644 index 00000000..c1af9b32 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static.css @@ -0,0 +1,31 @@ +@media (max-width: 996px) { + + aside.theme-doc-sidebar-container { + border-right: 1px solid var(--ifm-toc-border-color); + position: fixed; + width: var(--ifm-navbar-sidebar-width); + height: calc(100% - var(--ifm-navbar-height)); + z-index: var(--ifm-z-index-dropdown); + background-color: var(--ifm-navbar-background-color); + } + + aside.theme-doc-sidebar-container nav.menu { + display: inherit; + padding: 0.5rem 0 0.5rem 0.5rem; + scrollbar-gutter: stable; + } + + nav.navbar div.navbar__items div[class*="colorModeToggle_"], + nav.navbar div.navbar__items .navbar__item { + display: revert; + } + + nav.navbar div.navbar__items div[class*="navbarSearchContainer_"] { + position: revert; + } + +} + +div[class*="colorModeToggle_"] > button.clean-btn { + cursor: pointer; +} diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static.js b/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static.js new file mode 100644 index 00000000..0cae0243 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/docusaurus-static.js @@ -0,0 +1,67 @@ +(function(){ + + var pageElemQuery = 'div.main-wrapper[data-docusaurus-static-path]'; + var documentData = document.documentElement.dataset; + + function ToggleElemDisplay (elem) { + elem.style.display = (!elem.style.display ? 'revert' : ''); + } + + /* remove duplicate menu buttons */ + Array.from(document.querySelectorAll('div.navbar__items > button.navbar__toggle')).slice(1).forEach(function(elem){ + elem.remove(); + }); + + /* initialize navigation menu */ + document.querySelector('div.navbar__items > button.navbar__toggle').onclick = function(){ + Array.from(document.querySelectorAll('div[class*="docRoot_"] > aside.theme-doc-sidebar-container, div.main-wrapper nav[class*="sidebar_"]')).forEach(function(elem){ + ToggleElemDisplay(elem); + }); + }; + + /* theme switching */ + var themeButtonElem = document.querySelector('div[class*="colorModeToggle_"] > button'); + themeButtonElem.disabled = false; + themeButtonElem.onclick= function(){ + documentData.theme = (!documentData.theme || documentData.theme === 'light' ? 'dark' : 'light'); + localStorage.setItem('theme', documentData.theme); + var lightStyle = document.querySelector(`div.navbar__logo > img[class*="light"]`).style; + var darkStyle = document.querySelector(`div.navbar__logo > img[class*="dark"]`).style; + switch (documentData.theme) { + case 'light': + lightStyle.display = 'unset'; + darkStyle.display = 'none'; + break; + case 'dark': + lightStyle.display = 'none'; + darkStyle.display = 'unset'; + break; + } + }; + + /* set document to default theme */ + if (!['light', 'dark'].includes(documentData.theme)) { + documentData.theme = 'light'; + } + + window.addEventListener('load', function(){ + + /* initialize ToC menus */ + var desktopTocQuery = 'div.theme-doc-toc-desktop'; + var mobileTocQuery = 'div.theme-doc-toc-mobile'; + Array.from(document.querySelectorAll('div[class*="docRoot_"]')).forEach(function(docElem){ + var mobileTocElem = docElem.querySelector(`${mobileTocQuery}`); + if (mobileTocElem) { + mobileTocElem.innerHTML += docElem.querySelector(`${desktopTocQuery}`).outerHTML; + var newTocElem = mobileTocElem.querySelector(`${desktopTocQuery}`); + var newTocListElem = newTocElem.querySelector('ul'); + newTocListElem.className = newTocListElem.className.replaceAll('table-of-contents__left-border', ''); + docElem.querySelector(`${mobileTocQuery} > button`).onclick= function(){ + ToggleElemDisplay(newTocElem); + }; + } + }); + + }); + +})(); diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/docusaurus-social-card.jpg b/src/takserver-core/src/main/webapp/Marti/documentation/img/docusaurus-social-card.jpg new file mode 100644 index 00000000..ffcb4482 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/img/docusaurus-social-card.jpg differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/docusaurus.png b/src/takserver-core/src/main/webapp/Marti/documentation/img/docusaurus.png new file mode 100644 index 00000000..f458149e Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/img/docusaurus.png differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/favicon.ico b/src/takserver-core/src/main/webapp/Marti/documentation/img/favicon.ico new file mode 100644 index 00000000..c01d54bc Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/img/favicon.ico differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/logo.svg b/src/takserver-core/src/main/webapp/Marti/documentation/img/logo.svg new file mode 100644 index 00000000..9db6d0d0 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/img/logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/tak.PNG b/src/takserver-core/src/main/webapp/Marti/documentation/img/tak.PNG new file mode 100644 index 00000000..71a74695 Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/img/tak.PNG differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/tak.ico b/src/takserver-core/src/main/webapp/Marti/documentation/img/tak.ico new file mode 100644 index 00000000..8610dd5d Binary files /dev/null and b/src/takserver-core/src/main/webapp/Marti/documentation/img/tak.ico differ diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_mountain.svg b/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_mountain.svg new file mode 100644 index 00000000..af961c49 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_mountain.svg @@ -0,0 +1,171 @@ + + Easy to Use + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_react.svg b/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_react.svg new file mode 100644 index 00000000..94b5cf08 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_react.svg @@ -0,0 +1,170 @@ + + Powered by React + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_tree.svg b/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_tree.svg new file mode 100644 index 00000000..d9161d33 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/img/undraw_docusaurus_tree.svg @@ -0,0 +1,40 @@ + + Focus on What Matters + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/index.html new file mode 100644 index 00000000..776b1133 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/index.html @@ -0,0 +1,14 @@ + + + + + +Hello from TAK Server Documentation | TAK Server Documentation + + + + + + + + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/markdown-page/index.html b/src/takserver-core/src/main/webapp/Marti/documentation/markdown-page/index.html new file mode 100644 index 00000000..717eab3c --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/markdown-page/index.html @@ -0,0 +1,15 @@ + + + + + +Markdown page example | TAK Server Documentation + + + + + +

Markdown page example

+

You don't need React to write simple standalone pages.

+ + \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/documentation/sitemap.xml b/src/takserver-core/src/main/webapp/Marti/documentation/sitemap.xml new file mode 100644 index 00000000..3525ba20 --- /dev/null +++ b/src/takserver-core/src/main/webapp/Marti/documentation/sitemap.xml @@ -0,0 +1 @@ +https://your-docusaurus-site.example.com/blogweekly0.5https://your-docusaurus-site.example.com/blog/2024/01/28/welcome-to-docusaurus-staticweekly0.5https://your-docusaurus-site.example.com/blog/archiveweekly0.5https://your-docusaurus-site.example.com/blog/first-blog-postweekly0.5https://your-docusaurus-site.example.com/blog/long-blog-postweekly0.5https://your-docusaurus-site.example.com/blog/mdx-blog-postweekly0.5https://your-docusaurus-site.example.com/blog/tagsweekly0.5https://your-docusaurus-site.example.com/blog/tags/docusaurusweekly0.5https://your-docusaurus-site.example.com/blog/tags/docusaurus-staticweekly0.5https://your-docusaurus-site.example.com/blog/tags/facebookweekly0.5https://your-docusaurus-site.example.com/blog/tags/helloweekly0.5https://your-docusaurus-site.example.com/blog/tags/holaweekly0.5https://your-docusaurus-site.example.com/blog/welcomeweekly0.5https://your-docusaurus-site.example.com/markdown-pageweekly0.5https://your-docusaurus-site.example.com/docs/aboutweekly0.5https://your-docusaurus-site.example.com/docs/appendixaweekly0.5https://your-docusaurus-site.example.com/docs/appendixbweekly0.5https://your-docusaurus-site.example.com/docs/appendixcweekly0.5https://your-docusaurus-site.example.com/docs/appendixdweekly0.5https://your-docusaurus-site.example.com/docs/appendixeweekly0.5https://your-docusaurus-site.example.com/docs/changelogweekly0.5https://your-docusaurus-site.example.com/docs/configuration/authenticationbackendsweekly0.5https://your-docusaurus-site.example.com/docs/configuration/configuremessagingrepositorywebuiweekly0.5https://your-docusaurus-site.example.com/docs/configuration/configurewebuiweekly0.5https://your-docusaurus-site.example.com/docs/configuration/groupassignmentbyinputweekly0.5https://your-docusaurus-site.example.com/docs/configuration/groupassignmentusingauthweekly0.5https://your-docusaurus-site.example.com/docs/configuration/groupassignmentusingclientcertsweekly0.5https://your-docusaurus-site.example.com/docs/configuration/groupfilteringweekly0.5https://your-docusaurus-site.example.com/docs/configuration/optionallydisableuiweekly0.5https://your-docusaurus-site.example.com/docs/configuration/overviewweekly0.5https://your-docusaurus-site.example.com/docs/configuration/vbmadminconfigweekly0.5https://your-docusaurus-site.example.com/docs/dataretentiontoolweekly0.5https://your-docusaurus-site.example.com/docs/deviceprofilesweekly0.5https://your-docusaurus-site.example.com/docs/dockerinstall/buildinstallweekly0.5https://your-docusaurus-site.example.com/docs/dockerinstall/ironbankweekly0.5https://your-docusaurus-site.example.com/docs/federation/datapackagemissionfileblockerweekly0.5https://your-docusaurus-site.example.com/docs/federation/enablefederationweekly0.5https://your-docusaurus-site.example.com/docs/federation/federatedgroupmappingweekly0.5https://your-docusaurus-site.example.com/docs/federation/federationdisruptiontoleranceweekly0.5https://your-docusaurus-site.example.com/docs/federation/federationexampleweekly0.5https://your-docusaurus-site.example.com/docs/federation/maketheconnectionweekly0.5https://your-docusaurus-site.example.com/docs/federation/overviewweekly0.5https://your-docusaurus-site.example.com/docs/federation/uploadfederatecertweekly0.5https://your-docusaurus-site.example.com/docs/firewall/overviewweekly0.5https://your-docusaurus-site.example.com/docs/firewall/rhelrockycentosweekly0.5https://your-docusaurus-site.example.com/docs/firewall/ubunturaspberrypiweekly0.5https://your-docusaurus-site.example.com/docs/groupfilteringformulticastweekly0.5https://your-docusaurus-site.example.com/docs/imagetestweekly0.5https://your-docusaurus-site.example.com/docs/installation/oneserver/prerequisiteweekly0.5https://your-docusaurus-site.example.com/docs/installation/oneserver/takserverconfigurationweekly0.5https://your-docusaurus-site.example.com/docs/installation/oneserver/takserverinstallationweekly0.5https://your-docusaurus-site.example.com/docs/installation/overviewweekly0.5https://your-docusaurus-site.example.com/docs/installation/setup_wizardweekly0.5https://your-docusaurus-site.example.com/docs/installation/twoserver/overviewweekly0.5https://your-docusaurus-site.example.com/docs/installation/twoserver/serverone/dependencysetupweekly0.5https://your-docusaurus-site.example.com/docs/installation/twoserver/servertwo/configuretakserverweekly0.5https://your-docusaurus-site.example.com/docs/installation/twoserver/servertwo/installtakserverweekly0.5https://your-docusaurus-site.example.com/docs/installation/twoserver/servertwo/prerequisitesweekly0.5https://your-docusaurus-site.example.com/docs/loggingweekly0.5https://your-docusaurus-site.example.com/docs/metricsweekly0.5https://your-docusaurus-site.example.com/docs/oath2authenticationweekly0.5https://your-docusaurus-site.example.com/docs/softwareinstallationlocationweekly0.5https://your-docusaurus-site.example.com/docs/system-requirements/awsrequirementsweekly0.5https://your-docusaurus-site.example.com/docs/system-requirements/serverrequirementsweekly0.5https://your-docusaurus-site.example.com/docs/system-requirements/systemrequirementsweekly0.5https://your-docusaurus-site.example.com/docs/upgrade/overviewweekly0.5https://your-docusaurus-site.example.com/docs/upgrade/singleserverweekly0.5https://your-docusaurus-site.example.com/docs/upgrade/twoserverweekly0.5https://your-docusaurus-site.example.com/docs/usermanagementuiweekly0.5https://your-docusaurus-site.example.com/docs/webtakweekly0.5https://your-docusaurus-site.example.com/weekly0.5 \ No newline at end of file diff --git a/src/takserver-core/src/main/webapp/Marti/federation/js/controllers.js b/src/takserver-core/src/main/webapp/Marti/federation/js/controllers.js index ad14767e..8384752f 100644 --- a/src/takserver-core/src/main/webapp/Marti/federation/js/controllers.js +++ b/src/takserver-core/src/main/webapp/Marti/federation/js/controllers.js @@ -735,16 +735,11 @@ federationManagerControllers.controller('OutgoingConnectionModificationCtrl', [' $scope.saveOutgoingConnection = function (outgoingConnection) { // if outgoing is enabled, warn that current changes will restart connection if ($scope.originalOutgoing.enabled && $scope.modifiedOutgoing.enabled) { - if ($scope.originalOutgoing.displayName !== $scope.modifiedOutgoing.displayName || - $scope.originalOutgoing.address !== $scope.modifiedOutgoing.address || - $scope.originalOutgoing.port !== $scope.modifiedOutgoing.port || - $scope.originalOutgoing.protocolVersion !== $scope.modifiedOutgoing.protocolVersion) { - if (confirm('Config changes will restart the connection')) { - // nothing to do - } else { - alert('Save Canceled.'); - return; - } + if (confirm('Config changes will restart the connection')) { + // nothing to do + } else { + alert('Save Canceled.'); + return; } } diff --git a/src/takserver-core/src/main/webapp/Marti/federation/partials/modifyOutgoingConnection.html b/src/takserver-core/src/main/webapp/Marti/federation/partials/modifyOutgoingConnection.html index f31f3065..3ce3e150 100644 --- a/src/takserver-core/src/main/webapp/Marti/federation/partials/modifyOutgoingConnection.html +++ b/src/takserver-core/src/main/webapp/Marti/federation/partials/modifyOutgoingConnection.html @@ -174,6 +174,16 @@

Edit Outgoing Connection

+ + + + +
+ This field is used for creating a federation connection using token authentication rather than X509 client mutual authentication. +
+ Warning!! X509 mutual authentication is disabled and is being replaced by token authentication. + + diff --git a/src/takserver-core/src/main/webapp/Marti/federation/partials/newOutgoingConnection.html b/src/takserver-core/src/main/webapp/Marti/federation/partials/newOutgoingConnection.html index 9b81fce4..a5ba9534 100644 --- a/src/takserver-core/src/main/webapp/Marti/federation/partials/newOutgoingConnection.html +++ b/src/takserver-core/src/main/webapp/Marti/federation/partials/newOutgoingConnection.html @@ -208,6 +208,16 @@

Create Outgoing Connection

+ + + + +
+ This field is used for creating a federation connection using token authentication rather than X509 client mutual authentication. +
+ Warning!! X509 mutual authentication is disabled and is being replaced by token authentication. + + diff --git a/src/takserver-core/src/main/webapp/Marti/menubar.html b/src/takserver-core/src/main/webapp/Marti/menubar.html index b7c32ad5..33e8c400 100644 --- a/src/takserver-core/src/main/webapp/Marti/menubar.html +++ b/src/takserver-core/src/main/webapp/Marti/menubar.html @@ -178,6 +178,8 @@
  • Plugins
  • + +
  • Documentation
  • diff --git a/src/takserver-core/src/test/java/tak/server/KMLMissionServletTests.java b/src/takserver-core/src/test/java/tak/server/KMLMissionServletTests.java index e6c8c601..cffeff15 100644 --- a/src/takserver-core/src/test/java/tak/server/KMLMissionServletTests.java +++ b/src/takserver-core/src/test/java/tak/server/KMLMissionServletTests.java @@ -255,18 +255,18 @@ public void initKMLExtendedData() { List schemas = doc.getSchema(); assertEquals(1, schemas.size()); - List schemaExtension = schemas.get(0).getSchemaExtension(); + List schemaExtension = schemas.get(0).getSchemaExtension(); assertEquals(3, schemaExtension.size()); - SimpleArrayField schemaField = schemaExtension.get(0); + SimpleArrayField schemaField = (SimpleArrayField)schemaExtension.get(0); assertEquals("speed", schemaField.getName()); assertEquals("Speed m/s", schemaField.getDisplayName()); - schemaField = schemaExtension.get(1); + schemaField = (SimpleArrayField)schemaExtension.get(1); assertEquals("ce", schemaField.getName()); assertEquals("Circular Error (m)", schemaField.getDisplayName()); - schemaField = schemaExtension.get(2); + schemaField = (SimpleArrayField)schemaExtension.get(2); assertEquals("le", schemaField.getName()); assertEquals("Linear Error (m)", schemaField.getDisplayName()); } diff --git a/src/takserver-core/takserver-war/build.gradle b/src/takserver-core/takserver-war/build.gradle index ffe124b5..f6c9050b 100644 --- a/src/takserver-core/takserver-war/build.gradle +++ b/src/takserver-core/takserver-war/build.gradle @@ -150,7 +150,8 @@ dependencies { //implementation 'javax.activation:activation:' + javax_activation_version implementation('org.glassfish.jaxb:jaxb-runtime:' + jaxb_glassfish_version) - api name: 'JavaAPIforKml-2.2.2' + implementation group: 'uk.m0nom', name: 'javaapiforkml', version: javaapiforkml_version + implementation group: 'com.squareup.okhttp3', name: 'okhttp', version: okhttp3_version } diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/MissionKMLServlet.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/MissionKMLServlet.java index b2d8d338..d1dbbbce 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/MissionKMLServlet.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/MissionKMLServlet.java @@ -263,9 +263,11 @@ public void doPost(HttpServletRequest request, HttpServletResponse response) } boolean includeExtendedData = false; - if (extendedData != null && extendedData.equalsIgnoreCase("true")) { - includeExtendedData = true; - } + + // ignore extendedData for now until JAK custom schema issue is resolved +// if (extendedData != null && extendedData.equalsIgnoreCase("true")) { +// includeExtendedData = true; +// } boolean optimizeExportAsBoolean = true; //Default to true to maintain backward compatibility if (optimizeExport != null && optimizeExport.equalsIgnoreCase("false")) { diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/TracksKMLServlet.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/TracksKMLServlet.java index 88f1089b..2c0a8020 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/TracksKMLServlet.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/TracksKMLServlet.java @@ -278,7 +278,9 @@ protected void doPost(HttpServletRequest request, HttpServletResponse response) throw new IllegalStateException("empty group vector"); } - logger.info("group vector: " + groupVector); + if (logger.isDebugEnabled()) { + logger.debug("group vector: " + groupVector); + } String uid = getParameterValue(httpParameters, QueryParameter.uid.name()); String callsign = getParameterValue(httpParameters, QueryParameter.callsign.name()); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/device/profile/api/ProfileAdminAPI.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/device/profile/api/ProfileAdminAPI.java index 8848d045..b7c178d8 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/device/profile/api/ProfileAdminAPI.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/device/profile/api/ProfileAdminAPI.java @@ -9,7 +9,6 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; -import javax.transaction.Transactional; import org.jetbrains.annotations.NotNull; import org.owasp.esapi.errors.IntrusionException; @@ -22,6 +21,7 @@ import org.springframework.data.domain.Sort; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -198,6 +198,7 @@ public ResponseEntity deleteProfile(@PathVariable("id") @NotNull Long id) profileRepository.deleteById(id); return new ResponseEntity(HttpStatus.OK); } catch (Exception e) { + logger.error("exception in deleteProfile!", e); throw new TakException("exception in deleteProfile", e); } } diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/excheck/ExCheckService.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/excheck/ExCheckService.java index 94814d5e..85edcf3a 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/excheck/ExCheckService.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/excheck/ExCheckService.java @@ -664,8 +664,10 @@ private void updateChecklistMission(Checklist oldChecklist, Checklist newCheckli getExCheckService().deleteChecklistTask(checklistUid, taskUid, clientUid, groupVector); } + Mission checklistMission = missionService.getMissionByNameCheckGroups(checklistUid, groupVector); + subscriptionManager.announceMissionChange( - null, checklistUid, SubscriptionManagerLite.ChangeType.METADATA, clientUid, EXCHECK_TOOL, null); + checklistMission.getGuidAsUUID(), checklistUid, SubscriptionManagerLite.ChangeType.METADATA, clientUid, EXCHECK_TOOL, null); } @CacheEvict(value = Constants.EXCHECK_CACHE, allEntries = true) diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/jwt/JwtUtils.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/jwt/JwtUtils.java index 0cbf4aec..c9b43353 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/jwt/JwtUtils.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/jwt/JwtUtils.java @@ -9,6 +9,7 @@ import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; +import org.apache.commons.codec.binary.Base64; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -20,10 +21,12 @@ import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.KeyStore; +import java.security.NoSuchAlgorithmException; import java.security.PrivateKey; import java.security.PublicKey; import java.security.cert.Certificate; import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; import java.security.spec.X509EncodedKeySpec; import java.util.ArrayList; import java.util.Enumeration; @@ -176,6 +179,12 @@ private JwtParser getParser(SignatureAlgorithm signatureAlgorithm, Key key) { return parser; } + private RSAPublicKey loadPublicKey(byte[] key) throws NoSuchAlgorithmException, InvalidKeySpecException { + X509EncodedKeySpec spec = new X509EncodedKeySpec(key); + KeyFactory kf = KeyFactory.getInstance("RSA"); + return (RSAPublicKey) kf.generatePublic(spec); + } + public List getExternalVerifiers() { try { Oauth oAuth = CoreConfigFacade.getInstance().getRemoteConfiguration().getAuth().getOauth(); @@ -194,10 +203,26 @@ public List getExternalVerifiers() { List rsaPublicKeys = new ArrayList<>(); for (Oauth.AuthServer authServer : oAuth.getAuthServer()) { - byte[] keyBytes = Files.readAllBytes(Paths.get(authServer.getIssuer())); - X509EncodedKeySpec spec = new X509EncodedKeySpec(keyBytes); - KeyFactory kf = KeyFactory.getInstance("RSA"); - rsaPublicKeys.add((RSAPublicKey) kf.generatePublic(spec)); + try { + String issuer = authServer.getIssuer(); + byte[] keyBytes = Files.readAllBytes(Paths.get(issuer)); + + if (issuer.toLowerCase().endsWith(".pem")) { + String key = new String(keyBytes); + String[] keys = key.split("-----BEGIN PUBLIC KEY-----"); + for (int i = 1; i < keys.length; i++) { + keys[i] = keys[i] + .replaceAll("\n", "") + .replaceAll("-----END PUBLIC KEY-----", ""); + byte[] decoded = Base64.decodeBase64(keys[i]); + rsaPublicKeys.add(loadPublicKey(decoded)); + } + } else { + rsaPublicKeys.add(loadPublicKey(keyBytes)); + } + } catch (Exception e) { + logger.error("exception loading authServer public key", e); + } } return rsaPublicKeys; diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/logging/AuditLogUtil.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/logging/AuditLogUtil.java index 6a0d6509..dbb9817c 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/logging/AuditLogUtil.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/logging/AuditLogUtil.java @@ -27,6 +27,7 @@ import com.bbn.security.web.MartiValidatorConstants; import com.google.common.base.Strings; +import org.springframework.web.util.ContentCachingRequestWrapper; import org.springframework.web.util.ContentCachingResponseWrapper; import tak.server.Constants; @@ -139,7 +140,7 @@ private static void setMdcRoles(Principal principal) { } } - public static void setMdc(HttpServletRequest req, HttpServletResponse resp) throws IOException { + public static void setMdc(HttpServletRequest req, HttpServletResponse resp) { // request setMdcUsernameAndRoles(req); @@ -148,12 +149,12 @@ public static void setMdc(HttpServletRequest req, HttpServletResponse resp) thro MDC.put("source-ip", req.getRemoteAddr()); MDC.put("session", req.getRequestedSessionId()); MDC.put("method", req.getMethod()); - MDC.put("request-body", IOUtils.toString(req.getInputStream(), StandardCharsets.UTF_8)); + MDC.put("request-body", new String(new ContentCachingRequestWrapper(req).getContentAsByteArray(), StandardCharsets.UTF_8)); // response MDC.put("response-status", String.valueOf(resp.getStatus())); resp.getHeaderNames().forEach(name -> MDC.put("response-"+(String) name, (String) resp.getHeader((String) name))); - MDC.put("response-body", IOUtils.toString(new ContentCachingResponseWrapper(resp).getContentInputStream(), StandardCharsets.UTF_8)); + MDC.put("response-body", new String(new ContentCachingResponseWrapper(resp).getContentAsByteArray(), StandardCharsets.UTF_8)); } private static void setMdcUsernameAndRoles(HttpServletRequest request) { diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/ContactManagerService.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/ContactManagerService.java index 82d23957..c0f6d26b 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/ContactManagerService.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/ContactManagerService.java @@ -41,6 +41,8 @@ public class ContactManagerService { private ContactCacheHelper contactCache; private AtomicLong lastUpdateMillis = new AtomicLong(-1); + + private volatile List result = null; public List getCachedClientEndpointData(boolean connected, boolean recent, String groupVector, long secAgo) { @@ -49,8 +51,6 @@ public List getCachedClientEndpointData(boolean connected, boole boolean skipCache = !CoreConfigFacade.getInstance().getCachedConfiguration().getBuffer().getQueue().isEnableClientEndpointCache(); - List result = null; - String key = contactCache.getKeyGetCachedClientEndpointData(connected, recent, secAgo); result = (List) contactCache.getContactsCache().getIfPresent(key); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/FederationApi.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/FederationApi.java index 980d662f..31a106a5 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/FederationApi.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/FederationApi.java @@ -790,7 +790,8 @@ public ResponseEntity> createOutgoing outgoingConnection.isUnlimitedRetries(), outgoingConnection.isEnabled(), outgoingConnection.getProtocolVersion(), - outgoingConnection.getFallback()); + outgoingConnection.getFallback(), + outgoingConnection.getConnectionToken()); result = new ResponseEntity>(new ApiResponse(Constants.API_VERSION, Federation.FederationOutgoing.class.getName(), outgoingConnection), HttpStatus.OK); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/OutgoingConnectionSummary.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/OutgoingConnectionSummary.java index 5915e621..a864c6fa 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/OutgoingConnectionSummary.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/network/OutgoingConnectionSummary.java @@ -88,6 +88,14 @@ public int getReconnectInterval() { public void setReconnectInterval(Integer value) { this.outgoingConnection.setReconnectInterval(value); } + + public String getConnectionToken() { + return outgoingConnection.getConnectionToken(); + } + + public void setConnectionToken(String value) { + this.outgoingConnection.getConnectionToken(); + } public ConnectionInfoSummary getConnectionInfoSummary() { return connectionInfoSummary; diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/swaggerconfig/SwaggerAuthorizationFilter.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/swaggerconfig/SwaggerAuthorizationFilter.java index f4f179cd..8c1b2d4d 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/swaggerconfig/SwaggerAuthorizationFilter.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/swaggerconfig/SwaggerAuthorizationFilter.java @@ -16,7 +16,7 @@ public class SwaggerAuthorizationFilter extends OncePerRequestFilter { - CommonUtil martiUtil; + volatile CommonUtil martiUtil; @Override protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/ContentServlet.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/ContentServlet.java index 8c50da54..16e2b2af 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/ContentServlet.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/ContentServlet.java @@ -3,6 +3,7 @@ package com.bbn.marti.sync; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; import java.sql.SQLException; import java.util.Arrays; @@ -11,12 +12,6 @@ import java.util.logging.Logger; import javax.naming.NamingException; -import jakarta.servlet.AsyncContext; -import jakarta.servlet.ServletException; -import jakarta.servlet.annotation.WebServlet; -import jakarta.servlet.http.HttpServlet; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import org.apache.commons.lang3.StringUtils; import org.owasp.esapi.errors.IntrusionException; @@ -26,10 +21,18 @@ import com.bbn.marti.remote.config.CoreConfigFacade; import com.bbn.marti.remote.exception.NotFoundException; +import com.bbn.marti.remote.exception.TakException; import com.bbn.security.web.SecurityUtils; import com.google.common.base.Strings; +import com.google.common.io.ByteStreams; import io.micrometer.core.instrument.Metrics; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; /** * Servlet for retrieving the content @@ -71,7 +74,9 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx String remoteHost = "unidentified host"; String context = "GET request parameters"; - byte[] content = new byte[0]; + + InputStream contentStream = null; + Metadata match = null; List matches = null; try { @@ -90,10 +95,8 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx } if (hash != null) { - content = enterpriseSyncService.getContentByHash(hash, groupVector); - if (logger.isDebugEnabled()) { - logger.debug("content by hash size: " + (content != null ? content.length : "null")); - } + contentStream = enterpriseSyncService.getContentStreamByHash(hash, groupVector); + try { matches = enterpriseSyncService.getMetadataByHash(hash, groupVector); } catch (Exception e) { @@ -102,7 +105,7 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx } } } else if (uid != null){ - content = enterpriseSyncService.getContentByUid(uid, groupVector); + contentStream = enterpriseSyncService.getContentStreamByUid(uid, groupVector); matches = enterpriseSyncService.getMetadataByUid(uid, groupVector); } @@ -110,9 +113,9 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx throw new NotFoundException("no metadata results for " + query); } - if (content == null) { + if (contentStream == null) { if (logger.isErrorEnabled()) { - logger.error("found null content for :" + StringUtils.normalizeSpace(query)); + logger.error("found null content stream for :" + StringUtils.normalizeSpace(query)); } response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); return; @@ -163,19 +166,18 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx if (method == HttpMethod.GET) { - // apply offset param if (offset != null && offset > 0) { - // validate the offset - if (content.length < 1 || offset > content.length - 1) { - throw new IllegalArgumentException("invalid offset " + offset + " for file size " + content.length); + try { + contentStream.skip(offset); + } catch (Exception e) { + throw new TakException("error applying offset parameter in request " + offset, e); } if (logger.isInfoEnabled()) { logger.info("applying offset " + offset); } - content = Arrays.copyOfRange(content, offset, content.length); } try (OutputStream outStream = response.getOutputStream()) { @@ -186,7 +188,8 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx return; } - outStream.write(content); + // use guava buffered stream conversion to copy data stream from database to servlet request OutputStream + ByteStreams.copy(contentStream, outStream); } } @@ -209,6 +212,13 @@ private void getResource(AsyncContext async, HttpMethod method) throws ServletEx logger.error("Exception in getResource ", e); response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } finally { + if (contentStream != null) { + try { + contentStream.close(); + } catch (Exception e) { + logger.error("Exception closing content stream", e); + } + } async.complete(); } } @@ -237,6 +247,9 @@ protected void doGet(HttpServletRequest request, HttpServletResponse response) if (logger.isDebugEnabled()) { logger.debug("GET resource"); } + + // Set the timeout for async context for file download (ms) + async.setTimeout(CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().getEnterpriseSyncSizeDownloadTimeoutMillis()); async.start(() -> { try { diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncCacheHelper.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncCacheHelper.java index 3e73a16f..6746885c 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncCacheHelper.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncCacheHelper.java @@ -1,5 +1,7 @@ package com.bbn.marti.sync; +import java.io.InputStream; +import java.io.OutputStream; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; @@ -9,7 +11,6 @@ import javax.sql.DataSource; -import com.bbn.marti.remote.config.CoreConfigFacade; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; @@ -20,7 +21,7 @@ import com.bbn.marti.JDBCQueryAuditLogHelper; import com.bbn.marti.config.Cluster; -import com.bbn.marti.remote.CoreConfig; +import com.bbn.marti.remote.config.CoreConfigFacade; import com.bbn.marti.remote.exception.TakException; import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.base.Strings; @@ -207,6 +208,86 @@ private FileWrapper getFileFromDB(String hash) { return fileWrapper; } + + public FileWrapper getInputStreamFileWrapperFromDB(String hash) { + + if (Strings.isNullOrEmpty(hash)) { + throw new IllegalArgumentException("empty hash"); + } + + FileWrapper fileWrapper = null; + + try (Connection connection = dataSource.getConnection(); PreparedStatement query = queryHelper.prepareStatement( + "SELECT data, groups, length(data) FROM resource r WHERE hash = ? ORDER BY submissionTime limit 1", connection)) { + + query.setString(1, hash.toLowerCase()); + logger.debug("getInputStreamFileWrapperFromDB Executing SQL: {}", query.toString()); + + try (ResultSet queryResults = query.executeQuery()) { + + fileWrapper = new FileWrapper(); + + if (queryResults.next()) { + + InputStream contentStream = queryResults.getBinaryStream(1); + long contentLen = queryResults.getLong(3); + + logger.debug("content length {}, stream {}, ", contentLen, contentStream); + + fileWrapper.setInputStream(queryResults.getBinaryStream(1)); + fileWrapper.setHash(hash); + fileWrapper.setGroupVector(queryResults.getString(2)); + } else { + logger.info("getContentByHash no results {}", hash); + } + } + + } catch (Exception e) { + String msg = "exception executing getInputStreamFileWrapperFromDB query " + e.getMessage(); + logger.error(msg, e); + throw new TakException(msg, e); + } + + return fileWrapper; + + } + + public FileWrapper getInputStreamFileWrapperFromDBbyUid(String uid) { + + if (Strings.isNullOrEmpty(uid)) { + throw new IllegalArgumentException("empty uid"); + } + + FileWrapper fileWrapper = null; + + try (Connection connection = dataSource.getConnection(); PreparedStatement query = queryHelper.prepareStatement( + "SELECT data, groups FROM resource r WHERE uid = ? ORDER BY submissionTime desc limit 1", connection)) { + + query.setString(1, uid.toLowerCase()); + logger.debug("getInputStreamFileWrapperFromDBbyUid Executing SQL: {}", query.toString()); + + try (ResultSet queryResults = query.executeQuery()) { + + fileWrapper = new FileWrapper(); + + if (queryResults.next()) { + fileWrapper.setInputStream(queryResults.getBinaryStream(1)); + fileWrapper.setUid(uid); + fileWrapper.setGroupVector(queryResults.getString(2)); + } else { + logger.info("getInputStreamFileWrapperFromDBbyUid no results {}", uid); + } + } + + } catch (Exception e) { + String msg = "exception executing getInputStreamFileWrapperFromDB query " + e.getMessage(); + logger.error(msg, e); + throw new TakException(msg, e); + } + + return fileWrapper; + + } private boolean isCacheEsync() { return esyncEnableCache || (clusterConfig.isEnabled() && clusterConfig.isKubernetes()); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncService.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncService.java index 63652669..f8a32823 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncService.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/EnterpriseSyncService.java @@ -78,6 +78,15 @@ Metadata insertResourceStreamUID(Metadata metadata, InputStream contentStream, S * @throws NamingException */ byte[] getContentByUid(String uid, String groupVector) throws SQLException, NamingException; + + /** + * Gets the content of an Enterprise Sync object. + * @param uid UID of the object to retrieve + * @return the content of the latest stored object matching that UID, or null if no match + * @throws SQLException + * @throws NamingException + */ + InputStream getContentStreamByUid(String uid, String groupVector) throws SQLException, NamingException; /** * Gets the content of an Enterprise Sync object. @@ -106,6 +115,15 @@ Metadata insertResourceStreamUID(Metadata metadata, InputStream contentStream, S * @throws NamingException */ byte[] getContentByHash(String hash, String groupVector) throws SQLException, NamingException; + + /** + * Gets the content of an Enterprise Sync object. + * @param hash Hash of the object to retrieve + * @return InputStream for the latest stored object matching that hash, or null if no match + * @throws SQLException + * @throws NamingException + */ + InputStream getContentStreamByHash(String hash, String groupVector) throws SQLException, NamingException; /** * Gets the content of an Enterprise Sync object. This searches the full resource table vs the latest resource diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/FileWrapper.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/FileWrapper.java index 227d8c79..33998d00 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/FileWrapper.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/FileWrapper.java @@ -1,5 +1,6 @@ package com.bbn.marti.sync; +import java.io.InputStream; import java.io.Serializable; import java.util.Arrays; @@ -12,6 +13,7 @@ public class FileWrapper implements Serializable { String hash; String uid; String groupVector; + InputStream inputStream; public byte[] getContents() { return contents; @@ -37,6 +39,13 @@ public String getGroupVector() { public void setGroupVector(String groupVector) { this.groupVector = groupVector; } + + public InputStream getInputStream() { + return inputStream; + } + public void setInputStream(InputStream inputStream) { + this.inputStream = inputStream; + } @Override public String toString() { return "FileHolder [contents=" + Arrays.toString(contents) + ", hash=" + hash + ", uid=" + uid diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/JDBCEnterpriseSyncService.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/JDBCEnterpriseSyncService.java index 7d8a8991..257eb792 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/JDBCEnterpriseSyncService.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/JDBCEnterpriseSyncService.java @@ -335,7 +335,8 @@ public Metadata insertResource(Metadata metadata, byte[] content, String groupVe if (CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().isEsyncEnableCotFilter()) { try { - if (Arrays.asList(metadata.getKeywords()).contains("missionpackage")) { + if (metadata.getKeywords() != null && + Arrays.asList(metadata.getKeywords()).contains("missionpackage")) { String cotFilter = CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().getEsyncCotFilter(); if (!Strings.isNullOrEmpty(cotFilter)) { content = DataPackageFileBlocker.blockCoT(metadata, content, cotFilter); @@ -1334,8 +1335,6 @@ public byte[] getContentByHash(String hash, String groupVector) throws SQLExcept FileWrapper file = enterpriseSyncCacheHelper.getFileByHash(hash); - logger.debug("get file {} {} ", hash, (file == null || file.getContents() == null) ? "not found" : (file.getContents().length + " bytes")); - // not found if (file == null || file.getContents() == null) { return null; @@ -1657,4 +1656,72 @@ private boolean isCacheEsync() { return esyncEnableCache || (clusterConfig.isEnabled() && clusterConfig.isKubernetes()); } + /** + * Gets the content of an Enterprise Sync object. + * @param hash Hash of the object to retrieve + * @return the content of the latest stored object matching that hash, or null if no match + * @throws SQLException + * @throws NamingException + */ + @Override + public InputStream getContentStreamByHash(String hash, String groupVector) throws SQLException, NamingException { + + if (Strings.isNullOrEmpty(groupVector)) { + throw new IllegalArgumentException("empty group vector"); + } + + FileWrapper file = enterpriseSyncCacheHelper.getInputStreamFileWrapperFromDB(hash); + + // not found + if (file == null || file.getInputStream() == null) { + logger.info("file for hash " + hash + " not found."); + return null; + } + + if (Strings.isNullOrEmpty(file.getGroupVector())) { + throw new IllegalArgumentException("empty group vector in file for hash " + hash); + } + + if (!remoteUtil.isGroupVectorAllowed(groupVector, file.getGroupVector())) { + // not allowed + return null; + } + + // found and allowed + return file.getInputStream(); + } + + /** + * Gets the content of an Enterprise Sync object. + * @param hash Hash of the object to retrieve + * @return the content of the latest stored object matching that hash, or null if no match + * @throws SQLException + * @throws NamingException + */ + @Override + public InputStream getContentStreamByUid(String uid, String groupVector) throws SQLException, NamingException { + + if (Strings.isNullOrEmpty(groupVector)) { + throw new IllegalArgumentException("empty group vector"); + } + + FileWrapper file = enterpriseSyncCacheHelper.getInputStreamFileWrapperFromDBbyUid(uid); + + // not found + if (file == null || file.getInputStream() == null) { + return null; + } + + if (Strings.isNullOrEmpty(file.getGroupVector())) { + throw new IllegalArgumentException("empty group vector in file for uid " + uid); + } + + if (!remoteUtil.isGroupVectorAllowed(groupVector, file.getGroupVector())) { + // not allowed + return null; + } + + // found and allowed + return file.getInputStream(); + } } diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/MissionApi.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/MissionApi.java index 066fa2b1..c20cdc8b 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/MissionApi.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/MissionApi.java @@ -46,6 +46,7 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.crypto.bcrypt.BCrypt; @@ -367,6 +368,8 @@ Callable>> getMissionByGuid( UUID missionGuid = parseGuid(guid); Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + logger.debug("mission from getMissionByGuid {} {}", missionGuid, mission); try { if (!Strings.isNullOrEmpty(password)) { @@ -404,10 +407,8 @@ Callable>> getMissionByGuid( Set result = new HashSet<>(); result.add(mission); - if (logger.isDebugEnabled()) { - logger.debug("GET mission: " + result); - } - + logger.debug("GET mission: {}", mission); + return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), result); } catch (Exception e) { @@ -520,6 +521,7 @@ public Callable>> createMission(@PathVariable("name") @ expirationParam, inviteOnlyParam, false, + false, requestBody, request, sessionId, @@ -593,6 +595,7 @@ public Callable>> createMissionAllowDupe(@PathVariable( expirationParam, inviteOnlyParam, allowDupe, + true, requestBody, request, sessionId, @@ -617,6 +620,7 @@ private ApiResponse> doCreateMissionAllowDupe(String nameParam, Long expirationParam, Boolean inviteOnlyParam, boolean allowDupe, + boolean allowDupeInDifferentGroups, byte[] requestBody, final HttpServletRequest request, final String sessionId, @@ -688,6 +692,7 @@ private ApiResponse> doCreateMissionAllowDupe(String nameParam, // if the user is trying to add a mission to anon, apply the user's groups instead of failing if (groupNames.length == 1 && groupNames[0].equals("__ANON__")) { groupVectorMission = groupVectorUser; + bitVectorMission = bitVectorUser; } else { throw new ForbiddenException("Illegal attempt to set groupVector for Mission!"); } @@ -722,28 +727,28 @@ private ApiResponse> doCreateMissionAllowDupe(String nameParam, String name = missionService.trimName(nameParam); - Mission mission; + Mission mission = null; try { + // get mission try { - // uses MissionCacheHelper to get the mission - // then calls validate mission - // throws NotFoundException or MissionDeletedException if the mission doesn't exist - mission = missionService.getMission(name, groupVectorUser); + mission = missionService.getMissionByName(name, true); + missionService.validateMission(mission, name); // checks if the mission was deleted, or null } catch (Exception e) { if (logger.isDebugEnabled()) { logger.debug("exception getting mission", e); // make sure this gets logged, then rethrow } + throw e; } - - if (mission == null) { - if (logger.isDebugEnabled()) { - logger.debug("mission {} not found in database or cache.", name); - } + + // If the mission exists, but the user is not allowed to access it, don't use or try to update the existing mission. Instead, allow duplicate mission creation. + if (!remoteUtil.isGroupVectorAllowed(groupVectorUser, mission.getGroupVector())) { + allowDupe = true; + mission = null; } - if (allowDupe) { + if (allowDupe && allowDupeInDifferentGroups) { logger.info("creating duplicate mission {} ", name); @@ -922,7 +927,7 @@ private ApiResponse> doCreateMissionAllowDupe(String nameParam, return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), Sets.newHashSet(mission)); - } catch (Exception e) { + } catch (Exception e) { // log and rethrow logger.error("exception in createMission", e); throw e; } @@ -952,7 +957,7 @@ private Mission doInternalCreateMission( // result Mission mission = null; - logger.info("Create mission {} (does not exist)", name); + logger.info("Creating new mission {}", name); String passwordHash = null; if (!Strings.isNullOrEmpty(password)) { @@ -1333,7 +1338,7 @@ ApiResponse> deleteMissionByGuid( try { // This validates the input string (UUID) - UUID.fromString(guidString); + guid = UUID.fromString(guidString); } catch (IllegalArgumentException e) { // rethrow with additional context throw new IllegalArgumentException("Invalid mission guid in request", e); @@ -1436,9 +1441,8 @@ byte[] getMissionArchive(@PathVariable("name") @NotNull String name, HttpServlet } /* - * Send a mission package to a list of contacts + * Send a mission package to a list of contacts (by mission name) */ - // TODO: add support for sending mission package by mission guid - edge case @PreAuthorize("hasPermission(#request, 'MISSION_READ')") @RequestMapping(value = "/missions/{name:.+}/send", method = RequestMethod.POST) ApiResponse> sendMissionArchive(@PathVariable("name") @NotNull String missionName, HttpServletRequest request) throws ValidationException, IntrusionException { @@ -1494,12 +1498,69 @@ ApiResponse> sendMissionArchive(@PathVariable("name") @NotNull Stri result.add(mission); return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), result); } + + /* + * Send a mission package to a list of contacts (by mission guid) + */ + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{guid:.+}/send", method = RequestMethod.POST) + ApiResponse> sendMissionArchiveByGuid(@PathVariable("name") @NotNull String missionGuidParam, HttpServletRequest request) throws ValidationException, IntrusionException { + + + UUID missionGuid = parseGuid(missionGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + String[] contactUids = request.getParameterValues("contacts"); + + if (contactUids == null || contactUids.length == 0) { + throw new IllegalArgumentException("empty contacts array"); + } + + // validate the uids before sending on to the subscriptionManager + for (String uid : contactUids) { + validator.getValidInput(context, uid, "MartiSafeString", DEFAULT_PARAMETER_LENGTH, false); + } + validateParameters(new Object() {}.getClass().getEnclosingMethod()); + + String groupVector = martiUtil.getGroupVectorBitString(request); + + byte[] archive = missionService.archiveMission(mission.getGuidAsUUID(), groupVector, request.getServerName()); + + String archiveName = mission.getName() + "_" + mission.getGuid().toString(); // include the guid in the archive zipo name for uniqueness + + String shaHash = missionService.addMissionArchiveToEsync(archiveName, archive, groupVector, false); + + String requestUrl = request.getRequestURL().toString(); + String url = requestUrl.substring(0, requestUrl.indexOf(request.getServletPath())) + + "/Marti/sync/content?hash=" + shaHash; // yes: uid will be set to the shaHash in the CoT message (see below) + + // Generate the CoT message + String cotMessage = CommonUtil.getFileTransferCotMessage( + /*String uid*/ shaHash, // yes: set the UID to be the hash, this makes it consistent with ATAK-generated mission packages + /*String shaHash*/ shaHash, + /*String callsign*/ SecurityContextHolder.getContext().getAuthentication().getName(), + /*String filename*/ archiveName + ".zip", + /*String url*/ url, + /*long sizeInBytes*/ archive.length, + /*String[] contacts*/ contactUids); + + try { + submission.submitCot(cotMessage, martiUtil.getGroupsFromRequest(request)); + } catch (Exception e) { + throw new TakException(e); + } + + Set result = new HashSet<>(); + result.add(mission); + return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), result); + } /* - * add single or multiple mission content, by hash or UID + * add single or multiple mission content (hash or UID) - referencing mission by name. * */ - // TODO: add guid API for this @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{name:.+}/contents", method = RequestMethod.PUT) public Callable>> addMissionContent(@PathVariable("name") String name, @@ -1536,11 +1597,9 @@ public Callable>> addMissionContent(@PathVariable("name } /* - * add single or multiple mission content, by hash or UID + * add single or multiple mission content (hash or UID) - referencing mission by guid. * */ - - @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/guid/{guid:.+}/contents", method = RequestMethod.PUT) public Callable>> addMissionContentByGuid(@PathVariable("guid") String guid, @@ -1566,9 +1625,6 @@ public Callable>> addMissionContentByGuid(@PathVariable Mission mission = missionService.addMissionContent(missionGuid, content, creatorUid, groupVector); - // TODO can remove? -// MissionUtils.findAndSetTransientValuesForMission(mission); - Set result = new HashSet<>(); result.add(mission); @@ -1613,7 +1669,6 @@ public ApiResponse> addMissionPackage(@PathVariable("name") * remove mission content by hash or uid */ - // TODO: add API case with guid param @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{name:.+}/contents", method = RequestMethod.DELETE) ApiResponse> removeMissionContent(@PathVariable("name") String name, @@ -1644,6 +1699,40 @@ ApiResponse> removeMissionContent(@PathVariable("name") String name // return mission object without resource and uid list (since the query could be expensive) return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), Sets.newHashSet(updatedMission)); } + + @RequestMapping(value = "/missions/guid/{guid:.+}/contents", method = RequestMethod.DELETE) + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + ApiResponse> removeMissionContentByGuid(@PathVariable("guid") String guid, + @RequestParam(value = "hash", required = false) @ValidatedBy("MartiSafeString") String hash, + @RequestParam(value = "uid", required = false) @ValidatedBy("MartiSafeString") String uid, + @RequestParam(value = "creatorUid", defaultValue = "") @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) throws ValidationException, IntrusionException { + + // validate this differently since it's a path variable + validator.getValidInput(context, guid, "MartiSafeString", DEFAULT_PARAMETER_LENGTH, false); + + validateParameters(new Object() {}.getClass().getEnclosingMethod()); + + String groupVector = null; + + try { + // Get group vector for the user associated with this session + groupVector = martiUtil.getGroupBitVector(request); + } catch (Exception e) { + logger.debug("exception getting group membership for user request", e); + } + + UUID guidUuid = parseGuid(guid); + + Mission mission = missionService.getMissionByGuidCheckGroups(guidUuid, groupVector); + + // remove the content and track change + Mission updatedMission = missionService.deleteMissionContent(mission.getGuidAsUUID(), hash, uid, creatorUid, martiUtil.getGroupVectorBitString(request)); + + // return mission object without resource and uid list (since the query could be expensive) + return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), Sets.newHashSet(updatedMission)); + } + /* * get the content change set for a missions given a time period @@ -2233,6 +2322,34 @@ public ApiResponse getSubscriptionForUser( return new ApiResponse( Constants.API_VERSION, MissionSubscription.class.getName(), missionSubscription); } + + /* + * Returns the mission subscription for the current user by guid + */ + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/subscription", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ApiResponse getSubscriptionForUserByGuid( + @PathVariable("missionGuid") String missionGuidParam, + @RequestParam(value = "uid", defaultValue = "") String uid) + { + + UUID missionGuid = parseGuid(missionGuidParam); + + // validate existence of mission + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + MissionSubscription missionSubscription = missionSubscriptionRepository + .findByMissionGuidAndClientUidAndUsernameNoMission(missionGuid.toString(), uid, username); + + if (missionSubscription == null) { + throw new NotFoundException("Mission subscription not found"); + } + + return new ApiResponse( + Constants.API_VERSION, MissionSubscription.class.getName(), missionSubscription); + } /* * subscribe to mission changes @@ -2355,6 +2472,127 @@ public Callable> createMissionSubscription( Constants.API_VERSION, MissionSubscription.class.getName(), missionSubscription); }; } + + /* + * subscribe to mission changes by guid + */ + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/subscription", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.CREATED) + public Callable> createMissionSubscriptionByGuid( + @RequestParam(value = "uid", defaultValue = "") String uid, + @RequestParam(value = "topic", defaultValue = "") String topic, + @RequestParam(value = "password", defaultValue = "") String password, + @RequestParam(value = "secago", required = false) Long secago, + @RequestParam(value = "start", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date start, + @RequestParam(value = "end", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) Date end, + @PathVariable("missionGuid") String missionGuidParam) { + + final HttpServletRequest request = requestHolderBean.getRequest(); + + final String sessionId = requestHolderBean.sessionId(); + + final String groupVector = martiUtil.getGroupVectorBitString(sessionId); + + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + + final UUID missionGuid = parseGuid(missionGuidParam); + + return () -> { + + Mission mission; + if (missionService.getApiVersionNumberFromRequest(request) >= 4) { + mission = missionService.getMissionByGuid(missionGuid, groupVector); + } else { + mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + missionService.validateMissionByGuid(mission); + } + + MissionRole subRole = missionService.getRoleFromToken(mission, + new MissionTokenUtils.TokenType[]{ + MissionTokenUtils.TokenType.INVITATION, + MissionTokenUtils.TokenType.SUBSCRIPTION, + MissionTokenUtils.TokenType.ACCESS + }, request); + + // + // for password protected missions, the caller must provide a password or a token invite + // + if (mission.isPasswordProtected()) { + if (!Strings.isNullOrEmpty(password)) { + if (!BCrypt.checkpw(password, mission.getPasswordHash())) { + throw new ForbiddenException("Illegal attempt to subscribe to mission! Password did not match."); + } + } else if (subRole == null) { + throw new ForbiddenException("Illegal attempt to subscribe to mission! No token role provided."); + } + } else if (!Strings.isNullOrEmpty(password)) { + throw new ForbiddenException("Illegal attempt to subscribe to mission! No password provided."); + } else if (mission.isInviteOnly() && subRole == null) { + // find a username invitation for the current user + subRole = missionService.getRoleFromTypeAndInvitee( + mission.getGuidAsUUID(), MissionInvitation.Type.userName.name(), username); + if (subRole == null) { + throw new ForbiddenException("Illegal attempt to subscribe to invite only mission!"); + } + } + + MissionRole role = null; + if (subRole != null) { + role = subRole; + } else { + role = missionService.getDefaultRole(mission); + } + + if (Strings.isNullOrEmpty(uid) && Strings.isNullOrEmpty(topic)) { + throw new IllegalArgumentException("either 'uid' or 'topic' parameter must be specified"); + } + + MissionSubscription missionSubscription = missionService.missionSubscribe(mission.getGuidAsUUID(), mission.getId(), + Strings.isNullOrEmpty(topic) ? uid : "topic:" + topic, username, role, groupVector); + + if (mission.getFeeds() != null) { + for (MissionFeed missionFeed : mission.getFeeds()) { + dataFeedCotService.sendLatestFeedEvents(mission, missionFeed, Collections.singletonList(uid), groupVector); + } + } + + try { + // clear out any uid invitations now that the uid has subscribed + missionService.missionUninvite(mission.getGuidAsUUID(), + uid, MissionInvitation.Type.clientUid, uid, groupVector); + + // clear out any callsign invitations for the devices current callsign + RemoteSubscription subscription = subscriptionManagerProxy.getSubscriptionManagerForClientUid(uid).getRemoteSubscriptionByClientUid(uid); + if (subscription != null && !Strings.isNullOrEmpty(subscription.callsign)) { + missionService.missionUninvite(mission.getGuidAsUUID(), subscription.callsign, + MissionInvitation.Type.callsign, uid, groupVector); + } + } + catch (Exception e) { + throw new TakException(e); + } + + // return changes and logs with API 3+ + if (missionService.getApiVersionNumberFromRequest(request) >= 3) { + + missionSubscription.setMission(mission); + + Set changes = missionService.getMissionChanges( + mission.getName(), groupVector, secago, start, end, false); + missionSubscription.getMission().setMissionChanges(changes); + + List logs = missionService.getLogEntriesForMission(mission, secago, start, end); + missionSubscription.getMission().setLogs(logs); + + } else { + // mission is not returned < API 3 + missionSubscription.setMission(null); + } + + return new ApiResponse( + Constants.API_VERSION, MissionSubscription.class.getName(), missionSubscription); + }; + } @PreAuthorize("hasPermission(#request, 'MISSION_SET_ROLE')") @RequestMapping(value = "/missions/{missionName:.+}/subscription", method = RequestMethod.POST) @@ -2389,23 +2627,57 @@ public void setSubscriptionRole( boolean success = missionService.inviteOrUpdate(mission, subscriptions, creatorUid, groupVector); response.setStatus(success ? HttpServletResponse.SC_OK : HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } + + @PreAuthorize("hasPermission(#request, 'MISSION_SET_ROLE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/subscription", method = RequestMethod.POST) + public void setSubscriptionRoleByGuid( + @RequestBody List subscriptions, + @PathVariable("missionGuid") String missionGuidParam, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) { - /* - * unsubscribe to mission changes - * - */ - @PreAuthorize("hasPermission(#request, 'MISSION_READ')") // use MISSION_READ here to allow readonly users to unsubscribe - @RequestMapping(value = "/missions/{missionName:.+}/subscription", method = RequestMethod.DELETE) - @ResponseStatus(HttpStatus.OK) - public Callable deleteMissionSubscription( - @RequestParam(value = "uid", defaultValue = "") String uid, - @RequestParam(value = "topic", defaultValue = "") String topic, - @RequestParam(value = "disconnectOnly", defaultValue = "true") boolean disconnectOnly, - @PathVariable("missionName") String missionNameParam) { + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); - final String groupVector = martiUtil.getGroupVectorBitString(request); + // validate existence of mission + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + missionService.validateMissionByGuid(mission); - final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + // validate subscription list + for (MissionSubscription subscription : subscriptions) { + if (subscription.getClientUid() == null || subscription.getRole() == null) { + throw new IllegalArgumentException("missing clientUid or role"); + } + + MissionRole role = missionRoleRepository.findFirstByRole(subscription.getRole().getRole()); + + if (!missionService.validateRoleAssignment(mission, request, role)) { + throw new ForbiddenException("validateRoleAssignment failed! Illegal attempt to assign role " + + role.getRole().name()); + } + } + + boolean success = missionService.inviteOrUpdate(mission, subscriptions, creatorUid, groupVector); + response.setStatus(success ? HttpServletResponse.SC_OK : HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + /* + * unsubscribe from mission changes + * + */ + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") // use MISSION_READ here to allow readonly users to unsubscribe + @RequestMapping(value = "/missions/{missionName:.+}/subscription", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public Callable deleteMissionSubscription( + @RequestParam(value = "uid", defaultValue = "") String uid, + @RequestParam(value = "topic", defaultValue = "") String topic, + @RequestParam(value = "disconnectOnly", defaultValue = "true") boolean disconnectOnly, + @PathVariable("missionName") String missionNameParam) { + + final String groupVector = martiUtil.getGroupVectorBitString(request); + + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); return () -> { @@ -2424,6 +2696,41 @@ public Callable deleteMissionSubscription( return null; }; } + + /* + * unsubscribe from mission changes by guid + * + */ + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") // use MISSION_READ here to allow readonly users to unsubscribe + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/subscription", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public Callable deleteMissionSubscriptionByGuid( + @RequestParam(value = "uid", defaultValue = "") String uid, + @RequestParam(value = "topic", defaultValue = "") String topic, + @RequestParam(value = "disconnectOnly", defaultValue = "true") boolean disconnectOnly, + @PathVariable("missionGuid") String missionGuidParam) { + + final String groupVector = martiUtil.getGroupVectorBitString(request); + + final String username = SecurityContextHolder.getContext().getAuthentication().getName(); + + return () -> { + + UUID missionGuid = parseGuid(missionGuidParam); + + // validate existence of mission + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + missionService.validateMissionByGuid(mission); + + if (Strings.isNullOrEmpty(uid) && Strings.isNullOrEmpty(topic)) { + throw new IllegalArgumentException("either 'uid' or 'topic' parameter must be specified"); + } + + missionService.missionUnsubscribe(mission.getGuidAsUUID(), Strings.isNullOrEmpty(topic) ? uid : topic, username, groupVector, disconnectOnly); + + return null; + }; + } /* * Get all mission subscriptions @@ -2435,6 +2742,17 @@ public Callable deleteMissionSubscription( public ApiResponse>> getAllMissionSubscriptions() { return new ApiResponse>>(Constants.API_VERSION, "MissionSubscription", missionService.getAllMissionSubscriptions()); } + + /* + * Get all mission subscriptions with guid + * + */ + @PreAuthorize("hasRole('ROLE_ADMIN')") // restrict to admin + @RequestMapping(value = "/missions/all/subscriptions/guid", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ApiResponse, String>>> getAllMissionSubscriptionsWithGuid() { + return new ApiResponse, String>>>(Constants.API_VERSION, "MissionSubscription", missionService.getAllMissionSubscriptionsWithGuid()); + } /* * Get subscriptions to the specified mission @@ -2455,7 +2773,53 @@ public ApiResponse> getMissionSubscriptions( throw new TakException(e); } } + + /* + * + * Get subscriptions to the specified mission by guid + * + */ + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/subscriptions", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ApiResponse> getMissionSubscriptionsByGuid( + @PathVariable("missionGuid") String missionGuidParam, HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + try { + return new ApiResponse>(Constants.API_VERSION, "MissionSubscription", subscriptionManager.getMissionSubscriptions(mission.getGuidAsUUID(), false)); + } catch (Exception e) { + throw new TakException(e); + } + } + + /* roles by guid */ + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/subscriptions/roles", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ApiResponse> getMissionSubscriptionRolesByGuid( + @PathVariable("missionGuid") String missionGuidParam, HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + // ensure tokens are removed from the output + List missionSubscriptions = missionService.getMissionSubscriptionsByMissionGuidNoMissionNoToken(missionGuid); + for (MissionSubscription missionSubscription : missionSubscriptions) { + missionSubscription.setToken(null); + } + + return new ApiResponse>(Constants.API_VERSION, "MissionSubscription", + missionSubscriptions); + } + + /* roles by name */ @PreAuthorize("hasPermission(#request, 'MISSION_READ')") @RequestMapping(value = "/missions/{missionName:.+}/subscriptions/roles", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) @@ -2491,7 +2855,62 @@ public ApiResponse getMissionRoleFromToken( return new ApiResponse( Constants.API_VERSION, MissionRole.class.getName(), role); } + + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/role", method = RequestMethod.GET) + public ApiResponse getMissionRoleFromTokenByGuid( + @PathVariable("missionGuid") String missionGuidParam, HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + // validate existence of mission + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + MissionRole role = (MissionRole)request.getAttribute(MissionRole.class.getName()); + + return new ApiResponse( + Constants.API_VERSION, MissionRole.class.getName(), role); + } + + @PreAuthorize("hasPermission(#request, 'MISSION_SET_ROLE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/role", method = RequestMethod.PUT) + public void setMissionRoleByGuid( + @PathVariable("missionGuid") String missionGuidParam, + @RequestParam(value = "clientUid", defaultValue = "") @ValidatedBy("MartiSafeString") String clientUid, + @RequestParam(value = "username", defaultValue = "") @ValidatedBy("MartiSafeString") String username, + @RequestParam(value = "role", required = false) @ValidatedBy("MartiSafeString") MissionRole.Role newRole, + HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + // validate existence of mission + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + if (mission.getDefaultRole() == null) { + logger.error("illegal attempt to set role on non role-enabled mission!"); + throw new IllegalArgumentException(); + } + + boolean result = true; + + MissionRole role = null; + + if (newRole != null) { + role = missionRoleRepository.findFirstByRole(newRole); + + if (!missionService.validateRoleAssignment(mission, request, role)) { + throw new ForbiddenException( + "validateRoleAssignment failed! Illegal attempt to assign role " + newRole.name()); + } + } + + result = missionService.setRole(mission, clientUid, username, role, martiUtil.getGroupVectorBitString(request)); + response.setStatus(result ? HttpServletResponse.SC_OK : HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + @PreAuthorize("hasPermission(#request, 'MISSION_SET_ROLE')") @RequestMapping(value = "/missions/{missionName:.+}/role", method = RequestMethod.PUT) public void setMissionRole( @@ -2505,6 +2924,9 @@ public void setMissionRole( // validate existence of mission Mission mission = missionService.getMissionByNameCheckGroups(missionName, martiUtil.getGroupVectorBitString(request)); + + logger.debug("mission in setMissionRole {} default role {}", mission, mission.getDefaultRole()); + missionService.validateMission(mission, missionName); if (mission.getDefaultRole() == null) { @@ -2576,6 +2998,26 @@ public ApiResponse> getMissionInvitations( return new ApiResponse>(Constants.API_VERSION, "MissionInvitation", missionInvitations); } + + /* + * Get invitations to the specified mission by guid + * + */ + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/invitations", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ApiResponse> getMissionInvitationsByGuid( + @PathVariable("missionGuid") String missionGuidParam, HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + List missionInvitations = missionService.getMissionInvitationsByGuid(mission.getGuidAsUUID()); + + return new ApiResponse>(Constants.API_VERSION, "MissionInvitation", missionInvitations); + } @RequestMapping(value = "/missions/{name:.+}/invite/{type:.+}/{invitee:.+}", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) @@ -2639,7 +3081,126 @@ public void inviteToMission( missionService.missionInvite(mission.getGuidAsUUID(), invitee, type, inviteRole, creatorUid, groupVector); } + + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/invite/{type:.+}/{invitee:.+}", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + public void inviteToMissionByGuid( + @PathVariable("missionGuid") @NotNull String missionGuidParam, + @PathVariable("type") @NotNull MissionInvitation.Type type, + @PathVariable("invitee") @NotNull String invitee, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid, + @RequestParam(value = "role", defaultValue = "") @ValidatedBy("MartiSafeString") MissionRole.Role role, + HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + missionService.validateMissionByGuid(mission); + + CoreConfig config = CoreConfigFacade.getInstance(); + + // If VBM is enabled, only let the mission owner or admin invite users to a COP mission + if (config.getRemoteConfiguration().getVbm() != null && + config.getRemoteConfiguration().getVbm().isEnabled() && + config.getRemoteConfiguration().getNetwork().getMissionCopTool().equals(mission.getTool())) { + + if (logger.isDebugEnabled()) { + logger.debug("Mission Invite 2: VBM is enabled"); + } + + MissionRole roleForRequest = missionService.getRoleForRequest(mission, request); + if (roleForRequest == null) { + throw new IllegalArgumentException("no role for request!"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Mission Invite 2: Role for request: {}", roleForRequest.getRole().toString()); + } + + if (!roleForRequest.getRole().equals(MissionRole.Role.MISSION_OWNER)) { + String msg = "Only mission owner or admin can send an invite"; + logger.error(msg); + throw new ForbiddenException(msg); + } + } + + MissionRole inviteRole = null; + if (role != null) { + inviteRole = missionRoleRepository.findFirstByRole(role); + } else { + inviteRole = missionService.getDefaultRole(mission); + } + + if (!missionService.validateRoleAssignment(mission, request, inviteRole)) { + throw new ForbiddenException("validateRoleAssignment failed! Illegal attempt to assign role " + + inviteRole.getRole().name()); + } + + missionService.missionInvite(mission.getGuidAsUUID(), invitee, type, inviteRole, creatorUid, groupVector); + } + + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/invite/{type:.+}/{invitee:.+}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + @Transactional + public void uninviteFromMissionByGuid( + @PathVariable("missionGuid") @NotNull String missionGuidParam, + @PathVariable("type") @NotNull MissionInvitation.Type type, + @PathVariable("invitee") @NotNull String invitee, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) { + + try { + UUID missionGuid = parseGuid(missionGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + CoreConfig config = CoreConfigFacade.getInstance(); + + // If VBM is enabled, only let the mission owner or admin invite users to a COP mission + if (config.getRemoteConfiguration().getVbm() != null && + config.getRemoteConfiguration().getVbm().isEnabled() && + config.getRemoteConfiguration().getNetwork().getMissionCopTool().equals(mission.getTool())) { + + if (logger.isDebugEnabled()) { + logger.debug("Mission Invite 2: VBM is enabled"); + } + + MissionRole roleForRequest = missionService.getRoleForRequest(mission, request); + if (roleForRequest == null) { + throw new IllegalArgumentException("no role for request!"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Mission Invite 2: Role for request: {}", roleForRequest.getRole().toString()); + } + + if (!roleForRequest.getRole().equals(MissionRole.Role.MISSION_OWNER)) { + String msg = "Only mission owner or admin can send an invite"; + logger.error(msg); + throw new ForbiddenException(msg); + } + } + + MissionRole role = missionService.getRoleFromToken(mission, new MissionTokenUtils.TokenType[]{ + MissionTokenUtils.TokenType.INVITATION, + MissionTokenUtils.TokenType.SUBSCRIPTION + }, request); + + if (mission.isPasswordProtected() && role == null) { + throw new ForbiddenException("Illegal attempt to delete invitation"); + } + + missionService.missionUninvite( + mission.getGuidAsUUID(), invitee, type, creatorUid, martiUtil.getGroupVectorBitString(request)); + } catch (Exception e) { + logger.error("exception in uninviteFromMission!", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + } + @RequestMapping(value = "/missions/{name:.+}/invite/{type:.+}/{invitee:.+}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.OK) @Transactional @@ -2703,7 +3264,7 @@ public void uninviteFromMission( response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); } } - + // Mission Log /* * Create a new log entry (autogenerating the entry id) @@ -2949,11 +3510,54 @@ ResponseEntity getLatestMissionCotEvents(@PathVariable("name") @NotNull return new ResponseEntity(cot, headers, HttpStatus.OK); } + + // Get all latest CoT events for a mission by guid + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/cot", method = RequestMethod.GET) + ResponseEntity getLatestMissionCotEventsByGuid(@PathVariable("missionGuid") @NotNull String missionGuidParam, HttpServletRequest request) { + + final String sessionId = requestHolderBean.sessionId(); + + if (logger.isDebugEnabled()) { + logger.debug("session id: " + requestHolderBean.sessionId()); + } + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(sessionId); + + // will throw appropriate exeception if mission does not exist or has been deleted + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + missionService.validateMissionByGuid(mission); + + String cot = missionService.getCachedCot(missionGuid, mission.getUids(), groupVector); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(org.springframework.http.MediaType.APPLICATION_XML); + + return new ResponseEntity(cot, headers, HttpStatus.OK); + } + + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/contacts", method = RequestMethod.GET) + @ResponseStatus(HttpStatus.OK) + public ResponseEntity> getMissionContactsByGuid( + @PathVariable("missionGuid") @NotNull String missionGuidParam, HttpServletRequest request) throws RemoteException { + + UUID missionGuid = parseGuid(missionGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + List results = subscriptionManager.getSubscriptionsWithGroupAccess(mission.getGroupVector(), false); + return new ResponseEntity>(results, new HttpHeaders(), HttpStatus.OK); + } + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") @RequestMapping(value = "/missions/{name:.+}/contacts", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) - public ResponseEntity> results( + public ResponseEntity> getMissionContacts( @PathVariable("name") @NotNull String missionName, HttpServletRequest request) throws RemoteException { if (Strings.isNullOrEmpty(missionName)) { @@ -3094,8 +3698,129 @@ public void sendMissionInvites( response.setStatus(HttpServletResponse.SC_OK); } + + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/invite", method = RequestMethod.POST) + public void sendMissionInvitesByGuid( + @PathVariable("missionGuid") @NotNull String missionGuidParam, + @RequestParam(value = "creatorUid", required = false) @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request + ) throws RemoteException, ValidationException, IOException { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + missionService.validateMissionByGuid(mission); + + CoreConfig config = CoreConfigFacade.getInstance(); + + // If VBM is enabled, only let the mission owner or admin invite users to a COP mission + if (config.getRemoteConfiguration().getVbm() != null && + config.getRemoteConfiguration().getVbm().isEnabled() && + config.getRemoteConfiguration().getNetwork().getMissionCopTool().equals(mission.getTool())) { + + if (logger.isDebugEnabled()) { + logger.debug("Mission Invite: VBM is enabled"); + } + + MissionRole roleForRequest = missionService.getRoleForRequest(mission, request); + if (roleForRequest == null) { + throw new IllegalArgumentException("no role for request!"); + } + + if (logger.isDebugEnabled()) { + logger.debug("Mission Invite: Role for request: {}", roleForRequest.getRole().toString()); + } + + if (!roleForRequest.getRole().equals(MissionRole.Role.MISSION_OWNER)) { + String msg = "Only mission owner or admin can send an invite"; + logger.error(msg); + throw new ForbiddenException(msg); + } + } + + if (missionService.getApiVersionNumberFromRequest(request) > 2) { + + String body = new String(IOUtils.toByteArray(request.getInputStream())); + + ObjectMapper mapper = new ObjectMapper(); + List missionInvitations = + mapper.readValue(body, new TypeReference>(){}); + + for (MissionInvitation missionInvitation : missionInvitations) { + + if (missionInvitation.getType() == null || missionInvitation.getInvitee() == null) { + throw new IllegalArgumentException("invitation found without type or invitee attribute!"); + } + + missionInvitation.setMissionName(mission.getName()); + missionInvitation.setMissionGuid(mission.getGuidAsUUID()); + missionInvitation.setCreatorUid(creatorUid); + missionInvitation.setCreateTime(new Date()); + + MissionRole role = missionInvitation.getRole(); + if (role == null) { + role = missionService.getDefaultRole(mission); + } else { + role = missionRoleRepository.findFirstByRole(role.getRole()); + } + + if (!missionService.validateRoleAssignment(mission, request, role)) { + throw new ForbiddenException("validateRoleAssignment failed! Illegal attempt to assign role " + + role.getRole().name()); + } + + String token = missionService.generateToken( + UUID.randomUUID().toString(), mission.getGuidAsUUID(), mission.getName(), MissionTokenUtils.TokenType.INVITATION, -1); + missionInvitation.setToken(token); + + missionInvitation.setRole(role); + + missionService.missionInvite(mission, missionInvitation); + } + + } else { + + String[] contactUids = request.getParameterValues("contacts"); + + if (contactUids == null || contactUids.length == 0) { + throw new IllegalArgumentException("empty contacts array"); + } + + // validate the uids before sending on to the subscriptionManager + for (String uid : contactUids) { + validator.getValidInput(context, uid, "MartiSafeString", DEFAULT_PARAMETER_LENGTH, false); + } + validateParameters(new Object() { + }.getClass().getEnclosingMethod()); + + User user = groupManager.getUserByConnectionId(RequestContextHolder.currentRequestAttributes().getSessionId()); + + String author = Strings.isNullOrEmpty(creatorUid) ? user.getName() : creatorUid; + + MissionRole inviteRole = missionService.getDefaultRole(mission); + + if (!missionService.validateRoleAssignment(mission, request, inviteRole)) { + throw new ForbiddenException("validateRoleAssignment failed! Illegal attempt to assign role " + + inviteRole.getRole().name()); + } + + for (String uid : contactUids) { + try { + missionService.missionInvite( + mission.getGuidAsUUID(), uid, MissionInvitation.Type.clientUid, inviteRole, author, groupVector); + } catch (Exception e) { + logger.debug("Attempt to re-invite clientUid: " + uid + " to: " + mission.getName() + " " + mission.getGuid()); + continue; + } + } + } + + response.setStatus(HttpServletResponse.SC_OK); + + } - // TODO: add APIs to do this by guid @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{childName:.+}/parent/{parentName:.+}", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) @@ -3115,6 +3840,25 @@ public void setParent(@PathVariable("childName") @NotNull String childName, missionService.setParent(childMission.getGuidAsUUID(), parentMission.getGuidAsUUID(), martiUtil.getGroupVectorBitString(request)); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{childGuid:.+}/parent/guid/{parentGuid:.+}", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + public void setParentByGuid(@PathVariable("childGuid") @NotNull String childGuidParam, + @PathVariable("parentGuid") @NotNull String parentGuidParam, + HttpServletRequest request) { + + UUID childGuid = parseGuid(childGuidParam); + UUID parentGuid = parseGuid(parentGuidParam); + + Mission childMission = missionService.getMissionByGuidCheckGroups(childGuid, martiUtil.getGroupVectorBitString(request)); + Mission parentMission = missionService.getMissionByGuidCheckGroups(parentGuid, martiUtil.getGroupVectorBitString(request)); + + missionService.validateMissionByGuid(childMission); + missionService.validateMissionByGuid(parentMission); + + missionService.setParent(childMission.getGuidAsUUID(), parentMission.getGuidAsUUID(), martiUtil.getGroupVectorBitString(request)); + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{childName:.+}/parent", method = RequestMethod.DELETE) @@ -3128,8 +3872,20 @@ public void clearParent(@PathVariable("childName") @NotNull String childName, Ht missionService.clearParent(mission.getGuidAsUUID(), martiUtil.getGroupVectorBitString(request)); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{childGuid:.+}/parent", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void clearParentByGuid(@PathVariable("childGuid") @NotNull String childGuidParam, HttpServletRequest request) { + + UUID childGuid = parseGuid(childGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(childGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + missionService.clearParent(mission.getGuidAsUUID(), martiUtil.getGroupVectorBitString(request)); + } - // TODO: add API here for guid case @PreAuthorize("hasPermission(#request, 'MISSION_READ')") @RequestMapping(value = "/missions/{name:.+}/children", method = RequestMethod.GET) ApiResponse> getChildren(@PathVariable("name") @NotNull String parentName, HttpServletRequest request) { @@ -3144,6 +3900,21 @@ ApiResponse> getChildren(@PathVariable("name") @NotNull String pare return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), children); } + + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{guid:.+}/children", method = RequestMethod.GET) + ApiResponse> getChildrenByGuid(@PathVariable("guid") @NotNull String parentGuidParam, HttpServletRequest request) { + String groupVector = martiUtil.getGroupVectorBitString(request); + + UUID parentGuid = parseGuid(parentGuidParam); + + Mission mission = missionService.getMissionByGuidCheckGroups(parentGuid, martiUtil.getGroupVectorBitString(request)); + missionService.validateMissionByGuid(mission); + + Set children = missionService.getChildren(mission.getGuidAsUUID(), groupVector); + + return new ApiResponse>(Constants.API_VERSION, Mission.class.getSimpleName(), children); + } @PreAuthorize("hasPermission(#request, 'MISSION_READ')") @RequestMapping(value = "/missions/{name:.+}/parent", method = RequestMethod.GET) @@ -3188,8 +3959,38 @@ ResponseEntity getKml( return new ResponseEntity( missionService.getMissionKml(missionName, urlBase, groupVector), headers, HttpStatus.OK); } + + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/kml", method = RequestMethod.GET) + ResponseEntity getKmlByGuid( + @PathVariable("missionGuid") @NotNull String missionGuidParam, + @RequestParam(value = "download", defaultValue = "false") boolean download, + HttpServletRequest request) { + String groupVector = martiUtil.getGroupVectorBitString(request); + + UUID missionGuid = parseGuid(missionGuidParam); + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(new org.springframework.http.MediaType( + "application", "vnd.google-earth.kml+xml")); + + String urlBase = null; + if (download) { + headers.setContentDispositionFormData("kml", missionGuid + ".kml"); + } else { + String requestUrl = request.getRequestURL().toString(); + try { + requestUrl = URLDecoder.decode(requestUrl, "UTF-8"); + } catch (UnsupportedEncodingException e) { + logger.error("error decoding URL" + requestUrl); + } + urlBase = requestUrl.substring(0, requestUrl.indexOf(request.getServletPath())); + } + + return new ResponseEntity( + missionService.getMissionKml(missionGuid, urlBase, groupVector), headers, HttpStatus.OK); + } - // TODO: add case here for API with guid param @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{name}/externaldata", method = RequestMethod.POST) @ResponseStatus(HttpStatus.CREATED) @@ -3206,37 +4007,96 @@ public ApiResponse setExternalMissionData( Mission mission = missionService.getMissionByNameCheckGroups(missionName, groupVector); - missionService.validateMissionByGuid(mission); + missionService.validateMissionByGuid(mission); + + externalMissionData = missionService.setExternalMissionData(mission.getGuidAsUUID(), creatorUid, externalMissionData, groupVector); + + // save the new log entry and let the database generate the id + return new ApiResponse<>(Constants.API_VERSION, ExternalMissionData.class.getName(), externalMissionData); + } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{guid}/externaldata", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.CREATED) + public ApiResponse setExternalMissionDataByGuid( + @PathVariable(value = "guid") String missionGuidParam, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + @RequestBody @NotNull ExternalMissionData externalMissionData, + HttpServletRequest request) { + if (Strings.isNullOrEmpty(externalMissionData.getId())) { + throw new IllegalArgumentException("ExternalMissionData id must be included"); + } + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + + Mission mission = missionService.getMissionByGuidCheckGroups(missionGuid, groupVector); + + missionService.validateMissionByGuid(mission); + + externalMissionData = missionService.setExternalMissionData(mission.getGuidAsUUID(), creatorUid, externalMissionData, groupVector); + + // save the new log entry and let the database generate the id + return new ApiResponse<>(Constants.API_VERSION, ExternalMissionData.class.getName(), externalMissionData); + } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/{name}/externaldata/{id}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void deleteExternalMissionData( + @PathVariable(value = "name") String missionName, + @PathVariable(value = "id") String externalMissionDataId, + @RequestParam(value = "notes") @ValidatedBy("MartiSafeString") String notes, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) { + String groupVector = martiUtil.getGroupVectorBitString(request); + + Mission mission = missionService.getMission(missionName, groupVector); + + missionService.deleteExternalMissionData(mission.getGuidAsUUID(), externalMissionDataId, notes, creatorUid, groupVector); + } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{guid}/externaldata/{id}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void deleteExternalMissionDataByGuid( + @PathVariable(value = "guid") String missionGuidParam, + @PathVariable(value = "id") String externalMissionDataId, + @RequestParam(value = "notes") @ValidatedBy("MartiSafeString") String notes, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) { + String groupVector = martiUtil.getGroupVectorBitString(request); + + UUID missionGuid = parseGuid(missionGuidParam); - externalMissionData = missionService.setExternalMissionData(mission.getGuidAsUUID(), creatorUid, externalMissionData, groupVector); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); - // save the new log entry and let the database generate the id - return new ApiResponse<>(Constants.API_VERSION, ExternalMissionData.class.getName(), externalMissionData); + missionService.deleteExternalMissionData(mission.getGuidAsUUID(), externalMissionDataId, notes, creatorUid, groupVector); } - // TODO: add guid case to API @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") - @RequestMapping(value = "/missions/{name}/externaldata/{id}", method = RequestMethod.DELETE) + @RequestMapping(value = "/missions/{name}/externaldata/{id}/change", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) - public void deleteExternalMissionData( + public void notifyExternalDataChanged( @PathVariable(value = "name") String missionName, @PathVariable(value = "id") String externalMissionDataId, - @RequestParam(value = "notes") @ValidatedBy("MartiSafeString") String notes, @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + @RequestParam(value = "notes") @ValidatedBy("MartiSafeString") String notes, + @RequestBody String token, HttpServletRequest request) { String groupVector = martiUtil.getGroupVectorBitString(request); Mission mission = missionService.getMission(missionName, groupVector); - missionService.deleteExternalMissionData(mission.getGuidAsUUID(), externalMissionDataId, notes, creatorUid, groupVector); + missionService.notifyExternalMissionDataChanged(mission.getGuidAsUUID(), externalMissionDataId, token, notes, creatorUid, groupVector); } - // TODO: add guid case to API @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") - @RequestMapping(value = "/missions/{name}/externaldata/{id}/change", method = RequestMethod.POST) + @RequestMapping(value = "/missions/guid/{guid}/externaldata/{id}/change", method = RequestMethod.POST) @ResponseStatus(HttpStatus.OK) - public void notifyExternalDataChanged( - @PathVariable(value = "name") String missionName, + public void notifyExternalDataChangedByGuid( + @PathVariable(value = "guid") String missionGuidParam, @PathVariable(value = "id") String externalMissionDataId, @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, @RequestParam(value = "notes") @ValidatedBy("MartiSafeString") String notes, @@ -3244,7 +4104,9 @@ public void notifyExternalDataChanged( HttpServletRequest request) { String groupVector = martiUtil.getGroupVectorBitString(request); - Mission mission = missionService.getMission(missionName, groupVector); + UUID missionGuid = parseGuid(missionGuidParam); + + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); missionService.notifyExternalMissionDataChanged(mission.getGuidAsUUID(), externalMissionDataId, token, notes, creatorUid, groupVector); } @@ -3274,6 +4136,33 @@ public void setPassword( logger.debug("exception announcing mission change " + e.getMessage(), e); } } + + @PreAuthorize("hasPermission(#request, 'MISSION_SET_PASSWORD')") + @RequestMapping(value = "/missions/guid/{guid:.+}/password", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + public void setPasswordByGuid( + @PathVariable(value = "guid") @ValidatedBy("MartiSafeString") String missionGuidParam, + @RequestParam(value = "password", defaultValue = "") @ValidatedBy("MartiSafeString") String password, + @RequestParam(value = "creatorUid", defaultValue = "") @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) { + + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + String newPasswordHash = BCrypt.hashpw(password, BCrypt.gensalt()); + missionRepository.setPasswordHashByGuid(missionGuid.toString(), newPasswordHash, groupVector); + missionService.invalidateMissionCache(missionGuid); + + try { + subscriptionManager.broadcastMissionAnnouncement(UUID.fromString(mission.getGuid()), mission.getName(), mission.getGroupVector(), creatorUid, + SubscriptionManagerLite.ChangeType.METADATA, mission.getTool()); + } catch (Exception e) { + logger.debug("exception announcing mission change " + e.getMessage(), e); + } + } @PreAuthorize("hasPermission(#request, 'MISSION_SET_PASSWORD')") @RequestMapping(value = "/missions/{name:.+}/password", method = RequestMethod.DELETE) @@ -3299,6 +4188,31 @@ public void removePassword( } } + + @PreAuthorize("hasPermission(#request, 'MISSION_SET_PASSWORD')") + @RequestMapping(value = "/missions/guid/{guid:.+}/password", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + public void removePasswordByGuid( + @PathVariable(value = "guid") @ValidatedBy("MartiSafeString") String missionGuidParam, + @RequestParam(value = "creatorUid", defaultValue = "") @ValidatedBy("MartiSafeString") String creatorUid, + HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + missionRepository.setPasswordHashByGuid(missionGuid.toString(), null, groupVector); + missionService.invalidateMissionCache(missionGuid); + + try { + subscriptionManager.broadcastMissionAnnouncement(UUID.fromString(mission.getGuid()), mission.getName(), mission.getGroupVector(), creatorUid, + SubscriptionManagerLite.ChangeType.METADATA, mission.getTool()); + } catch (Exception e) { + logger.debug("exception announcing mission change " + e.getMessage(), e); + } + } + @RequestMapping(value = "/missions/{name}/expiration", method = RequestMethod.PUT) @ResponseStatus(HttpStatus.OK) public void setExpiration( @@ -3322,6 +4236,32 @@ public void setExpiration( logger.debug("exception setting mission expiration " + e.getMessage(), e); } } + + @RequestMapping(value = "/missions/guid/{guid}/expiration", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + public void setExpirationByGuid( + @PathVariable(value = "guid") @ValidatedBy("MartiSafeString") String missionGuidParam, + @RequestParam(value = "expiration", required = false) Long expiration, + HttpServletRequest request) { + + UUID missionGuid = parseGuid(missionGuidParam); + + try { + String groupVector = martiUtil.getGroupVectorBitString(request); + boolean result = missionService.setExpiration(missionGuid, expiration, groupVector); + response.setStatus(result ? HttpServletResponse.SC_OK : HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + try { + if (logger.isDebugEnabled()) { + logger.debug(" setting the mission expiration " + missionGuid.toString() + " time " + expiration); + } + retentionPolicyConfig.setMissionExpiryTask(missionGuid.toString(), expiration); + } catch (Exception e) { + logger.error(" Exception getting Retention service, task not scheduled immediately " + missionGuid.toString()); + } + } catch (Exception e) { + logger.debug("exception setting mission expiration " + e.getMessage(), e); + } + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/feed", method = RequestMethod.POST) @@ -3366,7 +4306,52 @@ public void addFeed( throw new Exception("Server error when adding feed to mission"); } } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/{missionGuid:.+}/feed", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + public void addFeedByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + @RequestParam(value = "dataFeedUid") @ValidatedBy("MartiSafeString") String dataFeedUid, + @RequestParam(value = "filterPolygon", required = false) @ValidatedBy("MartiSafeString") List filterPolygonList, + @RequestParam(value = "filterCotTypes", required = false) String filterCotTypesSerialized, // TODO: @ValidatedBy + @RequestParam(value = "filterCallsign", required = false) @ValidatedBy("MartiSafeString") String filterCallsign, + HttpServletRequest request) throws Exception { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + List filterCotTypes = null; + if (filterCotTypesSerialized != null) { + try { + ObjectMapper mapper = new ObjectMapper(); + filterCotTypes = Arrays.asList(mapper.readValue(filterCotTypesSerialized, String[].class)); + } catch (Exception e) { + logger.error("Error parsing parameter filterCotTypesSerialized from the request", e); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); + throw new Exception("Error parsing parameter filterCotTypesSerialized from the request"); + } + }else { + filterCotTypes = new ArrayList<>(); + } + + String filterPolygon = null; + if (filterPolygonList != null) { + filterPolygon = boundingPolygonPointsToString(filterPolygonList); + } + + try { + missionService.addFeedToMission(mission.getName(), creatorUid, mission, dataFeedUid, filterPolygon, filterCotTypes, filterCallsign); + } catch (Exception e) { + logger.error("exception in addFeed!", e); + response.setStatus(HttpServletResponse.SC_BAD_GATEWAY); + throw new Exception("Server error when adding feed to mission"); + } + } + // TODO add guid case @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/feed/{uid:.+}", method = RequestMethod.DELETE) @ResponseStatus(HttpStatus.OK) @@ -3409,6 +4394,27 @@ ApiResponse createMapLayer( return new ApiResponse<>(Constants.API_VERSION, "MapLayer", newMapLayer); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/maplayers", method = RequestMethod.POST) + @ResponseStatus(HttpStatus.OK) + ApiResponse createMapLayerByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + @RequestBody MapLayer mapLayer) { + + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + mapLayer.setMission(mission); + + MapLayer newMapLayer = missionService.addMapLayerToMission(mission.getName(), creatorUid, mission, mapLayer); + + return new ApiResponse<>(Constants.API_VERSION, "MapLayer", newMapLayer); + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/maplayers/{uid}", method = RequestMethod.DELETE) @@ -3425,6 +4431,22 @@ void deleteMapLayer( missionService.removeMapLayerFromMission(missionName, creatorUid, mission, uid); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/{missionGuid:.+}/maplayers/{uid}", method = RequestMethod.DELETE) + @ResponseStatus(HttpStatus.OK) + void deleteMapLayerByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + @PathVariable("uid") @NotNull String uid) { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + missionService.removeMapLayerFromMission(mission.getName(), creatorUid, mission, uid); + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/maplayers", method = RequestMethod.PUT) @@ -3445,6 +4467,26 @@ ApiResponse updateMapLayer( return new ApiResponse<>(Constants.API_VERSION, "MapLayer", newMapLayer); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/maplayers", method = RequestMethod.PUT) + @ResponseStatus(HttpStatus.OK) + ApiResponse updateMapLayerByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "creatorUid") @ValidatedBy("MartiSafeString") String creatorUid, + @RequestBody MapLayer mapLayer) { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + mapLayer.setMission(mission); + + MapLayer newMapLayer = missionService.updateMapLayer(mission.getName(), creatorUid, mission, mapLayer); + + return new ApiResponse<>(Constants.API_VERSION, "MapLayer", newMapLayer); + } private static String boundingPolygonPointsToString(List points) { if (points == null || points.size() == 0) { @@ -3503,6 +4545,31 @@ public ApiResponse> getMissionLayers( return null; } } + + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers", method = RequestMethod.GET) + public ApiResponse> getMissionLayersByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam) + throws ValidationException, IntrusionException, RemoteException { + + try { + + UUID missionGuid = parseGuid("missionGuidParam"); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + List missionLayers = missionService.hydrateMissionLayers(mission.getName(), mission); + + return new ApiResponse>( + Constants.API_VERSION, MissionLayer.class.getSimpleName(), missionLayers); + + } catch (Exception e) { + logger.error("exception in getMissionLayers", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return null; + } + } @PreAuthorize("hasPermission(#request, 'MISSION_READ')") @RequestMapping(value = "/missions/{missionName:.+}/layers/{layerUid:.+}", method = RequestMethod.GET) @@ -3529,6 +4596,34 @@ public ApiResponse getMissionLayer( return null; } } + + @PreAuthorize("hasPermission(#request, 'MISSION_READ')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers/{layerUid:.+}", method = RequestMethod.GET) + public ApiResponse getMissionLayerByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @PathVariable(value = "layerUid") String layerUid) + throws ValidationException, IntrusionException, RemoteException { + + try { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + MissionLayer missionLayer = missionService.hydrateMissionLayer(mission.getName(), mission, layerUid); + + return new ApiResponse( + Constants.API_VERSION, MissionLayer.class.getSimpleName(), missionLayer); + + } catch (NotFoundException nfe) { + throw nfe; + } catch (Exception e) { + logger.error("exception in getMissionLayer", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + return null; + } + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/layers", method = RequestMethod.PUT) @@ -3554,6 +4649,30 @@ public ApiResponse createMissionLayer( Constants.API_VERSION, MissionLayer.class.getSimpleName(), missionLayer); } + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers", method = RequestMethod.PUT) + public ApiResponse createMissionLayerByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "name", required = true) @ValidatedBy("MartiSafeString") String name, + @RequestParam(value = "type", required = true) @ValidatedBy("MartiSafeString") MissionLayer.Type type, + @RequestParam(value = "uid", required = false) @ValidatedBy("MartiSafeString") String uid, + @RequestParam(value = "parentUid", required = false) @ValidatedBy("MartiSafeString") String parentUid, + @RequestParam(value = "afterUid", required = false) @ValidatedBy("MartiSafeString") String afterUid, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid) + throws ValidationException, IntrusionException, RemoteException { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + MissionLayer missionLayer = missionService.addMissionLayer( + mission.getName(), mission, uid, name, type, parentUid, afterUid, creatorUid, groupVector); + + return new ApiResponse( + Constants.API_VERSION, MissionLayer.class.getSimpleName(), missionLayer); + } + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/layers/{layerUid:.+}/name", method = RequestMethod.PUT) public void setLayerName( @@ -3570,15 +4689,32 @@ public void setLayerName( missionService.setLayerName(missionName, mission, layerUid, name, creatorUid); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers/{layerUid:.+}/name", method = RequestMethod.PUT) + public void setLayerNameByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @PathVariable(value = "layerUid") String layerUid, + @RequestParam(value = "name", required = true) @ValidatedBy("MartiSafeString") String name, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid) + throws ValidationException, IntrusionException, RemoteException { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + missionService.setLayerName(mission.getName(), mission, layerUid, name, creatorUid); + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") - @RequestMapping(value = "/missions/{missionName:.+}/layers/{layerUid:.+}/position", method = RequestMethod.PUT) + @RequestMapping(value = "/missions/guid/{missionName:.+}/layers/{layerUid:.+}/position", method = RequestMethod.PUT) public void setLayerPosition( @PathVariable(value = "missionName") String missionName, @PathVariable(value = "layerUid") String layerUid, @RequestParam(value = "afterUid", required = false) @ValidatedBy("MartiSafeString") String afterUid, @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid) - throws ValidationException, IntrusionException, RemoteException { + throws ValidationException, IntrusionException, RemoteException { missionName = missionService.trimName(missionName); @@ -3587,6 +4723,23 @@ public void setLayerPosition( missionService.setLayerPosition(missionName, mission, layerUid, afterUid, creatorUid); } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers/{layerUid:.+}/position", method = RequestMethod.PUT) + public void setLayerPositionByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @PathVariable(value = "layerUid") String layerUid, + @RequestParam(value = "afterUid", required = false) @ValidatedBy("MartiSafeString") String afterUid, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid) + throws ValidationException, IntrusionException, RemoteException { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + missionService.setLayerPosition(mission.getName(), mission, layerUid, afterUid, creatorUid); + } @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/layers/parent", method = RequestMethod.PUT) @@ -3612,6 +4765,30 @@ public void setLayerParent( } } + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers/parent", method = RequestMethod.PUT) + public void setLayerParentByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "layerUid") @ValidatedBy("MartiSafeString") String[] layerUids, + @RequestParam(value = "parentUid", required = false) @ValidatedBy("MartiSafeString") String parentUid, + @RequestParam(value = "afterUid", required = false) @ValidatedBy("MartiSafeString") String afterUid, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid) + throws ValidationException, IntrusionException, RemoteException { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + // move the first layer in the parameter list after the afterUid + String useAfterUid = afterUid; + for (String layerUid : layerUids) { + missionService.setLayerParent(mission.getName(), mission, layerUid, parentUid, useAfterUid, creatorUid); + // move each other layer in the last after its predecessor in the parameter list + useAfterUid = layerUid; + } + } + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") @RequestMapping(value = "/missions/{missionName:.+}/layers", method = RequestMethod.DELETE) public void deleteMissionLayer( @@ -3629,9 +4806,27 @@ public void deleteMissionLayer( missionService.removeMissionLayer(missionName, mission, layerUid, creatorUid, groupVector); } } + + @PreAuthorize("hasPermission(#request, 'MISSION_WRITE')") + @RequestMapping(value = "/missions/guid/{missionGuid:.+}/layers", method = RequestMethod.DELETE) + public void deleteMissionLayerByGuid( + @PathVariable(value = "missionGuid") String missionGuidParam, + @RequestParam(value = "uid", required = true) @ValidatedBy("MartiSafeString") String[] layerUids, + @RequestParam(value = "creatorUid", required = true) @ValidatedBy("MartiSafeString") String creatorUid) + throws ValidationException, IntrusionException, RemoteException { + + UUID missionGuid = parseGuid(missionGuidParam); + + String groupVector = martiUtil.getGroupVectorBitString(request); + Mission mission = missionService.getMissionByGuid(missionGuid, groupVector); + + for (String layerUid : layerUids) { + missionService.removeMissionLayer(mission.getName(), mission, layerUid, creatorUid, groupVector); + } + } /* - * get all missions with pagination + * get all missions with pagination (used by mission manager UI) */ @RequestMapping(value = "/pagedmissions", method = RequestMethod.GET) Callable>> getPagedMissions( diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/SubscriptionApi.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/SubscriptionApi.java index c0efad05..037c018e 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/SubscriptionApi.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/api/SubscriptionApi.java @@ -31,7 +31,9 @@ import com.bbn.marti.config.Filter; import com.bbn.marti.cot.search.model.ApiResponse; import com.bbn.marti.network.BaseRestController; +import com.bbn.marti.remote.config.CoreConfigFacade; import com.bbn.marti.remote.ContactManager; +import com.bbn.marti.remote.exception.ValidationException; import com.bbn.marti.remote.RemoteCachedSubscription; import com.bbn.marti.remote.RemoteSubscription; import com.bbn.marti.remote.RemoteSubscriptionMetrics; @@ -944,6 +946,14 @@ public void setIface(String iface) { private void doSetActiveGroups(String username, List activeGroups, String clientUid) { + if (CoreConfigFacade.getInstance().getRemoteConfiguration() + .getAuth().isX509UseGroupCacheRequiresActiveGroup()) { + if (activeGroups.stream().noneMatch(g -> g.getActive())) { + subscriptionManager.sendGroupsUpdatedMessage(username, null); + throw new ValidationException(username + " must have at least 1 active group"); + } + } + // store the active group selection in the cache activeGroupCacheHelper.setActiveGroupsForUser(username, activeGroups); @@ -975,6 +985,11 @@ public ResponseEntity setActiveGroups( return new ResponseEntity(HttpStatus.OK); + } catch (ValidationException e) { + if (logger.isDebugEnabled()) { + logger.debug("ValidationException in setActiveGroups", e); + } + return new ResponseEntity(HttpStatus.BAD_REQUEST); } catch (Exception e) { logger.error("exception in setActiveGroups!", e); return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); @@ -1000,6 +1015,11 @@ public ResponseEntity setActiveGroups( return new ResponseEntity(HttpStatus.OK); + } catch (ValidationException e) { + if (logger.isDebugEnabled()) { + logger.debug("ValidationException in setActiveGroups", e); + } + return new ResponseEntity(HttpStatus.BAD_REQUEST); } catch (Exception e) { logger.error("exception in setActiveGroups!", e); return new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationAspect.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationAspect.java index 80acfd70..a23e6632 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationAspect.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationAspect.java @@ -2,8 +2,10 @@ import java.rmi.RemoteException; import java.util.NavigableSet; +import java.util.UUID; import com.bbn.marti.remote.config.CoreConfigFacade; +import com.bbn.marti.sync.service.MissionService; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.AfterReturning; import org.aspectj.lang.annotation.Aspect; @@ -44,6 +46,9 @@ public class MissionFederationAspect { @Autowired private GroupManager gm; + @Autowired + private MissionService ms; + // If we were using AspectJ instead of Spring AOP - something like !adviceexecution() would avoid the stack tracing // or name this pointcut and use !within(A) @AfterReturning(value = "execution(* com.bbn.marti.sync.service.MissionService.createMission(..))", returning="returnValue") @@ -238,8 +243,11 @@ public void addMissionContent(JoinPoint jp) throws RemoteException { logger.debug("addMissionContent advice " + jp.getSignature().getName() + " " + jp.getKind()); } + Mission mission = ms.getMissionByGuidCheckGroups((UUID) jp.getArgs()[0], (String) jp.getArgs()[3]); + ms.validateMissionByGuid(mission); + // federated add mission content - mfm.addMissionContent((String) jp.getArgs()[0], (MissionContent) jp.getArgs()[1], (String) jp.getArgs()[2], gm.groupVectorToGroupSet((String) jp.getArgs()[3])); + mfm.addMissionContent(mission.getName(), (MissionContent) jp.getArgs()[1], (String) jp.getArgs()[2], gm.groupVectorToGroupSet((String) jp.getArgs()[3])); } catch (Exception e) { if (logger.isDebugEnabled()) { @@ -266,8 +274,11 @@ public void deleteMissionContent(JoinPoint jp) throws RemoteException { logger.debug("addMissionContent advice " + jp.getSignature().getName() + " " + jp.getKind()); } + Mission mission = ms.getMissionByGuidCheckGroups((UUID) jp.getArgs()[0], (String) jp.getArgs()[3]); + ms.validateMissionByGuid(mission); + // federated add mission content - mfm.deleteMissionContent((String) jp.getArgs()[0], (String) jp.getArgs()[1], (String) jp.getArgs()[2], (String) jp.getArgs()[3], gm.groupVectorToGroupSet((String) jp.getArgs()[4])); + mfm.deleteMissionContent(mission.getName(), (String) jp.getArgs()[1], (String) jp.getArgs()[2], (String) jp.getArgs()[3], gm.groupVectorToGroupSet((String) jp.getArgs()[4])); } catch (Exception e) { if (logger.isDebugEnabled()) { @@ -294,8 +305,14 @@ public void setParent(JoinPoint jp) throws RemoteException { logger.debug("setParent advice " + jp.getSignature().getName() + " " + jp.getKind()); } + Mission childMission = ms.getMissionByGuidCheckGroups((UUID) jp.getArgs()[0], (String) jp.getArgs()[2]); + ms.validateMissionByGuid(childMission); + + Mission parentMission = ms.getMissionByGuidCheckGroups((UUID) jp.getArgs()[1], (String) jp.getArgs()[2]); + ms.validateMissionByGuid(parentMission); + // federated add mission content - mfm.setParent((String) jp.getArgs()[0], (String) jp.getArgs()[1], gm.groupVectorToGroupSet((String) jp.getArgs()[2])); + mfm.setParent(childMission.getName(), parentMission.getName(), gm.groupVectorToGroupSet((String) jp.getArgs()[2])); } catch (Exception e) { if (logger.isDebugEnabled()) { @@ -322,8 +339,11 @@ public void clearParent(JoinPoint jp) throws RemoteException { logger.debug("setParent advice " + jp.getSignature().getName() + " " + jp.getKind()); } + Mission childMission = ms.getMissionByGuidCheckGroups((UUID) jp.getArgs()[0], (String) jp.getArgs()[1]); + ms.validateMissionByGuid(childMission); + // federated clear mission parent - mfm.clearParent((String) jp.getArgs()[0], gm.groupVectorToGroupSet((String) jp.getArgs()[1])); + mfm.clearParent(childMission.getName(), gm.groupVectorToGroupSet((String) jp.getArgs()[1])); } catch (Exception e) { if (logger.isDebugEnabled()) { diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationManagerROL.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationManagerROL.java index 421a236d..54c61db8 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationManagerROL.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/federation/MissionFederationManagerROL.java @@ -235,8 +235,10 @@ public void addMissionContent(String missionName, MissionContent content, String // TODO: When mission federation is updated to support federating guids, update this code accordingly. Mission guids // will extra handling in federated case. - - Mission fedMission = missionService.getMissionByNameCheckGroups(missionName, creatorUid); + + String groupVector = remoteUtil.bitVectorToString(RemoteUtil.getInstance().getBitVectorForGroups(groups)); + + Mission fedMission = missionService.getMissionByNameCheckGroups(missionName, groupVector); if (!(CoreConfigFacade.getInstance().getRemoteConfiguration().getFederation().isAllowMissionFederation())) { return; @@ -247,7 +249,7 @@ public void addMissionContent(String missionName, MissionContent content, String try { // include mission metadata in federated add content, so that the mission can be created on the federate, if it does not exist - Mission mission = missionService.getMission(missionName, remoteUtil.bitVectorToString(RemoteUtil.getInstance().getBitVectorForGroups(groups))); + Mission mission = missionService.getMission(missionName, groupVector); if (mission == null) { throw new IllegalArgumentException("can't federate mission change for mission " + missionName + " that does not exist"); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionRepository.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionRepository.java index 0470dea9..9f3f4aa4 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionRepository.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionRepository.java @@ -27,6 +27,10 @@ public interface MissionRepository extends JpaRepository { @Query(value = missionAttributes + " from mission where guid = uuid(:guid)", nativeQuery = true) Mission getByGuid(@Param("guid") UUID missionGuid); + + // Only fetch the mission name for the guid (rather than the whole mission) + @Query(value = "select name from mission where guid = uuid(:guid)", nativeQuery = true) + String getMissionNameForMissionGuid(@Param("guid") UUID missionGuid); Long findMissionIdByName(String name); @@ -180,7 +184,7 @@ public interface MissionRepository extends JpaRepository { "and tool in :tools AND " + RemoteUtil.GROUP_CLAUSE + " order by id desc ", nativeQuery = true) List getAllMissionsByTools(@Param("passwordProtected") boolean passwordProtected, @Param("defaultRole") boolean defaultRole, @Param("tools") List tools, @Param("groupVector") String groupVector); - @Query(value = missionAttributes + " from mission where invite_only = false and " + + @Query(value = missionAttributes + " from mission where name != 'exchecktemplates' and name != 'citrap' and invite_only = false and " + "((:passwordProtected = false and password_hash is null) or :passwordProtected = true)" + // only include password protected missions if asked to "and ((:defaultRole = false and (default_role_id is null or default_role_id = 2)) or :defaultRole = true) " + // return new missions with default role of MISSION_SUBSCRIBER to older clients "AND " + RemoteUtil.GROUP_CLAUSE + " and create_time < (now() - (:ttl * INTERVAL '1 second'))" + " order by id desc ", nativeQuery = true) @@ -258,6 +262,9 @@ Long update(@Param("name") String name, @Param("groupVector") String groupVector @Query(value = "update mission set password_hash = :passwordHash where lower(name) = lower(:name) and" + RemoteUtil.GROUP_CLAUSE + " returning id", nativeQuery = true) Long setPasswordHash(@Param("name") String name, @Param("passwordHash") String password, @Param("groupVector") String groupVector); + + @Query(value = "update mission set password_hash = :passwordHash where guid = uuid(:guid) and" + RemoteUtil.GROUP_CLAUSE + " returning id", nativeQuery = true) + Long setPasswordHashByGuid(@Param("guid") String guid, @Param("passwordHash") String password, @Param("groupVector") String groupVector); @Query(value = "update mission set default_role_id = :defaultRoleId where lower(name) = lower(:name) and" + RemoteUtil.GROUP_CLAUSE + " returning id", nativeQuery = true) Long setDefaultRoleId(@Param("name") String name, @Param("defaultRoleId") Long defaultRoleId, @Param("groupVector") String groupVector); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionSubscriptionRepository.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionSubscriptionRepository.java index 302acd65..cd2a09d8 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionSubscriptionRepository.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/repository/MissionSubscriptionRepository.java @@ -87,6 +87,10 @@ void subscribe(@Param("missionId") long missionId, @Param("clientUid") String cl @Query(value = "select ms.uid, null as token, null as mission_id, ms.client_uid, ms.username, ms.create_time, ms.role_id from mission m " + "inner join mission_subscription ms on m.id = ms.mission_id where m.name = :missionName", nativeQuery = true) List findAllByMissionNameNoMissionNoToken(@Param("missionName") String missionName); + + @Query(value = "select ms.uid, null as token, null as mission_id, ms.client_uid, ms.username, ms.create_time, ms.role_id from mission m " + + "inner join mission_subscription ms on m.id = ms.mission_id where m.guid = uuid(:missionGuid)", nativeQuery = true) + List findAllByMissionGuidNoMissionNoToken(@Param("missionGuid") String missionGuid); @Query(value = "select ms.uid, ms.token, m.id as mission_id, ms.client_uid, ms.username, ms.create_time, ms.role_id from mission m " + "inner join mission_subscription ms on m.id = ms.mission_id where m.tool = 'public' ", nativeQuery = true) diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionService.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionService.java index 54b3d73f..652bdb5e 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionService.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionService.java @@ -51,6 +51,8 @@ public interface MissionService { Mission getMission(String missionName, boolean hydrateDetails); Mission getMissionByGuid(UUID missionGuid, boolean hydrateDetails); + + String getMissionNameByGuid(UUID missionGuid); Mission getMission(String missionName, String groupVector); @@ -85,6 +87,8 @@ public interface MissionService { List getAllCotForUid(String uid, Date start, Date end, String groupVector); String getCachedCot(String missionName, Set uids, String groupVector); + + String getCachedCot(UUID missionGuid, Set uids, String groupVector); List getCotElementsByTimeAndBbox(Date start, Date end, GeospatialFilter.BoundingBox boundingBox, String groupVector); @@ -101,6 +105,8 @@ public interface MissionService { Set getAllMissionInvitationsForClient(String clientUid, String groupVector); List getMissionInvitations(String missionName); + + List getMissionInvitationsByGuid(UUID missionGuid); List getInviteOnlyMissions(String userName, String tool, NavigableSet groups); @@ -156,7 +162,7 @@ public interface MissionService { ExternalMissionData hydrate(String externalDataUid, String externalDataName, String externalDataTool, String externalDataToken, String externalDataNotes); - String addMissionArchiveToEsync(String name, byte[] archive, String groupVector, boolean archivedWhenDeleting); + String addMissionArchiveToEsync(String archiveName, byte[] archiveBytes, String groupVector, boolean archivedWhenDeleting); String trimName(String name); @@ -180,6 +186,8 @@ public interface MissionService { String getMissionKml(String missionName, String urlBase, String groupVector); + String getMissionKml(UUID missionGuid, String urlBase, String groupVector); + ExternalMissionData setExternalMissionData(UUID missionGuid, String creatorUid, ExternalMissionData externalMissionData, String groupVector); void deleteExternalMissionData(UUID missionGuid, String externalMissionDataId, String notes, String creatorUid, String groupVector); @@ -219,9 +227,11 @@ public interface MissionService { void setSubscriptionUsername(Long missionId, String clientUid, String username); boolean validatePermission(MissionPermission.Permission permission, HttpServletRequest request); - + List> getAllMissionSubscriptions(); + List, String>> getAllMissionSubscriptionsWithGuid(); + MissionRole getDefaultRole(Mission mission); int getApiVersionNumberFromRequest(HttpServletRequest request); @@ -232,12 +242,19 @@ public interface MissionService { CotEventContainer getLatestCotEventContainerForUid(String uid, String groupVector); - Mission getMissionByNameCheckGroups(String missionName, String groupVector); + // get mission by name (no group check) + Mission getMissionByName(String missionName, boolean hydrateDetails); + + Mission getMissionByNameCheckGroups(String missionName, boolean hydrateDetails, String groupVector); + + Mission getMissionByNameCheckGroups(String missionName, String groupVector); Mission getMissionByGuidCheckGroups(UUID missionGuid, String groupVector); boolean setExpiration(String missionName, Long ttl, String groupVector); + boolean setExpiration(UUID missionGuid, Long ttl, String groupVector); + void deleteMissionByTtl(Integer ttl); void deleteMissionByExpiration(Long expiration); @@ -246,16 +263,19 @@ public interface MissionService { // resource and mission change caching Map> getCachedResources(String missionName, Set resources); + + // resource and mission change caching + Map> getCachedResourcesByGuid(UUID missionGuid, Set resources); - List findLatestCachedMissionChanges(String missionName, List uids, List hashes, int changeType); + List findLatestCachedMissionChanges(UUID missionGuid, List uids, List hashes, int changeType); - List findLatestCachedMissionChangesForUids(String missionName, List uids, int changeType); + List findLatestCachedMissionChangesForUids(UUID missionGuid, List uids, int changeType); - List findLatestCachedMissionChangesForHashes(String missionName, List hashes, int changeType); + List findLatestCachedMissionChangesForHashes(UUID missionGuid, List hashes, int changeType); - Collection getLatestMissionCotWrappersForUids(String missionName, Set uids, String groupVector); + Collection getLatestMissionCotWrappersForUids(UUID missionGuid, Set uids, String groupVector); - List getCachedResourcesByHash(String missionName, String hash); + List getCachedResourcesByHash(UUID missionGuid, String hash); MissionFeed getMissionFeed(String missionFeedUid); @@ -270,10 +290,16 @@ public interface MissionService { MapLayer getMapLayer(String mapLayerUid); MapLayer addMapLayerToMission(String missionName, String creatorUid, Mission mission, MapLayer mapLayer); + +// MapLayer addMapLayerToMissionByGuid(UUID missionGuid, String creatorUid, Mission mission, MapLayer mapLayer); MapLayer updateMapLayer(String missionName, String creatorUid, Mission mission, MapLayer mapLayer); + +// MapLayer updateMapLayerByGuid(UUID missionGuid, String creatorUid, Mission mission, MapLayer mapLayer); void removeMapLayerFromMission(String missionName, String creatorUid, Mission mission, String mapLayerUid); + +// void removeMapLayerFromMissionByGuid(UUID missionGuid, String creatorUid, Mission mission, String mapLayerUid); List getMissionsForDataFeed(String feed_uid); @@ -312,6 +338,8 @@ public interface MissionService { List getMissionSubscriptionsByMissionGuidNoMission(UUID missionGuid); List getMissionSubscriptionsByMissionNameNoMissionNoToken(String missionName); + + List getMissionSubscriptionsByMissionGuidNoMissionNoToken(UUID missionGuid); List getAllMissionsGuids(boolean passwordProtected, boolean defaultRole, String tool); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionServiceDefaultImpl.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionServiceDefaultImpl.java index 0adf2048..bb0f5422 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionServiceDefaultImpl.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/sync/service/MissionServiceDefaultImpl.java @@ -489,7 +489,7 @@ public List getLatestCotForUids(Set uids, String groupVector @Override @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") - public Collection getLatestMissionCotWrappersForUids(String missionName, Set uids, String groupVector) { + public Collection getLatestMissionCotWrappersForUids(UUID missionGuid, Set uids, String groupVector) { return cotCacheHelper.getLatestCotWrappersForUids(uids, groupVector, true); } @@ -619,6 +619,29 @@ public String getCachedCot(String missionName, Set uids, String groupVec return result.toString(); } + + @Override + @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator", sync = true) + public String getCachedCot(UUID missionGuid, Set uids, String groupVector) { + + StringBuilder result = new StringBuilder(); + result.append(Constants.XML_HEADER); + result.append(""); + + // Get mission uids, then the latest CoT for each + List cot = getLatestCotForUids(uids, groupVector); + @SuppressWarnings("rawtypes") + Iterator it = cot.iterator(); + while (it.hasNext()) { + CotElement cotElement = (CotElement)it.next(); + result.append(cotElement.toCotXml()); + result.append('\n'); + } + + result.append(""); + + return result.toString(); + } @Override public boolean deleteAllCotForUids(List uids, String groupVector) { @@ -906,10 +929,8 @@ public Mission hydrate(Mission mission, boolean hydrateDetails) { long start = System.currentTimeMillis(); - if (cacheLogger.isTraceEnabled()) { - cacheLogger.trace("mission in hydrate " + hydrateDetails + " : " + mission); - } - + logger.debug("hyrdate mission details: {} mission: {} ", hydrateDetails, mission); + if (mission == null) { return null; } @@ -930,8 +951,11 @@ public Mission hydrate(Mission mission, boolean hydrateDetails) { HashMap resourceMap = new HashMap<>(); if (!mission.getContents().isEmpty()) { + + logger.debug("fetching contents of mission {} {}", mission.getName(), mission.getGuid()); + Set resources = mission.getContents(); - Map> keywordMap = getMissionService().getCachedResources(mission.getName(), resources); // hydrate resources! + Map> keywordMap = getMissionService().getCachedResourcesByGuid(mission.getGuidAsUUID(), resources); // hydrate resources for (Resource resource : resources) { List keywords = keywordMap.get(resource.getId()); resource.setKeywords(keywords); @@ -946,7 +970,7 @@ public Mission hydrate(Mission mission, boolean hydrateDetails) { // collect up the uids Map uidDetailsMap = new ConcurrentHashMap<>(); if (hydrateDetails && !mission.getUids().isEmpty()) { - Collection cotWrappers = getMissionService().getLatestMissionCotWrappersForUids(mission.getName(), mission.getUids(), mission.getGroupVector()); + Collection cotWrappers = getMissionService().getLatestMissionCotWrappersForUids(mission.getGuidAsUUID(), mission.getUids(), mission.getGroupVector()); cotWrappers.forEach((wrapper) -> uidDetailsMap.put(wrapper.getUid(), wrapper.getUidDetails())); } @@ -959,14 +983,15 @@ public Mission hydrate(Mission mission, boolean hydrateDetails) { List changes = null; if (!mission.getUids().isEmpty() && !mission.getContents().isEmpty()) { changes = getMissionService().findLatestCachedMissionChanges(// db / cache - mission.getName(), new ArrayList<>(mission.getUids()), + mission.getGuidAsUUID(), new ArrayList<>(mission.getUids()), new ArrayList<>(resourceMap.keySet()), MissionChangeType.ADD_CONTENT.ordinal()); } else if (!mission.getUids().isEmpty()) { changes = getMissionService().findLatestCachedMissionChangesForUids(// db / cache - mission.getName(), new ArrayList<>(mission.getUids()), MissionChangeType.ADD_CONTENT.ordinal()); + mission.getGuidAsUUID(), new ArrayList<>(mission.getUids()), MissionChangeType.ADD_CONTENT.ordinal()); } else if (!mission.getContents().isEmpty()) { changes = getMissionService().findLatestCachedMissionChangesForHashes( // db / cache - mission.getName(), new ArrayList<>(resourceMap.keySet()), MissionChangeType.ADD_CONTENT.ordinal()); + mission.getGuidAsUUID(), new ArrayList<>(resourceMap.keySet()), MissionChangeType.ADD_CONTENT.ordinal()); + } else { return mission; } @@ -1059,6 +1084,8 @@ public Map> hydrate(Set resources) { try (Connection connection = dataSource.getConnection()) { idArray = connection.createArrayOf("int", resourceIds.toArray()); } + + logger.debug("hydrating resourceIds {}", resourceIds); Map> keywordsMap = new HashMap<>(); @@ -1466,6 +1493,18 @@ public List getMissionInvitations(String missionName) { return missionInvitationRepository.findAllByMissionId(missionId); } + + @Override + public List getMissionInvitationsByGuid(UUID missionGuid) { + + Long missionId = missionRepository.getLatestMissionIdForMissionGuid(missionGuid.toString()); + + if (missionId == null) { + throw new NotFoundException("mission " + missionGuid + " does not exist."); + } + + return missionInvitationRepository.findAllByMissionId(missionId); + } @Override public Set getAllMissionInvitationsForClient(String clientUid, String groupVector) { @@ -1589,211 +1628,19 @@ public List> getAllMissionSubscriptions() { } return results; } + + @Override + public List, String>> getAllMissionSubscriptionsWithGuid() { + List, String>> results = new ArrayList<>(); + for (MissionSubscription subscription : missionSubscriptionRepository.findAll()) { + Map.Entry missionId = Maps.immutableEntry(subscription.getMission().getGuidAsUUID().toString(), subscription.getMission().getName()); + results.add(Maps.immutableEntry(missionId, subscription.getClientUid())); + } + return results; + } private AtomicInteger addCount = new AtomicInteger(); - // TODO: delete once verified - now dupe code - -// @Override -// public Mission addMissionContentAtTime(UUID missionGuid, MissionContent missionContent, String creatorUid, String groupVector, Date date, String xmlContentForNotification) { -// -// if (logger.isDebugEnabled()) { -// logger.debug("addMissionContent " + missionContent + " missionName: " + missionName + " creatorUid: " + creatorUid); -// } -// -// Mission mission = getMissionService().getMissionByNameCheckGroups(trimName(missionName), groupVector); -// getMissionService().validateMission(mission, missionName); -// -// mission.setName(trimName(missionName)); -// -// if (logger.isDebugEnabled()) { -// logger.debug("mission for add content: " + mission); -// } -// -// Collection changes = new CopyOnWriteArrayList(); -// -// Map> contentMap = new HashMap<>(); -// contentMap.put(null, Arrays.asList(missionContent)); -// if (missionContent.getPaths() != null) { -// contentMap.putAll(missionContent.getPaths()); -// } -// -// CoreConfig coreConfig = CoreConfigFacade.getInstance(); -// -// for (Map.Entry> pathContentEntry : contentMap.entrySet()) { -// -// String path = pathContentEntry.getKey(); -// List contents = pathContentEntry.getValue(); -// -// -// for (MissionContent content : contents) { -// -// String after = content.getAfter(); -// -// // add the resource by hash if it exists -// for (String hash : content.getHashes()) { -// -// if (mission.getContents().size() >= coreConfig.getRemoteConfiguration().getBuffer().getQueue().getMissionContentLimit()) { -// logger.error("File limit (" + coreConfig.getRemoteConfiguration().getBuffer().getQueue().getMissionContentLimit() + ") exceeded for mission " + missionName); -// break; -// } -// -// if (hash != null) { -// -// List resourceList = getMissionService().getCachedResourcesByHash(mission.getName(), hash); -// -// if (!resourceList.isEmpty() && resourceList.get(0) != null) { -// -// mission.getContents().add(resourceList.get(0)); -// -// // track change -// MissionChange change = new MissionChange(MissionChangeType.ADD_CONTENT, mission, resourceList.get(0).getHash(), null); -// change.setTimestamp(date); -// change.setCreatorUid(creatorUid); -// missionChangeRepository.saveAndFlush(change); -// -// MissionAdd resourceAdd = new MissionAdd<>(); -// resourceAdd.setData(resourceList.get(0)); -// resourceAdd.setTimestamp(change.getTimestamp()); -// resourceAdd.setCreatorUid(change.getCreatorUid()); -// -// List> resourceAdds = new CopyOnWriteArrayList<>(); -// resourceAdds.add(resourceAdd); -// mission.setResourceAdds(resourceAdds); -// -// // explicitly save in case it didn't propagate from the change -// try { -// missionRepository.addMissionResource(mission.getId(), resourceList.get(0).getId(), resourceList.get(0).getHash()); -// } catch (Exception e) { -// logger.debug("exception explicitly saving mission resource", e); -// } -// -// changes.add(change); -// -// if (logger.isDebugEnabled()) { -// logger.debug("Adding mission content mission id " + mission.getId() + " resource id " + resourceList.get(0).getId() + " resource name " + resourceList.get(0).getName() + " hash " + hash + " mission change " + change); -// } -// -// if (path != null) { -// try { -// getMissionService().addMissionLayer( -// missionName, mission, resourceList.get(0).getHash(), null, -// MissionLayer.Type.ITEM, path, after, creatorUid, groupVector); -// after = resourceList.get(0).getHash(); -// } catch (Exception e) { -// logger.error("exception adding mission layer", e); -// } -// } -// } -// } -// } -// -// for (String uid : content.getUids()) { -// try { -// -// try { -// -// if (mission.getUids().size() >= coreConfig.getRemoteConfiguration().getBuffer().getQueue().getMissionUidLimit()) { -// logger.error("Track limit (" + coreConfig.getRemoteConfiguration().getBuffer().getQueue().getMissionUidLimit() + ") exceeded for mission " + missionName); -// break; -// } -// -// try { -// // also track in core services -// subscriptionManager.putMissionContentUid(missionName, uid); -// } catch (Exception e) { -// if (logger.isDebugEnabled()) { -// logger.debug("exception tracking mission content uid " + e.getMessage(), e); -// } -// } -// -// } catch (DataIntegrityViolationException e) { -// logger.info("mission already contains resource " + e.getMessage(), e); -// } -// -// // track change -// MissionChange change = new MissionChange(MissionChangeType.ADD_CONTENT, mission, null, uid); -// change.setTimestamp(date); -// change.setCreatorUid(creatorUid); -// -// asyncExecutor.execute(() -> { -// -// missionChangeRepository.saveAndFlush(change); -// -// // explicitly save in case it didn't propagate from the change -// try { -// missionRepository.addMissionUid(mission.getId(), uid); -// } catch (Exception e) { -// logger.debug("exception explicitly saving mission uid", e); -// } -// -// }); -// -// changes.add(change); -// -// if (path != null) { -// try { -// getMissionService().addMissionLayer( -// missionName, mission, uid, null, -// MissionLayer.Type.ITEM, path, after, creatorUid, groupVector); -// after = uid; -// } catch (Exception e) { -// logger.error("exception adding mission layer", e); -// } -// } -// -// } catch (Exception e) { -// logger.warn("exception saving mission change", e); -// } -// } -// -// } -// } -// -// if (changeLogger.isDebugEnabled()) { -// addCount.addAndGet(changes.size()); -// changeLogger.debug("mission changes to save: " + changes.size() + " total changes: " + addCount.get()); -// } -// -// for (MissionChange change : changes) { -// -// try { -// -// MissionChanges missionChanges = new MissionChanges(); -// missionChanges.add(change); -// -// hydrateMissionChange(change); -// -// String changeXml = commonUtil.toXml(missionChanges); -// -// if (changeLogger.isTraceEnabled()) { -// changeLogger.trace(" announcing change " + changeXml); -// } -// -// subscriptionManager.announceMissionChange(UUID.fromString(mission.getGuid()) ,missionName, ChangeType.CONTENT, creatorUid, mission.getTool(), changeXml, xmlContentForNotification); -// -// if (logger.isDebugEnabled()) { -// logger.debug("mission change announced"); -// } -// } catch (Exception e) { -// logger.warn("exception announcing mission change " + e.getMessage(), e); -// } -// } -// -// // only empty the cache if something was actually added -// if (!changes.isEmpty()) { -// asyncExecutor.execute(() -> { -// try { -// getMissionService().invalidateMissionCache(missionName); -// } catch (Exception e) { -// logger.warn("exception clearing mission cache " + missionName, e); -// } -// }); -// } -// -// return mission; -// } - @Override public Mission addMissionContentAtTime(UUID missionGuid, MissionContent missionContent, String creatorUid, String groupVector, Date date, String xmlContentForNotification) { @@ -1838,7 +1685,7 @@ public Mission addMissionContentAtTime(UUID missionGuid, MissionContent missionC if (hash != null) { - List resourceList = getMissionService().getCachedResourcesByHash(mission.getName(), hash); + List resourceList = getMissionService().getCachedResourcesByHash(mission.getGuidAsUUID(), hash); if (!resourceList.isEmpty() && resourceList.get(0) != null) { @@ -2176,10 +2023,12 @@ public LogEntry addUpdateLogEntry(LogEntry entry, Date created, String groupVect } Map toolMap = new HashMap(); + Map guidMap = new HashMap(); for (String missionName : entry.getMissionNames()) { Mission mission = getMissionService().getMissionByNameCheckGroups(trimName(missionName), groupVector); validateMission(mission, missionName); toolMap.put(missionName, mission.getTool()); + guidMap.put(missionName, mission.getGuidAsUUID()); } entry.setServertime(new Date()); @@ -2194,8 +2043,7 @@ public LogEntry addUpdateLogEntry(LogEntry entry, Date created, String groupVect for (String missionName : entry.getMissionNames()) { try { - // TODO fix this method so that the guid is scope here for the annoucement - don't leave this null in - subscriptionManager.announceMissionChange(null, missionName, SubscriptionManagerLite.ChangeType.LOG, entry.getCreatorUid(), toolMap.get(missionName),null); + subscriptionManager.announceMissionChange(guidMap.get(missionName), missionName, SubscriptionManagerLite.ChangeType.LOG, entry.getCreatorUid(), toolMap.get(missionName),null); } catch (Exception e) { logger.warn("exception announcing mission change " + e.getMessage(), e); } @@ -2685,21 +2533,21 @@ private ContentType resourceToContentType(Resource resource) { @Override @Transactional @CacheEvict(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, allEntries = true) - public String addMissionArchiveToEsync(String name, byte[] archive, String groupVector, boolean archivedWhenDeleting) { + public String addMissionArchiveToEsync(String archiveName, byte[] archiveBytes, String groupVector, boolean archivedWhenDeleting) { try { // build up the metadata for adding to enterprise sync Metadata toStore = new Metadata(); if (archivedWhenDeleting) { toStore.set(Metadata.Field.Keywords, "ARCHIVED_MISSION"); } - toStore.set(Metadata.Field.DownloadPath, name + ".zip"); - toStore.set(Metadata.Field.Name, name); + toStore.set(Metadata.Field.DownloadPath, archiveName + ".zip"); + toStore.set(Metadata.Field.Name, archiveName); toStore.set(Metadata.Field.MIMEType, "application/zip"); toStore.set(Metadata.Field.UID, new String[]{UUID.randomUUID().toString()}); // add mission package to enterprise sync - toStore = syncStore.insertResource(toStore, archive, groupVector); + toStore = syncStore.insertResource(toStore, archiveBytes, groupVector); return toStore.getHash(); } catch (Exception e) { throw new TakException("Exception in addMissionArchiveToEsync!", e); @@ -2988,9 +2836,7 @@ public String trimName(@NotNull String name) { @Override public void validateMission(Mission mission, String missionName) { - if (logger.isDebugEnabled()) { - logger.debug("validateMission " + mission + " missionName"); - } + logger.debug("validateMission {} {} ", mission, missionName); if (mission == null) { // if a mission was deleted, respond with a 410 @@ -3184,6 +3030,8 @@ public Mission getMissionByGuid(UUID missionGuid, String groupVector) { if (mission != null && !remoteUtil.isGroupVectorAllowed(groupVector, mission.getGroupVector())) { mission = null; } + + logger.debug("return mission in getMissionByGuid {} {}", missionGuid, mission); // will throw MissionDeletedException if deleted, NotFoundException if not found getMissionService().validateMissionByGuid(mission); @@ -3204,12 +3052,17 @@ public Mission getMissionNoDetails(String missionName, String groupVector) { return mission; } + + @Override + public Mission getMissionByName(String missionName, boolean hydrateDetails) { + + return missionCacheHelper.getMission(missionName, hydrateDetails, false); + } @Override - //@Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") - public Mission getMissionByNameCheckGroups(String missionName, String groupVector) { + public Mission getMissionByNameCheckGroups(String missionName, boolean hydrateDetails, String groupVector) { - Mission mission = missionCacheHelper.getMission(missionName, false, false); + Mission mission = missionCacheHelper.getMission(missionName, hydrateDetails, false); if (mission != null) { if (!remoteUtil.isGroupVectorAllowed(groupVector, mission.getGroupVector())) { @@ -3220,6 +3073,11 @@ public Mission getMissionByNameCheckGroups(String missionName, String groupVecto return mission; } + @Override + public Mission getMissionByNameCheckGroups(String missionName, String groupVector) { + return getMissionByNameCheckGroups(missionName, false, groupVector); + } + @Override //@Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") public Mission getMissionByGuidCheckGroups(UUID missionGuid, String groupVector) { @@ -3548,7 +3406,7 @@ public void invalidateMissionCache(UUID missionGuid) { } @Override - @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER) + @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, sync = true) public Set getMissionChanges(String missionName, String groupVector, Long secago, Date start, Date end, boolean squashed) { @@ -3693,7 +3551,44 @@ public String getMissionKml(String missionName, String urlBase, String groupVect return baos.toString(); } catch (Exception e) { - logger.error("exception in getMissionKml!", e); + logger.error("exception in getMissionKml", e); + return null; + } + } + + @Override + @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER) + public String getMissionKml(UUID missionGuid, String urlBase, String groupVector) { + try { + + Mission mission = getMissionService().getMissionNoContentByGuid(missionGuid, groupVector); + + LinkedList cotElements = new LinkedList(); + for (String uid : mission.getUids()) { + CotElement cot = getLatestCotElement( + uid, RemoteUtil.getInstance().getBitStringAllGroups(), + TimeUtils.MAX_TS, + kmlDao.new CotElementResultExtractor()); + + if (cot == null) { + logger.error("skipping uid in getMissionKml " + uid); + continue; + } + + cotElements.add(cot); + } + + Kml kml = kmlService.process(cotElements); + if (urlBase != null) { + kmlService.setStyleUrlBase(kml, urlBase); + } + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + kml.marshal(baos); + return baos.toString(); + + } catch (Exception e) { + logger.error("exception in getMissionKml by guid", e); return null; } } @@ -4393,6 +4288,36 @@ public boolean setExpiration(String missionName, Long expiration, String groupVe } return updated > 0; } + + @CacheEvict(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, allEntries = true) + @Override + public boolean setExpiration(UUID missionGuid, Long expiration, String groupVector) { + + if (logger.isDebugEnabled()) { + logger.debug(" setting expiration on mission " + missionGuid + " expiration " + expiration); + } + + MapSqlParameterSource namedParameters; + + if (expiration == null) { + namedParameters = new MapSqlParameterSource("expiration", -1L); + } else { + if (expiration < -1) { + throw new IllegalArgumentException("bad expiration parameter" + expiration); + } else { + namedParameters = new MapSqlParameterSource("expiration", expiration); + } + } + + namedParameters.addValue("missionGuid", missionGuid.toString()); + + String sql = "update mission set expiration = :expiration where guid = uuid(:missionName)"; + int updated = new NamedParameterJdbcTemplate(dataSource).update(sql, namedParameters); + if (logger.isDebugEnabled()) { + logger.debug(" did the mission get updated " + updated); + } + return updated > 0; + } @Override public void deleteMissionByTtl(Integer ttl) { @@ -4439,9 +4364,9 @@ public void deleteMissionByExpiration(Long expiration) { @Override @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") - public List findLatestCachedMissionChanges(String missionName, List uids, List hashes, int changeType) { + public List findLatestCachedMissionChanges(UUID missionGuid, List uids, List hashes, int changeType) { - Long missionId = missionRepository.getLatestMissionIdForName(missionName); + Long missionId = missionRepository.getLatestMissionIdForMissionGuid(missionGuid.toString()); if (missionId == null) { return null; @@ -4455,12 +4380,18 @@ public List findLatestCachedMissionChanges(String missionName, Li public Map> getCachedResources(String missionName, Set resources) { return getMissionService().hydrate(resources); } + + @Override + @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") + public Map> getCachedResourcesByGuid(UUID missionGuid, Set resources) { + return getMissionService().hydrate(resources); + } @Override @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") - public List findLatestCachedMissionChangesForUids(@Param("missionName") String missionName, @Param("uids") List uids, @Param("changeType") int changeType) { + public List findLatestCachedMissionChangesForUids(UUID missionGuid, List uids, int changeType) { - Long missionId = missionRepository.getLatestMissionIdForName(missionName); + Long missionId = missionRepository.getLatestMissionIdForMissionGuid(missionGuid.toString()); if (missionId == null) { return null; @@ -4471,9 +4402,9 @@ public List findLatestCachedMissionChangesForUids(@Param("mission @Override @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") - public List findLatestCachedMissionChangesForHashes(@Param("missionName") String missionName, @Param("hashes") List hashes, @Param("changeType") int changeType) { + public List findLatestCachedMissionChangesForHashes(UUID missionGuid, List hashes, int changeType) { - Long missionId = missionRepository.getLatestMissionIdForName(missionName); + Long missionId = missionRepository.getLatestMissionIdForMissionGuid(missionGuid.toString()); if (missionId == null) { return null; @@ -4482,10 +4413,9 @@ public List findLatestCachedMissionChangesForHashes(@Param("missi return missionChangeRepository.findLatestForHashes(missionId, hashes, changeType); } - // mission name only for cache key @Override @Cacheable(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, keyGenerator = "methodNameMultiStringArgCacheKeyGenerator") - public List getCachedResourcesByHash(String missionName, String hash) { + public List getCachedResourcesByHash(UUID missionGuid, String hash) { return resourceRepository.findByHash(hash); } @@ -4628,7 +4558,7 @@ public MapLayer addMapLayerToMission(String missionName, String creatorUid, Miss return newMapLayer; } - + @Override @CacheEvict(cacheResolver = MissionCacheResolver.MISSION_CACHE_RESOLVER, allEntries = true) public MapLayer updateMapLayer(String missionName, String creatorUid, Mission mission, MapLayer mapLayer) { @@ -5062,5 +4992,29 @@ public List getMissionSubscriptionsByMissionNameNoMissionNo } + @Override + public List getMissionSubscriptionsByMissionGuidNoMissionNoToken(UUID missionGuid) { + + List msl = missionSubscriptionRepository.findAllByMissionGuidNoMissionNoToken(missionGuid.toString()); + + if (msl == null) { + return null; + } + + List result = new ArrayList<>(); + + for (MissionSubscription ms : msl) { + result.add((MissionSubscription) Hibernate.unproxy(ms)); + } + + return result; + + } + + @Override + public String getMissionNameByGuid(UUID missionGuid) { + return missionRepository.getMissionNameForMissionGuid(missionGuid); + } + } diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/KmlUtils.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/KmlUtils.java index 3bf24839..ea8a9e4a 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/KmlUtils.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/KmlUtils.java @@ -38,6 +38,7 @@ import de.micromata.opengis.kml.v_2_2_0.Feature; import de.micromata.opengis.kml.v_2_2_0.Folder; import de.micromata.opengis.kml.v_2_2_0.Geometry; +import de.micromata.opengis.kml.v_2_2_0.Icon; import de.micromata.opengis.kml.v_2_2_0.IconStyle; import de.micromata.opengis.kml.v_2_2_0.Kml; import de.micromata.opengis.kml.v_2_2_0.LabelStyle; @@ -298,11 +299,14 @@ public static Style buildStyle(String styleUrl, String iconUrl, if (lineColor == null) lineColor = "8fffffff"; + Icon icon = new Icon(); + + icon.setHref(iconUrl); + s.withId(styleUrl) .withIconStyle(new IconStyle() .withScale(1.0) - .withIcon(new BasicLink() - .withHref(iconUrl))) + .withIcon(icon)) .withLabelStyle(new LabelStyle() .withColor(labelColor) .withScale(1.5)) @@ -495,8 +499,10 @@ public static Placemark buildOptimizedTrack(LinkedList qrs, boolean if (includeExtendedData) { ExtendedData extendedData = t.createAndSetExtendedData(); - SchemaData schemaData = extendedData.createAndAddSchemaData(); - + + SchemaData schemaData = new SchemaData(); + extendedData.getSchemaData().add(schemaData); + schemaData.setSchemaUrl("#trackschema"); speed = new SimpleArrayData(); @@ -507,9 +513,9 @@ public static Placemark buildOptimizedTrack(LinkedList qrs, boolean ce.setName("ce"); le.setName("le"); - schemaData.addToSchemaDataExtension(speed); - schemaData.addToSchemaDataExtension(ce); - schemaData.addToSchemaDataExtension(le); + schemaData.getSchemaDataExtension().add(speed); + schemaData.getSchemaDataExtension().add(ce); + schemaData.getSchemaDataExtension().add(le); } lastReportTime = qr.servertime; } else { @@ -924,9 +930,15 @@ public static List trackToCot(Track track) { ExtendedData extendedData = track.getExtendedData(); SchemaData schemaData = extendedData.getSchemaData().get(0); - - for (SimpleArrayData simpleArrayData : schemaData.getSchemaDataExtension()) { - + + for (Object object : schemaData.getSchemaDataExtension()) { + + if (!(object instanceof SimpleArrayData)) { + continue; + } + + SimpleArrayData simpleArrayData = (SimpleArrayData) object; + if (simpleArrayData.getValue().size() != size) { log.severe("trackKmlToCot: track array size mismatch!"); return null; diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/VersionBean.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/VersionBean.java index 6a7c0068..abdcaa85 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/VersionBean.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/VersionBean.java @@ -20,9 +20,9 @@ public class VersionBean { - private String ver = null; + private volatile String ver = null; - private VersionInfo versionInfo = null; + private volatile VersionInfo versionInfo = null; Logger logger = LoggerFactory.getLogger(VersionBean.class); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/missionpackage/MissionPackage.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/missionpackage/MissionPackage.java index e23f10a9..6ff580dc 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/missionpackage/MissionPackage.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/missionpackage/MissionPackage.java @@ -152,7 +152,9 @@ public static HashMap extractMissionPackage(byte[] missionPackag bos.flush(); bos.close(); - files.put(entry.getName(), bos.toByteArray()); + String filename = entry.getName(); + filename = filename.substring(filename.lastIndexOf("/") + 1); + files.put(filename, bos.toByteArray()); } zis.close(); diff --git a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/spring/MissionRoleAssignmentRequestHolderFilterBean.java b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/spring/MissionRoleAssignmentRequestHolderFilterBean.java index 42c5525c..c9420e68 100644 --- a/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/spring/MissionRoleAssignmentRequestHolderFilterBean.java +++ b/src/takserver-core/takserver-war/src/main/java/com/bbn/marti/util/spring/MissionRoleAssignmentRequestHolderFilterBean.java @@ -4,15 +4,6 @@ import java.net.URLDecoder; import java.util.UUID; -import jakarta.servlet.FilterChain; -import jakarta.servlet.ServletException; -import jakarta.servlet.ServletRequest; -import jakarta.servlet.ServletResponse; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; - -import com.bbn.marti.config.Network; -import com.bbn.marti.remote.config.CoreConfigFacade; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.MDC; @@ -21,6 +12,7 @@ import org.springframework.web.filter.GenericFilterBean; import com.bbn.marti.logging.AuditLogUtil; +import com.bbn.marti.remote.config.CoreConfigFacade; import com.bbn.marti.remote.exception.MissionDeletedException; import com.bbn.marti.remote.exception.NotFoundException; import com.bbn.marti.sync.model.Mission; @@ -29,6 +21,13 @@ import com.bbn.marti.util.CommonUtil; import com.google.common.base.Strings; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.ServletRequest; +import jakarta.servlet.ServletResponse; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + @Order(0) public class MissionRoleAssignmentRequestHolderFilterBean extends GenericFilterBean { private static final Logger logger = LoggerFactory.getLogger(MissionRoleAssignmentRequestHolderFilterBean.class); @@ -44,6 +43,7 @@ public class MissionRoleAssignmentRequestHolderFilterBean extends GenericFilterB private final String apiMissions = "/api/missions/"; private final String copMissions = "/api/cops/"; + private final String DELETE_PATH = "/Marti/api/missions"; @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { @@ -75,141 +75,170 @@ public void doFilter(ServletRequest servletRequest, ServletResponse servletRespo apiPath = copMissions; } - if (logger.isDebugEnabled()) { - - // NB this can act as a request logger - logger.debug("path: " + path); - } + // NB this can act as a request logger + logger.debug("path: {}", path); if (CoreConfigFacade.getInstance().getRemoteConfiguration().getLogging() != null && CoreConfigFacade.getInstance().getRemoteConfiguration().getLogging().isAuditLoggingEnabled()) { AuditLogUtil.setMdc(req, resp); - logger.info("doFilter request path: " + path); + logger.info("doFilter request path: {}", path); } - - if (missionStart != -1) { - - boolean missionCreate = false; - - int missionEnd = path.indexOf("/", missionStart + apiPath.length()); - if (missionEnd == -1) { - missionEnd = path.length(); - - if (req.getMethod().equals("PUT") || req.getMethod().equals("POST") - || (req.getMethod().equals("OPTIONS") && CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().isAllowAllOrigins())) { - missionCreate = true; + + // Mission delete by guid + if (req.getMethod().equals("DELETE") && path.equals(DELETE_PATH)) { + logger.debug("DELETE mission {} {} ", req.getQueryString(), req.getParameter("guid")); + + UUID missionGuid = null; + + if (req.getParameter("guid") != null && (req.getParameter("guid") instanceof String)) { + + try { + logger.debug("process DELETE mission for guid {}", req.getParameter("guid")); + + missionGuid = UUID.fromString((String) req.getParameter("guid")); + + Mission mission = missionService.getMissionNoContentByGuid(missionGuid, martiUtil.getGroupVectorBitString(req.getSession().getId())); + + setMissionRole(mission, req, servletResponse, false); + + } catch (IllegalArgumentException e) { + logger.error("Invalid mission guid in mission delete request"); } } + + } else { + if (missionStart != -1) { + + boolean missionCreate = false; - String missionName = path.substring(missionStart + apiPath.length(), missionEnd); - if (missionName != null && !missionName.isEmpty()) { + int missionEnd = path.indexOf("/", missionStart + apiPath.length()); + if (missionEnd == -1) { + missionEnd = path.length(); - missionName = missionService.trimName(missionName); - missionName = URLDecoder.decode(missionName, "UTF-8"); + if (req.getMethod().equals("PUT") || req.getMethod().equals("POST") + || (req.getMethod().equals("OPTIONS") && CoreConfigFacade.getInstance().getRemoteConfiguration().getNetwork().isAllowAllOrigins())) { + missionCreate = true; + } + } - // - // ignore /missions endpoints that don't refer to an individual mission - // - if (missionName.compareTo("all") != 0 - && missionName.compareTo("logs") != 0 - && missionName.compareTo("invitations") != 0 - && missionName.compareTo("hierarchy") != 0) { + String missionName = path.substring(missionStart + apiPath.length(), missionEnd); + if (missionName != null && !missionName.isEmpty()) { - String missionGuid = null; + missionName = missionService.trimName(missionName); + missionName = URLDecoder.decode(missionName, "UTF-8"); - try { - String[] parts = path.split("/", 0); + // + // ignore /missions endpoints that don't refer to an individual mission + // + if (missionName.compareTo("all") != 0 + && missionName.compareTo("logs") != 0 + && missionName.compareTo("invitations") != 0 + && missionName.compareTo("hierarchy") != 0) { - logger.debug("path parts: {}", parts.length); + String missionGuid = null; - if (parts.length > 5) { - missionGuid = parts[5]; - } - } catch (Exception e) { - logger.warn("error getting mission guid from path", e); - } + try { + String[] parts = path.split("/", 0); - logger.debug("mission guid {}", missionGuid); + logger.debug("path parts: {}", parts.length); - // - // get the mission - // - Mission mission = null; - - logger.debug("mission name (can be guid) : {}", missionName); + if (parts.length > 5) { + missionGuid = parts[5]; + } + } catch (Exception e) { + logger.warn("error getting mission guid from path", e); + } - try { + logger.debug("mission guid {}", missionGuid); - // for guid case, the missonName looks like 'guid' here - if (missionName != null && missionName.toLowerCase().equals("guid") && !Strings.isNullOrEmpty(missionGuid)) { + try { + // + // get the mission + // + Mission mission = null; - logger.debug("getting mission by guid {}", missionGuid); + logger.debug("mission name (can be guid) : {}", missionName); - UUID missionUuid = UUID.fromString(missionGuid); + // for guid case, the missonName looks like 'guid' here + if (missionName != null && missionName.toLowerCase().equals("guid") && !Strings.isNullOrEmpty(missionGuid)) { - mission = missionService.getMissionNoContentByGuid(missionUuid, martiUtil.getGroupVectorBitString(req.getSession().getId())); - } else { + logger.debug("getting mission by guid {}", missionGuid); - logger.debug("getting mission by name {}", missionName); + UUID missionUuid = UUID.fromString(missionGuid); - mission = missionService.getMissionNoContent(missionName, martiUtil.getGroupVectorBitString(req.getSession().getId())); - } + mission = missionService.getMissionNoContentByGuid(missionUuid, martiUtil.getGroupVectorBitString(req.getSession().getId())); + } else { - MissionRole role = missionService.getRoleForRequest(mission, req); + logger.debug("getting mission by name {}", missionName); - if (logger.isDebugEnabled()) { - logger.debug("assigned role: " + role); - } + mission = missionService.getMissionNoContent(missionName, martiUtil.getGroupVectorBitString(req.getSession().getId())); + } - requestBean.setMissionRole(role); + setMissionRole(mission, req, servletResponse, missionCreate); - req.setAttribute(MissionRole.class.getName(), role); + } catch (NotFoundException nfe) { + if (logger.isDebugEnabled()) { + logger.debug("mission " + missionName + " not found - not assigning role"); + } - if (CoreConfigFacade.getInstance().getRemoteConfiguration().getVbm().isEnabled()) { - if (!missionService.validateAccess(mission, req)) { - ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_NOT_FOUND); + if (!missionCreate) { + ((HttpServletResponse)servletResponse).setStatus(HttpServletResponse.SC_NOT_FOUND); return; } - } - - } catch (NotFoundException nfe) { - if (logger.isDebugEnabled()) { - logger.debug("mission " + missionName + " not found - not assigning role"); - } + } catch (IllegalArgumentException e) { - if (!missionCreate) { - ((HttpServletResponse)servletResponse).setStatus(HttpServletResponse.SC_NOT_FOUND); - return; - } - } catch (MissionDeletedException mde) { - logger.warn("attempt to access a deleted mission : " + missionName); + if (CoreConfigFacade.getInstance().getRemoteConfiguration().getLogging().isAuditLoggingEnabled()) { + logger.error("invalid mission UUID: " + missionGuid); + } - if (!missionCreate) { - ((HttpServletResponse)servletResponse).setStatus(HttpServletResponse.SC_GONE); - return; - } - } catch (IllegalArgumentException e) { + throw new IllegalArgumentException("invalid mission UUID"); + } catch (MissionDeletedException mde) { + logger.warn("attempt to access a deleted mission : " + missionName); - if (CoreConfigFacade.getInstance().getRemoteConfiguration().getLogging().isAuditLoggingEnabled()) { - logger.error("invalid mission UUID: " + missionGuid); + if (!missionCreate) { + ((HttpServletResponse)servletResponse).setStatus(HttpServletResponse.SC_GONE); + return; + } } - - throw new IllegalArgumentException("invalid mission UUID"); - } catch (Exception e) { - logger.warn("exception assigning mission role", e); } } } } - if (logger.isDebugEnabled()) { - logger.debug("RequestHolderFilterBean doFilter: " + servletRequest); - } - + logger.debug("RequestHolderFilterBean doFilter: {}", servletRequest); + try { filterChain.doFilter(servletRequest, servletResponse); } finally { MDC.clear(); } } + + private void setMissionRole(Mission mission, HttpServletRequest req, ServletResponse servletResponse, boolean missionCreate) { + + if (mission == null) { + logger.warn("null mission"); + return; + } + + try { + + MissionRole role = missionService.getRoleForRequest(mission, req); + + logger.debug("assigned mission role: {} for mission ", role); + + requestBean.setMissionRole(role); + + req.setAttribute(MissionRole.class.getName(), role); + + if (CoreConfigFacade.getInstance().getRemoteConfiguration().getVbm().isEnabled()) { + if (!missionService.validateAccess(mission, req)) { + ((HttpServletResponse) servletResponse).setStatus(HttpServletResponse.SC_NOT_FOUND); + return; + } + } + } catch (Exception e) { + logger.warn("exception assigning mission role", e); + } + } } \ No newline at end of file diff --git a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/ActiveGroupCacheHelper.java b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/ActiveGroupCacheHelper.java index 30f16caf..c065a119 100644 --- a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/ActiveGroupCacheHelper.java +++ b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/ActiveGroupCacheHelper.java @@ -91,6 +91,7 @@ private synchronized IgniteCache getActiveGroupsCache() { return activeGroupCache; } + Date start = new Date(); logger.info("Populating the activeGroupCache"); Map> activeGroups = loadActiveGroups(); @@ -109,6 +110,8 @@ private synchronized IgniteCache getActiveGroupsCache() { logger.error("loadActiveGroups failed!"); } + logger.info("activeGroupCache cache warmed - took " + ((new Date().getTime() - start.getTime()) / 1000) + " seconds"); + return activeGroupCache; } @@ -215,7 +218,8 @@ public boolean assignGroupsCheckCache(Set groups, User user, String usern activeGroups.addAll(adds); for (Group group : adds) { - group.setActive(false); + group.setActive(CoreConfigFacade.getInstance().getRemoteConfiguration() + .getAuth().isX509UseGroupCacheDefaultActive()); } // if we only have one group, make sure its active diff --git a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/DataFeedCotCacheHelper.java b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/DataFeedCotCacheHelper.java index 63451c92..bfd4fd6b 100644 --- a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/DataFeedCotCacheHelper.java +++ b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/DataFeedCotCacheHelper.java @@ -22,7 +22,7 @@ public class DataFeedCotCacheHelper { private static final Logger logger = Logger.getLogger(DataFeedCotCacheHelper.class); - private static DataFeedCotCacheHelper instance; + private volatile static DataFeedCotCacheHelper instance; public static DataFeedCotCacheHelper getInstance() { if (instance == null) { synchronized (DataFeedCotCacheHelper.class) { @@ -34,7 +34,7 @@ public static DataFeedCotCacheHelper getInstance() { return instance; } - private Map> dataFeedCaches = new HashMap<>(); + private volatile Map> dataFeedCaches = new HashMap<>(); private Cache latestSACacheForDataFeed(DataFeed dataFeed) { Cache cache = dataFeedCaches.get(dataFeed.getUuid()); if (cache == null) { diff --git a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/MissionCacheHelper.java b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/MissionCacheHelper.java index be173309..93628ad6 100644 --- a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/MissionCacheHelper.java +++ b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/MissionCacheHelper.java @@ -159,6 +159,8 @@ public Mission getMissionByGuid(UUID guid, boolean hydrateDetails, boolean skipC } mission = doMissionQueryGuid(guid, hydrateDetails); + + logger.debug("Mission from doMissionQueryGuid {} {}", guid, mission); if (mission != null) { @@ -217,9 +219,7 @@ private Mission doMissionQueryGuid(UUID guid, boolean hydrateDetails) { Mission mission = missionRepository.getByGuidNoCache(guid); - if (logger.isTraceEnabled()) { - logger.trace("mission {} : {} ", guid, mission); - } + logger.debug("doMissionQueryGuid mission before hydrate {} : {} ", guid, mission); if (mission != null) { missionService.hydrate(mission, hydrateDetails); @@ -229,6 +229,9 @@ private Mission doMissionQueryGuid(UUID guid, boolean hydrateDetails) { } } + logger.debug("doMissionQueryGuid mission after hydrate {} : {} ", guid, mission); + + return mission; } diff --git a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/resolvers/MissionCacheResolver.java b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/resolvers/MissionCacheResolver.java index 7bf1aac2..aac45541 100644 --- a/src/takserver-core/takserver-war/src/main/java/tak/server/cache/resolvers/MissionCacheResolver.java +++ b/src/takserver-core/takserver-war/src/main/java/tak/server/cache/resolvers/MissionCacheResolver.java @@ -59,7 +59,11 @@ public Collection resolveCaches(CacheOperationInvocationContext cacheNameMissionName = ((String) context.getArgs()[1].toString()); } } - + + if (cacheNameMissionName != null) { + cacheNameMissionName = cacheNameMissionName.toLowerCase(); + } + List caches = new CopyOnWriteArrayList<>(); if (cacheNameMissionGuid != null) { diff --git a/src/takserver-core/takserver-war/src/main/java/tak/server/plugins/PluginDataApi.java b/src/takserver-core/takserver-war/src/main/java/tak/server/plugins/PluginDataApi.java index 09988587..5477fcaa 100644 --- a/src/takserver-core/takserver-war/src/main/java/tak/server/plugins/PluginDataApi.java +++ b/src/takserver-core/takserver-war/src/main/java/tak/server/plugins/PluginDataApi.java @@ -181,13 +181,13 @@ public ResponseEntity requestFromPlugin( // TODO: handle IllegalArgumentException better try { - String contentType = requestHolderBean.getRequest().getHeader("content-type"); + String accept = requestHolderBean.getRequest().getHeader("accept"); logger.info("submit " + pluginClassName); - logger.info("content type: " + contentType); + logger.info("accept " + accept); try { - result = pluginManager.requestDataFromPlugin(pluginClassName, allRequestParams, contentType); + result = pluginManager.requestDataFromPlugin(pluginClassName, allRequestParams, accept); } catch (Exception e) { throw new TakException("error accesing PluginManager process - is it running?", e); } diff --git a/src/takserver-core/takserver-war/src/main/java/tak/server/system/ApiDependencyProxy.java b/src/takserver-core/takserver-war/src/main/java/tak/server/system/ApiDependencyProxy.java index 84aef671..e021b08d 100644 --- a/src/takserver-core/takserver-war/src/main/java/tak/server/system/ApiDependencyProxy.java +++ b/src/takserver-core/takserver-war/src/main/java/tak/server/system/ApiDependencyProxy.java @@ -22,7 +22,7 @@ public class ApiDependencyProxy implements ApplicationContextAware { private static ApplicationContext springContext; - private static ApiDependencyProxy instance = null; + private static volatile ApiDependencyProxy instance = null; public static ApiDependencyProxy getInstance() { if (instance == null) { @@ -46,7 +46,7 @@ public void setApplicationContext(ApplicationContext context) throws BeansExcept this.springContext = context; } - private ServerInfo serverInfo = null; + private volatile ServerInfo serverInfo = null; public ServerInfo serverInfo() { if (serverInfo == null) { @@ -60,7 +60,7 @@ public ServerInfo serverInfo() { return serverInfo; } - private MissionRoleRepository missionRoleRepository = null; + private volatile MissionRoleRepository missionRoleRepository = null; public MissionRoleRepository missionRoleRepository() { if (missionRoleRepository == null) { @@ -74,7 +74,7 @@ public MissionRoleRepository missionRoleRepository() { return missionRoleRepository; } - private EnterpriseSyncService enterpriseSyncService = null; + private volatile EnterpriseSyncService enterpriseSyncService = null; public EnterpriseSyncService enterpriseSyncService() { if (enterpriseSyncService == null) { @@ -88,7 +88,7 @@ public EnterpriseSyncService enterpriseSyncService() { return enterpriseSyncService; } - private CoTCacheHelper cotCacheHelper = null; + private volatile CoTCacheHelper cotCacheHelper = null; public CoTCacheHelper cotCacheHelper() { if (cotCacheHelper == null) { @@ -102,7 +102,7 @@ public CoTCacheHelper cotCacheHelper() { return cotCacheHelper; } - private MissionRepository missionRepository = null; + private volatile MissionRepository missionRepository = null; public MissionRepository missionRepository() { if (missionRepository == null) { @@ -116,7 +116,7 @@ public MissionRepository missionRepository() { return missionRepository; } - private SubscriptionManagerLite subscriptionManagerLite = null; + private volatile SubscriptionManagerLite subscriptionManagerLite = null; public SubscriptionManagerLite subscriptionManagerLite() { if (subscriptionManagerLite == null) { @@ -130,7 +130,7 @@ public SubscriptionManagerLite subscriptionManagerLite() { return subscriptionManagerLite; } - private MissionService missionService = null; + private volatile MissionService missionService = null; public MissionService missionService() { if (missionService == null) { @@ -144,7 +144,7 @@ public MissionService missionService() { return missionService; } - private GroupManager groupManager = null; + private volatile GroupManager groupManager = null; public GroupManager groupManager() { if (groupManager == null) { @@ -158,7 +158,7 @@ public GroupManager groupManager() { return groupManager; } - private PluginManager pluginManager = null; + private volatile PluginManager pluginManager = null; public PluginManager pluginManager() { if (pluginManager == null) { @@ -172,7 +172,7 @@ public PluginManager pluginManager() { return pluginManager; } - private CommonUtil commonUtil = null; + private volatile CommonUtil commonUtil = null; public CommonUtil commonUtil() { if (commonUtil == null) { diff --git a/src/takserver-fig-core/build.gradle b/src/takserver-fig-core/build.gradle index 24c1ccea..29960677 100644 --- a/src/takserver-fig-core/build.gradle +++ b/src/takserver-fig-core/build.gradle @@ -43,15 +43,12 @@ apply plugin: 'eclipse' // This should be fixed in Gradle 7.2 according to // https://github.com/gradle/gradle/issues/4505, but that's a larger task than // A five minute work-around. -if ("$System.env.TAK_GRADLE_CI_MODE" != "true") { - println("NOT USING CI") - idea { - module { - // Not using generatedSourceDirs because of - // https://discuss.gradle.org/t/support-for-intellij-2016/15294/8 - sourceDirs += file("${projectDir}/build/generated/source/proto/main/java") - sourceDirs += file("${projectDir}/build/generated/source/proto/main/grpc") - } +idea { + module { + // Not using generatedSourceDirs because of + // https://discuss.gradle.org/t/support-for-intellij-2016/15294/8 + sourceDirs += file("${projectDir}/build/generated/source/proto/main/java") + sourceDirs += file("${projectDir}/build/generated/source/proto/main/grpc") } } diff --git a/src/takserver-fig-core/src/main/java/com/bbn/roger/fig/FederationUtils.java b/src/takserver-fig-core/src/main/java/com/bbn/roger/fig/FederationUtils.java index 73ee07f8..74c6fd1f 100644 --- a/src/takserver-fig-core/src/main/java/com/bbn/roger/fig/FederationUtils.java +++ b/src/takserver-fig-core/src/main/java/com/bbn/roger/fig/FederationUtils.java @@ -13,14 +13,14 @@ import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; -import jakarta.xml.bind.DatatypeConverter; - import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.hash.HashCode; import com.google.common.hash.HashFunction; import com.google.common.hash.Hashing; +import jakarta.xml.bind.DatatypeConverter; + public class FederationUtils { private static final String CONNECTION_REFUSED_MSG = "connection refused, check network connectivity to federate"; diff --git a/src/takserver-package/API/build.gradle b/src/takserver-package/API/build.gradle index 58e2166b..a4d1680a 100644 --- a/src/takserver-package/API/build.gradle +++ b/src/takserver-package/API/build.gradle @@ -97,16 +97,12 @@ ospackage { } } -task prePackage { } -prePackage.dependsOn copyJarsNoDb -prePackage.dependsOn copyWars -prePackage.dependsOn copyCoreConfigXSD -prePackage.dependsOn copyCoreConfigExample -prePackage.dependsOn copyAPIScripts -prePackage.dependsOn copyDocs -prePackage.dependsOn copyPolicy -prePackage.dependsOn copyLicense - +task prePackage(type: Copy) { task -> + with takserverCommonSpec(task) + with copyCoreConfigExampleSpec() + with copyAPIScriptsSpec() + destinationDir file("$buildDir/takArtifacts") +} buildRpm.dependsOn prePackage diff --git a/src/takserver-package/build.gradle b/src/takserver-package/build.gradle index 11641d0d..7c87834e 100644 --- a/src/takserver-package/build.gradle +++ b/src/takserver-package/build.gradle @@ -255,26 +255,27 @@ ospackage { -task prePackage { } -prePackage.dependsOn copyJars -prePackage.dependsOn copyWars -prePackage.dependsOn copyCoreConfigXSD -prePackage.dependsOn copyCoreConfigExample -prePackage.dependsOn copyCoreScripts -prePackage.dependsOn copyDbScripts -prePackage.dependsOn copyDocs -prePackage.dependsOn copySwagger -prePackage.dependsOn copyPolicy -prePackage.dependsOn copyLicense -prePackage.dependsOn copyConfigScripts -prePackage.dependsOn copyAPIScripts -prePackage.dependsOn copyPluginsJar -prePackage.dependsOn copyPluginsScripts -prePackage.dependsOn copyRetentionConfigs -prePackage.dependsOn copyRetentionJar -prePackage.dependsOn copyRetentionMissionStoreDir -prePackage.dependsOn copyRetentionScripts -prePackage.dependsOn copySchemaManagerJar +task prePackage(type: Copy) { task -> + with takserverCommonSpec(task) + with copySchemaManagerJarSpec(task) + with copyCoreConfigExampleSpec() + with copyCoreScriptsSpec() + with copyDbScriptsSpec() + with copySwaggerSpec() + with copyConfigScriptsSpec() + with copyAPIScriptsSpec() + with copyFullPluginManagerSpec(task) + with copyFullRetentionServiceSpec(task) + + // Ideally deconfliction would be done, but that requires more time + duplicatesStrategy DuplicatesStrategy.INCLUDE + destinationDir file("$buildDir/takArtifacts") + + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') +} buildRpm.dependsOn prePackage @@ -375,6 +376,11 @@ task buildHardenedFullDocker { dependsOn constructHardenedFullDockerZip } +task buildIntegrationTester { + description = 'Builds takserver-integration-tester-' + takversion + '-' + takreleaserpm + '.zip' + dependsOn constructIntegrationTesterZip +} + task createCIVersionArtifact() { // The docker tags, starting with the unique primary docker tag def primaryDockerTag = (takversion + '-' + takreleaserpm).toLowerCase() @@ -406,3 +412,9 @@ task createCIVersionArtifact() { "TAK_RPM_VERSION=\"$takversion-$takreleaserpm\"\n" + "TAK_DOCKER_PRIMARY_TAG=\"$primaryDockerTag\"\n" } + +task buildAll(type: DefaultTask) { + dependsOn('buildRpm', 'buildDeb', 'buildDocker', 'buildFullDocker', 'buildHardenedDocker', + 'buildHardenedFullDocker', 'buildFedhubDocker', ':takserver-cluster:buildCluster', 'bundlePublishArtifacts', + ':takserver-takcl-core:publicJar', ':takserver-plugins:publish') +} diff --git a/src/takserver-package/database/build.gradle b/src/takserver-package/database/build.gradle index 4f9d651e..9d2e074d 100644 --- a/src/takserver-package/database/build.gradle +++ b/src/takserver-package/database/build.gradle @@ -143,17 +143,16 @@ ospackage { } } -task prePackageDbOnlyScripts { } -prePackageDbOnlyScripts.dependsOn copyDbOnlyScripts - +task prePackage(type: Copy) { task -> + with copySchemaManagerJarSpec(task) + with copyDbScriptsSpec().exclude('pg_hba.conf', 'postgresql.conf') + with copyDbOnlyScriptsSpec() + with copyDocsSpec() + with copyLicenseSpec() + destinationDir = file("$buildDir/takArtifacts") +} -task prePackage { } -prePackage.dependsOn prePackageDbOnlyScripts -prePackage.dependsOn copySchemaManagerJar prePackage.dependsOn copyCoreConfigExampleOnly -prePackage.dependsOn copyDbScripts -prePackage.dependsOn copyDocs -prePackage.dependsOn copyLicense buildRpm.dependsOn prePackage diff --git a/src/takserver-package/federation-hub/build.gradle b/src/takserver-package/federation-hub/build.gradle index b9ebb2f6..145b1fa8 100644 --- a/src/takserver-package/federation-hub/build.gradle +++ b/src/takserver-package/federation-hub/build.gradle @@ -34,6 +34,7 @@ task copyFedHubJars(type: Copy) { dependsOn(':federation-hub-broker:build') dependsOn(':federation-hub-policy:build') dependsOn(':federation-hub-ui:bootWar') + dependsOn(':federation-hub-ui:bootJar') dependsOn(':federation-hub-ui:shadowJar') from getRootProject().subprojects.collect { "${it.buildDir}/libs" } @@ -149,23 +150,12 @@ cp /opt/tak/federation-hub/scripts/federation-hub-broker /etc/init.d cp /opt/tak/federation-hub/scripts/federation-hub-policy /etc/init.d cp /opt/tak/federation-hub/scripts/federation-hub-ui /etc/init.d -# Add mongo repo to yum -cp /opt/tak/federation-hub/scripts/db/mongodb-org.repo /etc/yum.repos.d/mongodb-org.repo # Set up logging directory. mkdir -p /opt/tak/federation-hub/logs chown tak:tak /opt/tak/federation-hub/logs chmod 755 /opt/tak/federation-hub/logs -# Change ownership of configuration files. -if [ -f /opt/tak/CoreConfig.xml ] ; then - chown -f tak:tak /opt/tak/CoreConfig.xml 2>/dev/null -fi - -if [ -d "/opt/tak/webcontent" ] ; then - chown -fR tak:tak /opt/tak/webcontent -fi - cat <<- "EOF" TAK SERVER FEDERATION HUB SOFTWARE LICENSE AGREEMENT @@ -253,14 +243,17 @@ with or without a co-located TAK server. } } -task prePackage { } +task prePackage(type: Copy) { task -> + with copyLicenseSpec() + destinationDir file("$buildDir/takArtifacts") +} + prePackage.dependsOn copyFedHubReadme prePackage.dependsOn copyFedHubDockerReadme prePackage.dependsOn copyFedHubConfigs prePackage.dependsOn copyFedHubJars prePackage.dependsOn copyFedHubScripts prePackage.dependsOn copyCertScripts -prePackage.dependsOn copyLicense prePackage.dependsOn copySELinuxPolicy prePackage.dependsOn copySELinuxScript @@ -276,9 +269,6 @@ buildDeb { requires('openjdk-17-jdk') } -def copyFedhubVersionFileDef() { return copySpec { - from 'build/takArtifacts/version.txt' -}} def copyFedhubResourcesDef() { return copySpec { from 'build/artifacts' }} @@ -301,20 +291,13 @@ task createFedhubDockerLogDir() { } } -task deleteNonDockerFedhubBrokerConfig(type : Delete) { - delete "$buildDir/artifacts/configs/federation-hub-broker.yml" - delete "$buildDir/artifacts/configs/federation-hub-broker-docker.yml" -} - -def copyFedhubVersionFile = copyFedhubVersionFileDef() def copyFedhubResources = copyFedhubResourcesDef() def copyFedhubDocker = copyFedhubDockerDef() def copyFedhubDockerScript = copyFedhubDockerScriptDef() def copyCertsSetup = copyCertsSetupDef() def copyDocsToTopLevel = copyDocsToTopLevelDef() -task constructFedhubDockerZip(type: Zip) { - dependsOn createVersionFile +task constructFedhubDockerZip(type: Zip) { Task task -> dependsOn createFedhubDockerLogDir dependsOn buildRpm @@ -323,18 +306,18 @@ task constructFedhubDockerZip(type: Zip) { destinationDir(file("$buildDir/distributions")) into('takserver-fedhub-docker-' + version) - with copyFedhubVersionFile.into('tak') - with copyFedhubResources.into('tak/federation-hub') + with copyVersionFileSpec(task).into('tak') + with copyFedhubResources.exclude("configs/federation-hub-broker.yml", "configs/federation-hub-broker-docker.yml") + .into('tak/federation-hub') with copyFedhubDockerScript.into('tak/federation-hub/scripts') with copyFedhubDocker.into('docker') with copyCertsSetup.into('tak/certs') with copyDocsToTopLevel.into('./') - dependsOn deleteNonDockerFedhubBrokerConfig - from(project(':federation-hub-broker').file('src/main/resources')) { include('federation-hub-broker-docker.yml') rename('federation-hub-broker-docker.yml', 'federation-hub-broker.yml') into('tak/federation-hub/configs') } + } diff --git a/src/takserver-package/launcher/build.gradle b/src/takserver-package/launcher/build.gradle index 1569a50c..f18b6af9 100644 --- a/src/takserver-package/launcher/build.gradle +++ b/src/takserver-package/launcher/build.gradle @@ -232,24 +232,26 @@ ospackage { } } -task prePackage { } -prePackage.dependsOn copyJarsNoDb -prePackage.dependsOn copyWars -prePackage.dependsOn copyPluginsJar -prePackage.dependsOn copyRetentionJar -prePackage.dependsOn copyCoreConfigXSD -prePackage.dependsOn copyCoreConfigExample -prePackage.dependsOn copyConfigScripts -prePackage.dependsOn copyMessagingScripts -prePackage.dependsOn copyAPIScripts -prePackage.dependsOn copyPluginsScripts -prePackage.dependsOn copyRetentionConfigs -prePackage.dependsOn copyRetentionMissionStoreDir -prePackage.dependsOn copyRetentionScripts +task prePackage(type: Copy) { task -> + with takserverCommonSpec(task) + with copyFullPluginManagerSpec(task) + with copyFullRetentionServiceSpec(task) + with copyCoreConfigExampleSpec() + with copyConfigScriptsSpec() + with copyMessagingScriptsSpec() + with copyAPIScriptsSpec() + + // Ideally deconfliction would be done, but that requires more time + duplicatesStrategy DuplicatesStrategy.INCLUDE + destinationDir = file("$buildDir/takArtifacts") + + + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') +} prePackage.dependsOn copyLauncherScripts -prePackage.dependsOn copyDocs -prePackage.dependsOn copyPolicy -prePackage.dependsOn copyLicense buildRpm.dependsOn prePackage diff --git a/src/takserver-package/messaging/build.gradle b/src/takserver-package/messaging/build.gradle index 1bb633e9..78e8cece 100644 --- a/src/takserver-package/messaging/build.gradle +++ b/src/takserver-package/messaging/build.gradle @@ -94,15 +94,12 @@ ospackage { } } -task prePackage { } -prePackage.dependsOn copyJarsNoDb -prePackage.dependsOn copyWars -prePackage.dependsOn copyCoreConfigXSD -prePackage.dependsOn copyCoreConfigExample -prePackage.dependsOn copyMessagingScripts -prePackage.dependsOn copyDocs -prePackage.dependsOn copyPolicy -prePackage.dependsOn copyLicense +task prePackage(type: Copy) { task -> + with takserverCommonSpec(task) + with copyCoreConfigExampleSpec() + with copyMessagingScriptsSpec() + destinationDir = file("$buildDir/takArtifacts") +} buildRpm.dependsOn prePackage diff --git a/src/takserver-package/takserver/build.gradle b/src/takserver-package/takserver/build.gradle index aa5b6f04..80d37158 100644 --- a/src/takserver-package/takserver/build.gradle +++ b/src/takserver-package/takserver/build.gradle @@ -216,29 +216,28 @@ ospackage { } } -task prePackage { } -prePackage.dependsOn copyJars -prePackage.dependsOn copyWars -prePackage.dependsOn copyCoreConfigXSD -prePackage.dependsOn copyCoreConfigExample -prePackage.dependsOn copyConfigScripts -prePackage.dependsOn copyMessagingScripts -prePackage.dependsOn copyAPIScripts -prePackage.dependsOn copyPluginsJar -prePackage.dependsOn copyPluginsScripts -prePackage.dependsOn copyRetentionJar -prePackage.dependsOn copyRetentionConfigs -prePackage.dependsOn copyRetentionMissionStoreDir -prePackage.dependsOn copyRetentionScripts -prePackage.dependsOn copyLauncherScripts -prePackage.dependsOn copyHealthCheckScripts -prePackage.dependsOn copyDockerSecurityFiles -prePackage.dependsOn copyDbScripts -prePackage.dependsOn copyDocs -prePackage.dependsOn copyPolicy -prePackage.dependsOn copyLicense - - +task prePackage(type: Copy) { task -> + with takserverCommonSpec(task) + with copyCoreConfigExampleSpec() + with copyConfigScriptsSpec() + with copyMessagingScriptsSpec() + with copyAPIScriptsSpec() + with copyFullPluginManagerSpec(task) + with copyFullRetentionServiceSpec(task) + with copySchemaManagerJarSpec(task) + dependsOn copyLauncherScripts + with copyDbScriptsSpec() + + // Ideally deconfliction would be done, but that requires more time + duplicatesStrategy DuplicatesStrategy.INCLUDE + destinationDir = file("$buildDir/takArtifacts") + + + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') +} buildRpm.dependsOn prePackage diff --git a/src/takserver-package/utils/copyspecs-base.gradle b/src/takserver-package/utils/copyspecs-base.gradle new file mode 100644 index 00000000..4d02616c --- /dev/null +++ b/src/takserver-package/utils/copyspecs-base.gradle @@ -0,0 +1,269 @@ +/** + * Base deployment copy specs. These are intended to be applied to artifacts that do not involve containerization or + * other modifications beyond the base configuration. Any that are adapted are stored along with the adapted copy specs + * in copyspecs-composite.gradle for ease of comparison. + */ + +task createDockerDirs() { + doFirst { + mkdir "$buildDir/takDockerDirs/logs" + mkdir "$buildDir/takDockerDirs/lib" + mkdir "$buildDir/takDockerDirs/webcontent/webtak-plugins" + new File("$buildDir/takDockerDirs/logs/takserver.log").text = "" + new File("$buildDir/takDockerDirs/version.txt").text = """$version""" + } +} + +task createVersionFile() { + doFirst { + mkdir "$buildDir/takVersionDir" + new File("$buildDir/takVersionDir/version.txt").text = """$version""" + } +} + +ext { + + // Required base takserver component executable files + copyBaseTakJarsAndWarsSpec = { task -> + task.dependsOn(':takserver-core:bootJar') + task.dependsOn(':takserver-core:bootWar') + task.dependsOn(':takserver-usermanager:shadowJar') + return copySpec { + with copySpec { + from getRootProject().subprojects.collect { it.tasks.withType(Jar) } + rename('UserManager-' + version + '-all.jar', 'utils/UserManager.jar') + exclude 'takserver-common*', 'takserver-core*', 'takserver-package*', 'API*', 'messaging*', + 'database*', 'takserver-schemamanager*', 'takserver-takcl*', 'takserver-usermanager*', + 'takserver-war-*', 'takcl*', 'takserver-plugin*', 'schemamanager*', + 'takserver-plugin-manager*', 'takserver-cluster*', 'takserver-fig-core*', 'takserver-retention*', + 'rol*', 'Periodic*', 'launcher*', 'takserver-' + version + '*', + 'federation-hub-*', 'federation-common-*', 'takserver-protobuf-*' + } + with copySpec { + from project(':takserver-core').collect { it.tasks.withType(War) } + rename('takserver-core-' + version + '.war', 'takserver.war') + exclude 'takserver-war*', 'takserver-war-*.war' + + } + } + } + + copySchemaManagerJarSpec = { Task task -> + task.dependsOn(':takserver-schemamanager:shadowJar') + return copySpec { + from project(':takserver-schemamanager').collect { it.tasks.withType(Jar) } + rename('schemamanager-' + version + '-uber.jar', 'db-utils/SchemaManager.jar') + exclude 'takserver-schemamanager*' + } + } + + copyLicenseSpec = { + return copySpec { + from("${project.rootDir}/../") + include 'LICENSE.txt' + } + } + + + // Copy db-utils scripts from dbonly directory + copyDbOnlyScriptsSpec = { + return copySpec { + from project(':takserver-schemamanager').file('scripts-dbonly') + rename('(.*)', 'db-utils/$1') + } + } + + // Get CoreConfig.xsd and other XSDs including the TAKIgniteConfig.xsd + copyCoreConfigXSDSpec = { + return copySpec { + from project(':takserver-common').file('src/main/xsd') + include '*.xsd' + } + } + + copyCoreScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'config*', 'utils', 'messaging*', 'API*', 'plugins*', 'retention*', "launcher*", 'takserver.sh' + } + } + + copyFullDockerScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'utils', 'plugins*', 'retention*', 'takserver.sh' + } + } + + copyConfigScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'utils*', 'messaging*', 'API*', 'plugins*', 'retention*', 'launcher*', 'takserver.sh' + } + } + + copyAPIScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'config*', 'utils*', 'messaging*', 'plugins*', 'retention*', 'launcher*', 'takserver.sh' + } + } + + copyMessagingScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'config*', 'utils*', 'API*', 'launcher*', 'plugins*', 'retention*', 'takserver.sh' + } + } + + copyLauncherAndConfigAndMessagingScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'utils*', 'API*', 'plugins*', 'retention*', 'takserver.sh' + } + } + + copyDocsSpec = { + return copySpec { + from getRootProject().file('docs') + include '*.pdf' + rename('(.*)', 'docs/$1') + } + } + + copySwaggerSpec = { + return copySpec { + from project(':takserver-core').file('oas') + include '*.html' + include '*.js' + rename('(.*)', 'oas/$1') + } + } + + copyPolicySpec = { + return copySpec { + from project(':takserver-core').file('scripts/utils') + include '*.te' + } + } + + copyFullRetentionServiceSpec = { task -> + task.dependsOn(':takserver-retention:bootJar') + return copySpec { + // Copy the retention config + with copySpec { + from project(':takserver-retention').file('conf/retention') + rename('(.*)', 'conf/retention/$1') + } + // Copy the mission store config + with copySpec { + from project(':takserver-retention').file('mission-archive/') + rename('(.*)', 'mission-archive/$1') + } + // Copy the Scripts + with copySpec { + from project(':takserver-core').file('scripts/retention/') + rename('(.*)', 'retention/$1') + } + // Add the jar build dependency and copy it + with copySpec { + from getRootProject().project(':takserver-retention').collect { it.tasks.withType(Jar) } + rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') + } + } + } + + copyFullFedhubSpec = { task -> + // Add jar task dependencies + task.dependsOn(':federation-hub-broker:build') + task.dependsOn(':federation-hub-policy:build') + task.dependsOn(':federation-hub-ui:bootWar') + task.dependsOn(':federation-hub-ui:shadowJar') + return copySpec { + // Add the jars + with copySpec { + from getRootProject().subprojects.collect { "${it.buildDir}/libs" } + include 'federation-hub-*-' + version + '.jar', 'federation-hub-ui-' + version + '.war' + rename 'federation-hub-broker-*' + version + '.jar', 'federation-hub/federation-hub-broker.jar' + rename 'federation-hub-policy-*' + version + '.jar', 'federation-hub/federation-hub-policy.jar' + rename 'federation-hub-ui-*' + version + '.war', 'federation-hub/federation-hub-ui.war' + rename 'federation-hub-manager-*' + version + '.jar', 'federation-hub/federation-hub-manager.jar' + } + // Add the configuration files + with copySpec { + from getRootProject().subprojects.collect { "${it.projectDir}/src/main/resources" } + include 'federation-hub-*.yml', 'logback-*.xml' + rename('(.*)', 'federation-hub/configs/$1') + } + } + } + + copyFedhubConfigsSpec = { + return copySpec { + from getRootProject().subprojects.collect { "${it.projectDir}/src/main/resources" } + include 'federation-hub-*.yml', 'logback-*.xml' + rename('(.*)', 'federation-hub/configs/$1') + } + } + + copyFullPluginManagerSpec = { task -> + task.dependsOn(':takserver-plugin-manager:bootJar') + return copySpec { + // Copy the jar + with copySpec { + from project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) } + rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') + } + + // Copy the scripts + with copySpec { + from project(':takserver-core').file('scripts/plugins') + } + } + } + + copyIntegrationTestAdditionsSpec = { task -> + // Add jar task dependency + task.dependsOn(':takserver-takcl-core:publicJar') + return copySpec { + // Copy the jar + from(project(':takserver-takcl-core').file('build/libs')) { + include("takcl-${version}-exe.jar") + rename("takcl-${version}-exe.jar", "utils/takcl.jar") + } + // Copy the p + from(project(':takserver-takcl-core').file('plugin-test-libs')) { + rename('(.*)', 'lib/$1') + } + } + } + + copyHealthCheckScriptsSpec = { + return copySpec { + from project(':takserver-core').file('docker/hardened/tak/health') + rename('(.*)', 'docker/hardened/takArtifacts/health/$1') + } + } + + copyGeneratedDockerFilesSpec = { task -> + task.dependsOn(createDockerDirs) + return copySpec { + from("$buildDir/takDockerDirs") + } + } + + copyVersionFileSpec = { task -> + task.dependsOn(createVersionFile) + return copySpec { + from("$buildDir/takVersionDir/version.txt") + } + } + + copyPluginTestLibsSpec = { task -> + return copySpec { + from(project(':takserver-takcl-core').file('plugin-test-libs')) { + rename('(.*)', 'lib/$1') + } + } + } +} diff --git a/src/takserver-package/utils/copyspecs-composite.gradle b/src/takserver-package/utils/copyspecs-composite.gradle new file mode 100644 index 00000000..526e2e3f --- /dev/null +++ b/src/takserver-package/utils/copyspecs-composite.gradle @@ -0,0 +1,119 @@ +/** + * Composite copyspecs that are either made of a combination of other specs or a base spec along with + * derived specs for other custom deployment scenarios such as containerization. + */ + +apply from: "$rootDir/takserver-package/utils/copyspecs-base.gradle" + +ext { + takserverCommonSpec = { task -> + return copySpec { + with copyBaseTakJarsAndWarsSpec(task) + with copyCoreConfigXSDSpec() + with copyDocsSpec() + with copyPolicySpec() + with copyLicenseSpec() + } + } + + takserverDockerCommonSpec = { task -> + return copySpec { + with takserverCommonSpec(task) + with copyGeneratedDockerFilesSpec(task) + with copySchemaManagerJarSpec(task) + with copyFullPluginManagerSpec(task) + with copyFullRetentionServiceSpec(task) + with copySwaggerSpec() + } + } + + copyDbScriptsSpec = { + return copySpec { + from project(':takserver-schemamanager').file('scripts') + rename('(.*)', 'db-utils/$1') + } + } + + copyDockerDbScriptsSpec = { + return copySpec { + with copyDbScriptsSpec().exclude('pg_hba.conf', 'postgresql.conf', 'configureInDocker.sh') + from(project(':takserver-schemamanager').file('docker')) { + include('pg_hba.conf', 'postgresql.conf', 'configureInDocker.sh') + rename('(.*)', 'db-utils/$1') + } + } + } + + copyHardenedDockerDbScriptsSpec = { + return copySpec { + with copyDockerDbScriptsSpec().exclude('configureInDocker.sh', 'takserver-setup-db.sh') + from(project(':takserver-schemamanager').file('docker/hardened')) { + include('configureInDocker.sh', 'takserver-setup-db.sh') + rename('(.*)', 'db-utils/$1') + } + } + } + + copyHardenedFullDockerDbScriptsSpec = { + return copySpec { + with copyDockerDbScriptsSpec().exclude('configureInDocker.sh', + 'pg_hba.conf', 'postgresql.conf') + } + } + + + copyCoreConfigExampleSpec = { + return copySpec { + from project(':takserver-core').file('example/') + include 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'logging-restrictsize.xml' + } + } + + copyDockerCoreConfigExampleSpec = { + return copySpec { + with copyCoreConfigExampleSpec().exclude('CoreConfig.example.xml') + from(project(':takserver-core').file('example')) { + include('CoreConfig.example.docker.xml') + rename('CoreConfig.example.docker.xml', 'CoreConfig.example.xml') + } + } + } + + copyHardenedDockerCoreConfigExampleSpec = { + return copySpec { + with copyCoreConfigExampleSpec().exclude('CoreConfig.example.xml') + from(project(':takserver-core').file('example')) { + include('CoreConfig.example.docker-hardened.xml') + rename('CoreConfig.example.docker-hardened.xml', 'CoreConfig.example.xml') + } + } + } + + + copyHardenedFullDockerCoreConfigExampleSpec = { + return copySpec { + with copyCoreConfigExampleSpec().exclude('CoreConfig.example.xml') + from(project(':takserver-core').file('example')) { + include('CoreConfig.example.docker-hardened-full.xml') + rename('CoreConfig.example.docker-hardened-full.xml', 'CoreConfig.example.xml') + } + } + } + + copyDockerScriptsSpec = { + return copySpec { + from project(':takserver-core').file('scripts') + exclude 'utils', 'plugins*', 'retention*', 'takserver.sh' + } + } + + copyDockerSecurityFilesSpec = { + return copySpec { + from project(':takserver-core').file('docker/hardened/tak/security') + include "rpms/repos/*" + include "rpms/signatures/*" + include "epel-release*" + rename('(.*)', 'docker/hardened/takArtifacts/security/$1') + } + } +} diff --git a/src/takserver-package/utils/utils.gradle b/src/takserver-package/utils/utils.gradle index 4d564e31..8abdf359 100644 --- a/src/takserver-package/utils/utils.gradle +++ b/src/takserver-package/utils/utils.gradle @@ -1,735 +1,265 @@ def hardened_rpm_download_directory = "$buildDir/docker/hardened-shared/takArtifacts/security" -task copyLicense(type: Copy) { - from("${project.rootDir}/../") - include 'LICENSE.txt' - into "$buildDir/takArtifacts" -} -// Copy db-utils scripts -task copyDbScripts(type: Copy) { - from project(':takserver-schemamanager').file('scripts') - into "$buildDir/takArtifacts/db-utils" -} - -// Gather all wars into this build -task copyWars(type: Copy) { - dependsOn(':takserver-core:bootWar') - - from project(':takserver-core').collect { it.tasks.withType(War) } - into "$buildDir/takArtifacts" - rename('takserver-core-' + version + '.war', 'takserver.war') - exclude 'takserver-war*', 'takserver-war-*.war' -} - -// Get CoreConfig.xsd and other XSDs including the TAKIgniteConfig.xsd -task copyCoreConfigXSD(type: Copy) { - from project(':takserver-common').file('src/main/xsd') - include '*.xsd' - into "$buildDir/takArtifacts" -} - -// Get CoreConfig.example.xml -task copyCoreConfigExample(type: Copy) { - from project(':takserver-core').file('example/') - include 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'logging-restrictsize.xml' - into "$buildDir/takArtifacts" -} +apply from: "$rootDir/takserver-package/utils/copyspecs-composite.gradle" -// Copy scripts -task copyCoreScripts(type: Copy) { - from project(':takserver-core').file('scripts') - into "$buildDir/takArtifacts" - exclude 'config*', 'utils','messaging*', 'API*', 'plugins*', 'retention*', "launcher*", 'takserver.sh' -} - -// Copy docs -task copyDocs(type: Copy) { - from getRootProject().file('docs') - include '*.pdf' - into "$buildDir/takArtifacts/docs" +task createCIVersionArtifacts() { + // The docker tags, starting with the unique primary docker tag + def primaryDockerTag = (takversion + '-' + takreleaserpm).toLowerCase() + def dockerTags = [primaryDockerTag] + def takVersion = version + + doLast { + // For properly tagged types, set the appropriate publishPath + if (takrelease.contains('RELEASE')) { + // If it is a release, also set the x.y version tag + dockerTags.add(takversion) + + // If the env var indicates it should be tagged as the latest, tag it as the latest + if ("$System.env.TAG_AS_LATEST".toLowerCase() == "true") { + dockerTags.add('latest') + } + + } else if (!(takrelease.contains('BETA') || takrelease.contains('DEV'))) { + // If it is an unknown type, clear the docker tags and set the publish path to dev with branch and hash information + primaryDockerTag = '' + dockerTags = [] + takVersion = takversion + '-' + gitbranch.replace('/', '_') + "-" + gitrev.substring(0, 8) + } + + // Write as env vars to file that can be easily sourced by CI scripts + def tagsStr = dockerTags.join(' ') + new File("$projectDir/CI_ENV_VARS").text = + "TAK_DOCKER_TAGS=\"$tagsStr\"\n" + + "TAK_VERSION=\"$takVersion\"\n" + + "TAK_RPM_VERSION=\"$takversion-$takreleaserpm\"\n" + + "TAK_DOCKER_PRIMARY_TAG=\"$primaryDockerTag\"\n" + } } -// Copy swagger files -task copySwagger(type: Copy) { - from project(':takserver-core').file('oas') - include '*.html' - include '*.js' - into "$buildDir/takArtifacts/oas" -} +task constructDockerZip(type: Zip) { Task task -> + archiveName 'takserver-docker-' + project.version + '.zip' + duplicatesStrategy DuplicatesStrategy.FAIL + destinationDir(file("$buildDir/distributions")) + into('takserver-docker-' + project.version) -// Copy selinux policy -task copyPolicy(type: Copy) { - from project(':takserver-core').file('scripts/utils') - include '*.te' - into "$buildDir/takArtifacts" -} + with takserverDockerCommonSpec(task).into('tak') + with copyDockerCoreConfigExampleSpec().into('tak') + with copyDockerScriptsSpec().into('tak') + with copyDockerDbScriptsSpec().into('tak') -task createLogDir() { - def logs = new File("$buildDir/takArtifacts/logs") - doLast { - logs.mkdirs() - new File(logs, "takserver.log").text = "" - } -} + from(project(':takserver-core').file('docker')) { + include('configureInDocker.sh') + include('Dockerfile.takserver') + rename('(configureInDocker.sh)', 'tak/$1') + rename('(Dockerfile.takserver)', 'docker/$1') + } -task createPluginsLibDir() { - def logs = new File("$buildDir/takArtifacts/lib") - doLast { - logs.mkdirs() - } -} + from(project(':takserver-schemamanager').file('docker')) { + include('Dockerfile.takserver-db') + rename('(Dockerfile.takserver-db)', 'docker/$1') + } -// Gather all jars into this build (including shadow jars) -task copyJars(type: Copy) { - dependsOn(':takserver-core:bootJar') - dependsOn(':takserver-schemamanager:shadowJar') - dependsOn(':takserver-usermanager:shadowJar') - dependsOn(':takserver-takcl-core:publicJar') - - from getRootProject().subprojects.collect { it.tasks.withType(Jar) } - into "$buildDir/takArtifacts" - rename('schemamanager-' + version + '-uber.jar', 'db-utils/SchemaManager.jar') - rename('UserManager-' + version + '-all.jar', 'utils/UserManager.jar') - rename('takcl-' + version + '-exe.jar', 'utils/takcl.jar') - exclude 'takserver-common*', 'takserver-core*', 'takserver-package*', 'API*', 'messaging*', - 'database*', 'takserver-schemamanager*', 'takserver-takcl*', 'takserver-usermanager*', - 'takserver-war-*', 'takcl-dev*', 'takserver-plugin*', - 'takserver-plugin-manager*', 'takserver-cluster*', 'takserver-fig-core*', 'takserver-retention*', - 'rol*', 'Periodic*', 'launcher*', 'takserver-' + version + '*', - 'federation-hub-*', 'federation-common-*', 'takserver-protobuf-*' + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') } -// Gather all jars into this build (including shadow jars) -task copyJarsNoDb(type: Copy) { - dependsOn(':takserver-core:bootJar') - dependsOn(':takserver-usermanager:shadowJar') - dependsOn(':takserver-takcl-core:publicJar') - - from getRootProject().subprojects.collect { it.tasks.withType(Jar) } - into "$buildDir/takArtifacts" - rename('UserManager-' + version + '-all.jar', 'utils/UserManager.jar') - rename('takcl-' + version + '-exe.jar', 'utils/takcl.jar') - exclude 'takserver-common*', 'takserver-core*', 'takserver-package*', 'API*', 'messaging*', - 'database*', 'takserver-schemamanager*', 'schemamanager*', 'takserver-takcl*', - 'takserver-usermanager*', 'takserver-war-*', 'takcl-dev*', 'takserver-plugin*', 'takserver-plugin-manager*', - 'takserver-cluster*', 'takserver-fig-core*', 'takserver-retention*', 'rol*', 'Periodic*', 'launcher*', 'takserver-' + version + '*', - 'federation-hub-*', 'federation-common-*', 'takserver-protobuf-*' -} +task constructIntegrationTesterZip(type: Zip) { task -> + archiveName "takserver-integration-tester-${project.version}.zip" + duplicatesStrategy DuplicatesStrategy.FAIL + destinationDir(file("$buildDir/distributions")) + into("takserver-integration-tester-${project.version}") -// Copy Configuration Microervice scripts -task copyConfigScripts(type: Copy) { - from project(':takserver-core').file('scripts') - into "$buildDir/takArtifacts" - exclude 'utils*', 'messaging*', 'API*', 'plugins*', 'retention*', 'launcher*', 'takserver.sh' -} + with takserverDockerCommonSpec(task).into('tak') + with copyDockerCoreConfigExampleSpec().into('tak') + with copyDockerScriptsSpec().into('tak') + with copyDockerDbScriptsSpec().into('tak') -// Copy API Microervice scripts -task copyAPIScripts(type: Copy) { - from project(':takserver-core').file('scripts') - into "$buildDir/takArtifacts" - exclude 'config*', 'utils*', 'messaging*', 'plugins*', 'retention*', 'launcher*', 'takserver.sh' -} -// Copy Messaging Microervice scripts -task copyMessagingScripts(type: Copy) { - from project(':takserver-core').file('scripts') - into "$buildDir/takArtifacts" - exclude 'config*', 'utils*', 'API*', 'launcher*', 'plugins*', 'retention*', 'takserver.sh' +// with copyCoreScriptsSpec().into('tak') +// with copyDbScriptsSpec().into('tak') +// with copyLauncherAndConfigAndMessagingScriptsSpec().into('tak') + with copyFullFedhubSpec(task).into('tak') +// with copyDockerDbScriptsSpec().into('tak') +// with copyDockerCoreConfigExampleSpec().into('tak') + with copyIntegrationTestAdditionsSpec(task).into('tak') } -// Copy scripts -task copyLauncherAndConfigAndMessagingScripts(type: Copy) { - from project(':takserver-core').file('scripts') - into "$buildDir/takArtifacts" - exclude 'utils*', 'API*', 'plugins*', 'retention*', 'takserver.sh' +task fetchRpmsForHardenedDocker(type: Exec) { + dependsOn(':takserver-package:prePackage') + def epelFilename = "epel-release-latest-8.noarch.rpm" + def outputfilepath = "${hardened_rpm_download_directory}/${epelFilename}" + outputs.file(outputfilepath) + commandLine "curl", "--create-dirs", "https://dl.fedoraproject.org/pub/epel/${epelFilename}", "--output", outputfilepath } -// These are wrapped in a creation method to preserve immutability. - -// Copy scripts -def copyPluginsScriptsSpec() { - return copySpec { - from project(':takserver-core').file('scripts/plugins') - } -} +task constructIronbankDBZip(type: Zip) { task -> + archiveName 'takserver-ironbank-db-' + project.version + '.zip' + duplicatesStrategy DuplicatesStrategy.FAIL + destinationDir(file("$buildDir/distributions")) + into('takserver-ironbank-db-' + project.version) -// Copy retention config files -def copyRetentionConfigsSpec() { - return copySpec { - from project(':takserver-retention').file('conf/retention') - } -} + with copySchemaManagerJarSpec(task) + with copyHardenedDockerDbScriptsSpec() + with copyHardenedDockerCoreConfigExampleSpec().exclude('logging-restrictsize.xml') -// Copy retention mission archive store dir -def copyRetentionMissionStoreDirSpec() { - return copySpec { - from project(':takserver-retention').file('mission-archive/') - rename('(.*)', 'mission-archive/$1') - } + from(project(':takserver-schemamanager').file('docker/hardened')) { + include('Dockerfile.hardened-takserver-db') + } } -// Copy scripts -def copyRetentionScriptsSpec() { - return copySpec { - from project(':takserver-core').file('scripts/retention/') - rename('(.*)', 'retention/$1') - } -} +task constructHardenedDockerZip(type: Zip) { Task task -> + dependsOn fetchRpmsForHardenedDocker -// Gather all jars into this build (including shadow jars) -task copySchemaManagerJar(type: Copy) { - dependsOn(':takserver-schemamanager:shadowJar') + archiveName 'takserver-docker-hardened-' + project.version + '.zip' + duplicatesStrategy DuplicatesStrategy.FAIL + destinationDir(file('build/distributions')) + into('takserver-docker-hardened-' + project.version) - from project(':takserver-schemamanager').collect { it.tasks.withType(Jar) } - into "$buildDir/takArtifacts" - rename('schemamanager-' + version + '-uber.jar', 'db-utils/SchemaManager.jar') - exclude 'takserver-schemamanager*' -} + with takserverDockerCommonSpec(task).into('tak') + with copyDockerScriptsSpec().into('tak') + with copyHardenedDockerDbScriptsSpec().into('tak') + with copyHardenedDockerCoreConfigExampleSpec().into('tak') -// Copy db-utils scripts from dbonly directory -task copyDbOnlyScripts(type: Copy) { - from project(':takserver-schemamanager').file('scripts-dbonly') - into "$buildDir/takArtifacts/db-utils" -} + from(project(':takserver-core').file('docker')) { + include('configureInDocker.sh') + rename('(configureInDocker.sh)', 'tak/$1') + } -def copyPluginsJarSpec() { - return copySpec { - from project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) } - rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') - } -} + // Include the hardened docker-specific files + from(project(':takserver-core').file('docker/hardened')) { + exclude('full') + rename('(Dockerfile.ca)', 'docker/$1') + rename('(Dockerfile.hardened-takserver)', 'docker/$1') + rename('(README_hardened_docker.md)', 'docker/$1') + } -def copyRetentionJarSpec() { - return copySpec { - from project(':takserver-retention').collect { it.tasks.withType(Jar) } - rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') - } -} + from(hardened_rpm_download_directory) { + into('tak/security') + } -task createCIVersionArtifacts() { - // The docker tags, starting with the unique primary docker tag - def primaryDockerTag = (takversion + '-' + takreleaserpm).toLowerCase() - def dockerTags = [primaryDockerTag] - def takVersion = version - - doLast { - // For properly tagged types, set the appropriate publishPath - if (takrelease.contains('RELEASE')) { - // If it is a release, also set the x.y version tag - dockerTags.add(takversion) - - // If the env var indicates it should be tagged as the latest, tag it as the latest - if ("$System.env.TAG_AS_LATEST".toLowerCase() == "true") { - dockerTags.add('latest') - } - - } else if (!(takrelease.contains('BETA') || takrelease.contains('DEV'))) { - // If it is an unknown type, clear the docker tags and set the publish path to dev with branch and hash information - primaryDockerTag = '' - dockerTags = [] - takVersion = takversion + '-' + gitbranch.replace('/', '_') + "-" + gitrev.substring(0, 8) - } - - // Write as env vars to file that can be easily sourced by CI scripts - def tagsStr = dockerTags.join(' ') - new File("$projectDir/CI_ENV_VARS").text = - "TAK_DOCKER_TAGS=\"$tagsStr\"\n" + - "TAK_VERSION=\"$takVersion\"\n" + - "TAK_RPM_VERSION=\"$takversion-$takreleaserpm\"\n" + - "TAK_DOCKER_PRIMARY_TAG=\"$primaryDockerTag\"\n" - } -} + // Include the hardened schemamanager-specific dockerfile + from(project(':takserver-schemamanager').file('docker/hardened')) { + include('Dockerfile.hardened-takserver-db') + rename('(Dockerfile.hardened-takserver-db)', 'docker/$1') + } -task createVersionFile() { - def takArtifacts = "$buildDir/takArtifacts" - doLast { - new File(takArtifacts).mkdirs() - new File(takArtifacts, "version.txt").text = """$version""" - } + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') } -task createDockerDirs() { - doLast { - mkdir "$buildDir/takArtifacts/logs" - mkdir "$buildDir/takArtifacts/lib" - mkdir "$buildDir/takArtifacts/webcontent/webtak-plugins" - new File("$buildDir/takArtifacts/logs/takserver.log").text = "" - } -} +task constructFullDockerZip(type: Zip) { Task task -> + archiveName 'takserver-docker-full-' + project.version + '.zip' + duplicatesStrategy DuplicatesStrategy.FAIL + destinationDir(file("$buildDir/distributions")) + into('takserver-docker-full-' + project.version) -def copyCoreDockerFilesSpec() { - return from('build/takArtifacts') { - exclude('db-utils/pg_hba.conf', 'db-utils/postgresql.conf', - 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'takserver-plugins.sh', 'takserver-plugins-cluster.sh', - 'takserver-plugins', 'takserver-pm.jar', 'takserver-retention.jar', 'mission-archive', 'retention') - } -} + with takserverDockerCommonSpec(task).into('tak') + with copyFullDockerScriptsSpec().into('tak') + with copyDockerDbScriptsSpec().into('tak') + with copyDockerCoreConfigExampleSpec().into('tak') -def copyDockerSchemaManagerSpec() { - return from(project(':takserver-schemamanager').file('docker')) { - include('pg_hba.conf') - include('postgresql.conf') - rename('(pg_hba.conf)', 'db-utils/$1') - rename('(postgresql.conf)', 'db-utils/$1') - } -} + from(project(':takserver-core').file('docker')) { + include('configureInDocker.sh') + rename('(configureInDocker.sh)', 'tak/$1') + } -//// Legacy individual ones for other elements so they properly build still + from(project(':takserver-core').file('docker/full')) { + include('coreConfigEnvHelper.py') + include('docker-compose.yml') + include('docker_entrypoint.sh') + include('Dockerfile.takserver') + include('EDIT_ME.env') + include('full-README.md') + rename('(coreConfigEnvHelper.py)', 'tak/$1') + rename('(docker-compose.yml)', 'docker/$1') + rename('(docker_entrypoint.sh)', 'tak/$1') + rename('(Dockerfile.takserver)', 'docker/$1') + rename('(EDIT_ME.env)', 'docker/$1') + rename('(full-README.md)', 'docker/$1') + } -task copyPluginsJar(type: Copy) { - from project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) } - rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') - into "$buildDir/takArtifacts" + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') } -task copyRetentionJar(type: Copy) { - from project(':takserver-retention').collect { it.tasks.withType(Jar) } - rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') - into "$buildDir/takArtifacts" -} -task copyRetentionScripts(type: Copy) { - from project(':takserver-core').file('scripts/retention/') - rename('(.*)', 'retention/$1') - into "$buildDir/takArtifacts" -} +task constructHardenedFullDockerZip(type: Zip) { Task task -> + archiveName 'takserver-docker-hardened-full-' + project.version + '.zip' + duplicatesStrategy DuplicatesStrategy.FAIL + destinationDir(file("$buildDir/distributions")) + into('takserver-docker-hardened-full-' + project.version) -task copyRetentionMissionStoreDir(type: Copy) { - from project(':takserver-retention').file('mission-archive/') - rename('(.*)', 'mission-archive/$1') - into "$buildDir/takArtifacts/" -} -task copyPluginsScripts(type: Copy) { - from project(':takserver-core').file('scripts/plugins') - into "$buildDir/takArtifacts" -} + dependsOn fetchRpmsForHardenedDocker -task copyRetentionConfigs(type: Copy) { - from project(':takserver-retention').file('conf/retention') - into "$buildDir/takArtifacts/conf/retention" -} + with takserverDockerCommonSpec(task).into('tak') + with copyHardenedFullDockerDbScriptsSpec().into('tak') + with copyDockerScriptsSpec().into('tak') + with copyHardenedFullDockerCoreConfigExampleSpec().exclude('TAKIgniteConfig.example.xml').into('tak') -task constructDockerZip(type: Zip) { - dependsOn createDockerDirs - dependsOn createVersionFile - dependsOn (':takserver-package:prePackage') - dependsOn copyCoreScripts - dependsOn copyDbScripts - dependsOn copyLauncherAndConfigAndMessagingScripts - dependsOn createVersionFile - - archiveName 'takserver-docker-' + version + '.zip' - duplicatesStrategy 'fail' - destinationDir(file("$buildDir/distributions")) - into('takserver-docker-' + version) - - from('build/takArtifacts') { - exclude('db-utils/pg_hba.conf', 'db-utils/postgresql.conf', - 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'takserver-plugins.sh', 'takserver-plugins-cluster.sh', - 'takserver-plugins', 'takserver-pm.jar', 'takserver-retention.jar', 'mission-archive', 'retention') - into('tak') - } - - from(project(':takserver-core').file('scripts/plugins')) { - into('tak') - } - - from(project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) }) { - rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') - into('tak') - } - - from(project(':takserver-retention').file('mission-archive/')) { - rename('(.*)', 'mission-archive/$1') - into('tak') - } - - from(project(':takserver-core').file('scripts/retention/')) { - rename('(.*)', 'retention/$1') - into('tak') - } - - from(project(':takserver-retention').collect { it.tasks.withType(Jar) }) { - rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') - into('tak') - } - - from(project(':takserver-schemamanager').file('docker')) { - include('pg_hba.conf') - include('postgresql.conf') - rename('(pg_hba.conf)', 'db-utils/$1') - rename('(postgresql.conf)', 'db-utils/$1') - into('tak') - } - - // Include the standard docker CoreConfig.xml - from(project(':takserver-core').file('example')) { - include('TAKIgniteConfig.example.xml') - include('CoreConfig.example.docker.xml') - rename('CoreConfig.example.docker.xml', 'tak/CoreConfig.example.xml') - rename('TAKIgniteConfig.example.xml', 'tak/TAKIgniteConfig.example.xml') - } - - from(project(':takserver-core').file('docker')) { - include('configureInDocker.sh') - include('Dockerfile.takserver') - rename('(configureInDocker.sh)', 'tak/$1') - rename('(Dockerfile.takserver)', 'docker/$1') - } - - from(project(':takserver-schemamanager').file('docker')) { - include('configureInDocker.sh') - include('Dockerfile.takserver-db') - rename('(configureInDocker.sh)', 'tak/db-utils/$1') - rename('(Dockerfile.takserver-db)', 'docker/$1') - } -} + from(project(':takserver-core').file('docker')) { + include('configureInDocker.sh') + rename('(configureInDocker.sh)', 'tak/$1') + } -task fetchRpmsForHardenedDocker(type: Exec) { - dependsOn (':takserver-package:prePackage') - def epelFilename="epel-release-latest-8.noarch.rpm" - def outputfilepath="${hardened_rpm_download_directory}/${epelFilename}" - outputs.file(outputfilepath) - commandLine "curl", "--create-dirs", "https://dl.fedoraproject.org/pub/epel/${epelFilename}", "--output", outputfilepath -} + from(project(':takserver-core').file('docker/hardened/full')) { + include('docker-compose.yml') + include('docker_entrypoint.sh') + include('EDIT_ME.env') + include('full-README.md') -task constructIronbankDBZip(type: Zip) { - dependsOn createDockerDirs - dependsOn createVersionFile - dependsOn fetchRpmsForHardenedDocker - dependsOn copyLauncherAndConfigAndMessagingScripts - - archiveName 'takserver-ironbank-db-' + version + '.zip' - duplicatesStrategy 'fail' - destinationDir(file("$buildDir/distributions")) - into('takserver-ironbank-db-' + version) - - from('build/takArtifacts/db-utils') { - exclude('pg_hba.conf', 'postgresql.conf', 'takserver-setup-db.sh') - into('db-utils') - } - - from(project(':takserver-schemamanager').file('docker')) { - include('pg_hba.conf') - include('postgresql.conf') - rename('(pg_hba.conf)', '$1') - rename('(postgresql.conf)', '$1') - into('db-utils') - } - - // Include the hardened docker CoreConfig.xml - from(project(':takserver-core').file('example')) { - include('TAKIgniteConfig.example.xml') - include('CoreConfig.example.docker-hardened.xml') - rename('CoreConfig.example.docker-hardened.xml', 'CoreConfig.example.xml') - } - - // Include the hardened schemamanager-specific files - from(project(':takserver-schemamanager').file('docker/hardened')) { - rename('(configureInDocker.sh)', 'db-utils/$1') - rename('(takserver-setup-db.sh)', 'db-utils/$1') - } + rename('(docker-compose.yml)', 'docker/$1') + rename('(docker_entrypoint.sh)', 'tak/$1') + rename('(EDIT_ME.env)', 'docker/$1') + rename('(full-README.md)', 'docker/$1') + } -} + from(project(':takserver-core').file('docker/hardened/full')) { + include('Dockerfile.hardened-full-takserver') + into('docker') + } -task constructHardenedDockerZip(type: Zip) { - dependsOn createDockerDirs - dependsOn createVersionFile - dependsOn fetchRpmsForHardenedDocker - dependsOn copyLauncherAndConfigAndMessagingScripts - - - archiveName 'takserver-docker-hardened-' + version + '.zip' - duplicatesStrategy 'fail' - destinationDir(file('build/distributions')) - into('takserver-docker-hardened-' + version) - - from('build/takArtifacts') { - exclude('db-utils/pg_hba.conf', 'db-utils/postgresql.conf', - 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'takserver-plugins.sh', 'takserver-plugins-cluster.sh', - 'takserver-plugins', 'takserver-pm.jar', 'takserver-retention.jar', 'mission-archive', 'retention', 'db-utils/takserver-setup-db.sh') - into('tak') - } - - from(project(':takserver-core').file('scripts/plugins')) { - into('tak') - } - - from(project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) }) { - rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') - into('tak') - } - - from(project(':takserver-retention').file('mission-archive/')) { - rename('(.*)', 'mission-archive/$1') - into('tak') - } - - from(project(':takserver-core').file('scripts/retention/')) { - rename('(.*)', 'retention/$1') - into('tak') - } - - from(project(':takserver-retention').collect { it.tasks.withType(Jar) }) { - rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') - into('tak') - } - - from(project(':takserver-schemamanager').file('docker')) { - include('pg_hba.conf') - include('postgresql.conf') - rename('(pg_hba.conf)', 'db-utils/$1') - rename('(postgresql.conf)', 'db-utils/$1') - into('tak') - } - - // Include the hardened CoreConfig.xml and TAKIgniteConfig.example.xml - from(project(':takserver-core').file('example')) { - include('TAKIgniteConfig.example.xml') - include('CoreConfig.example.docker-hardened.xml') - rename('CoreConfig.example.docker-hardened.xml', 'tak/CoreConfig.example.xml') - rename('TAKIgniteConfig.example.xml', 'tak/TAKIgniteConfig.example.xml') - } - - from(project(':takserver-core').file('docker')) { - include('configureInDocker.sh') - rename('(configureInDocker.sh)', 'tak/$1') - } - - // Include the hardened docker-specific files - from(project(':takserver-core').file('docker/hardened')) { - exclude('full') - rename('(Dockerfile.ca)', 'docker/$1') - rename('(Dockerfile.hardened-takserver)', 'docker/$1') - rename('(README_hardened_docker.md)', 'docker/$1') - } - - from(hardened_rpm_download_directory) { - into('tak/security') - } - - // Include the hardened schemamanager-specific files - from(project(':takserver-schemamanager').file('docker/hardened')) { - exclude('full') - rename('(configureInDocker.sh)', 'tak/db-utils/$1') - rename('(takserver-setup-db.sh)', 'tak/db-utils/$1') - rename('(Dockerfile.hardened-takserver-db)', 'docker/$1') - } -} + from(project(':takserver-core').file('docker/hardened/tak')) { + include('security/**') + include('health/**') + into('tak') + } -task constructFullDockerZip(type: Zip) { - dependsOn createDockerDirs - dependsOn copyLauncherAndConfigAndMessagingScripts - dependsOn createVersionFile - dependsOn (':takserver-package:prePackage') - - - - archiveName 'takserver-docker-full-' + version + '.zip' - duplicatesStrategy 'exclude' - destinationDir(file("$buildDir/distributions")) - into('takserver-docker-full-' + version) - - from('build/takArtifacts') { - exclude('db-utils/pg_hba.conf', 'db-utils/postgresql.conf', - 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'takserver-plugins.sh', 'takserver-plugins-cluster.sh', - 'takserver-plugins', 'takserver-pm.jar', 'takserver-retention.jar', 'mission-archive', 'retention') - into('tak') - } - - from(project(':takserver-core').file('scripts/plugins')) { - into('tak') - } - - from(project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) }) { - rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') - into('tak') - } - - from(project(':takserver-retention').file('conf/retention')) { - into('tak/conf/retention') - } - - from(project(':takserver-retention').file('mission-archive/')) { - rename('(.*)', 'mission-archive/$1') - into('tak') - } - - from(project(':takserver-core').file('scripts/retention/')) { - rename('(.*)', 'retention/$1') - into('tak') - } - - from(project(':takserver-retention').collect { it.tasks.withType(Jar) }) { - rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') - into('tak') - } - - from(project(':takserver-schemamanager').file('docker')) { - include('pg_hba.conf') - include('postgresql.conf') - rename('(pg_hba.conf)', 'db-utils/$1') - rename('(postgresql.conf)', 'db-utils/$1') - into('tak') - } - - // Include the standard docker CoreConfig.xml and TAKIgniteConfig.example.xml - from(project(':takserver-core').file('example')) { - include('TAKIgniteConfig.example.xml') - include('CoreConfig.example.docker.xml') - rename('CoreConfig.example.docker.xml', 'tak/CoreConfig.example.xml') - rename('TAKIgniteConfig.example.xml', 'tak/TAKIgniteConfig.example.xml') - } - - from(project(':takserver-core').file('docker')) { - include('configureInDocker.sh') - rename('(configureInDocker.sh)', 'tak/$1') - } - - from(project(':takserver-core').file('docker/full')) { - include('coreConfigEnvHelper.py') - include('docker-compose.yml') - include('docker_entrypoint.sh') - include('Dockerfile.takserver') - include('EDIT_ME.env') - include('full-README.md') - - rename('(coreConfigEnvHelper.py)', 'tak/$1') - rename('(docker-compose.yml)', 'docker/$1') - rename('(docker_entrypoint.sh)', 'tak/$1') - rename('(Dockerfile.takserver)', 'docker/$1') - rename('(EDIT_ME.env)', 'docker/$1') - rename('(full-README.md)', 'docker/$1') - } - - from(project(':takserver-schemamanager').file('docker')) { - include('configureInDocker.sh') - rename('(configureInDocker.sh)', 'tak/db-utils/$1') - } -} + from(hardened_rpm_download_directory) { + into('tak/security') + } + from(project(':takserver-schemamanager').file('docker/hardened/full')) { + exclude('Dockerfile.hardened-full-takserver-db') + into("tak/db-utils/full") + } -task constructHardenedFullDockerZip(type: Zip) { - dependsOn createDockerDirs - dependsOn copyLauncherAndConfigAndMessagingScripts - dependsOn createVersionFile - dependsOn fetchRpmsForHardenedDocker - dependsOn createLogDir - dependsOn createPluginsLibDir - dependsOn copySchemaManagerJar - - - archiveName 'takserver-docker-hardened-full-' + version + '.zip' - duplicatesStrategy 'exclude' - destinationDir(file("$buildDir/distributions")) - into('takserver-docker-hardened-full-' + version) - - from('build/takArtifacts') { - exclude('db-utils/pg_hba.conf', 'db-utils/postgresql.conf', - 'TAKIgniteConfig.example.xml', 'CoreConfig.example.xml', 'takserver-plugins.sh', 'takserver-plugins-cluster.sh', - 'takserver-plugins', 'takserver-pm.jar', 'takserver-retention.jar', 'mission-archive', 'retention') - into('tak') - } - - from(project(':takserver-core').file('scripts/plugins')) { - into('tak') - } - - from(project(':takserver-plugin-manager').collect { it.tasks.withType(Jar) }) { - rename('takserver-plugin-manager-' + version + '.jar', 'takserver-pm.jar') - into('tak') - } - - from(project(':takserver-retention').file('mission-archive/')) { - rename('(.*)', 'mission-archive/$1') - into('tak') - } - - from(project(':takserver-core').file('scripts/retention/')) { - rename('(.*)', 'retention/$1') - into('tak') - } - - from(project(':takserver-retention').collect { it.tasks.withType(Jar) }) { - rename('takserver-retention-' + version + '.jar', 'takserver-retention.jar') - into('tak') - } - - // Include the standard docker CoreConfig.xml and TAKIgniteConfig.example.xml - from(project(':takserver-core').file('docker/hardened/full')) { - include('TAKIgniteConfig.example.xml') - include('CoreConfig.example.docker-hardened-full.xml') - rename('CoreConfig.example.docker-hardened-full.xml', 'tak/CoreConfig.example.xml') - rename('TAKIgniteConfig.example.xml', 'tak/TAKIgniteConfig.example.xml') - } - - from(project(':takserver-core').file('docker')) { - include('configureInDocker.sh') - rename('(configureInDocker.sh)', 'tak/$1') - } - - from(project(':takserver-core').file('docker/hardened/full')) { - include('docker-compose.yml') - include('docker_entrypoint.sh') - include('EDIT_ME.env') - include('full-README.md') - - rename('(docker-compose.yml)', 'docker/$1') - rename('(docker_entrypoint.sh)', 'tak/$1') - rename('(EDIT_ME.env)', 'docker/$1') - rename('(full-README.md)', 'docker/$1') - } - - from(project(':takserver-core').file('docker/hardened/full')) { - include('Dockerfile.hardened-full-takserver') - into('docker') - } - - from(project(':takserver-core').file('docker/hardened/tak')) { - include('security/**') - include('health/**') - into('tak') - } - - from(hardened_rpm_download_directory) { - into('tak/security') - } - - from(project(':takserver-schemamanager').file('docker/hardened/full')) { - exclude('Dockerfile.hardened-full-takserver-db') - into("tak/db-utils/full") + from(project(':takserver-schemamanager').file('docker/hardened/full')) { + include('Dockerfile.hardened-full-takserver-db') + into("docker") } - from(project(':takserver-schemamanager').file('docker/hardened/full')) { - include('Dockerfile.hardened-full-takserver-db') - into("docker") - } + // TODO: Fix this gradle-detected dependency that probably shouldn't be + dependsOn(':takserver-cluster:copyClusterConfig', + ':takserver-cluster:copyClusterProperties', + ':takserver-cluster:moveCoreConfig') } //// I'm not sure these two should even be in the takserver RPM. But I don't want to risk breaking it task copyDockerSecurityFiles(type: Copy) { - from project(':takserver-core').file('docker/hardened/tak/security') - include "rpms/repos/*" - include "rpms/signatures/*" - include "epel-release*" - into "$buildDir/docker/hardened/takArtifacts/security" + from project(':takserver-core').file('docker/hardened/tak/security') + include "rpms/repos/*" + include "rpms/signatures/*" + include "epel-release*" + into "$buildDir/docker/hardened/takArtifacts/security" } task copyHealthCheckScripts(type: Copy) { - from project(':takserver-core').file('docker/hardened/tak/health') - into "$buildDir/docker/hardened/takArtifacts/health" -} - - -// Copy hardened schemamanager dockerfiles -task copyHardenedSchemaManagerFiles(type: Copy) { - from project(':takserver-schemamanager').file('docker') - include 'pg_hba.conf' - include 'postgresql.conf' - include 'hardened/*' - into "$buildDir/docker" - rename('pg_hba.conf', '../docker/hardened/takArtifacts/db-utils/pg_hba.conf') - rename('postgresql.conf', '../docker/hardened/takArtifacts/db-utils/postgresql.conf') - rename('configureInDocker.sh', '../hardened/takArtifacts/db-utils/configureInDocker.sh') - rename('takserver-setup-db.sh', '../hardened/takArtifacts/db-utils/takserver-setup-db.sh') + from project(':takserver-core').file('docker/hardened/tak/health') + into "$buildDir/docker/hardened/takArtifacts/health" } diff --git a/src/takserver-plugin-manager/src/main/java/tak/server/plugins/service/DistributedPluginManager.java b/src/takserver-plugin-manager/src/main/java/tak/server/plugins/service/DistributedPluginManager.java index e3ba69d7..38cdd094 100644 --- a/src/takserver-plugin-manager/src/main/java/tak/server/plugins/service/DistributedPluginManager.java +++ b/src/takserver-plugin-manager/src/main/java/tak/server/plugins/service/DistributedPluginManager.java @@ -240,13 +240,13 @@ public void updateDataInPlugin(String pluginClassName, Map allRe } @Override - public PluginResponse requestDataFromPlugin(String pluginClassName, Map allRequestParams, String contentType) { + public PluginResponse requestDataFromPlugin(String pluginClassName, Map allRequestParams, String accept) { if (Strings.isNullOrEmpty(pluginClassName)) { throw new IllegalArgumentException("plugin class name is empty"); } - if (Strings.isNullOrEmpty(contentType)) { - throw new IllegalArgumentException("content type must be specified"); + if (Strings.isNullOrEmpty(accept)) { + throw new IllegalArgumentException("accept must be specified"); } AtomicInteger pluginDataRequestCounter = new AtomicInteger(); @@ -259,7 +259,7 @@ public PluginResponse requestDataFromPlugin(String pluginClassName, Map allRequestParams, String data, Str * Request data from a plugin. Implementation of this function is provided by the plugin. * */ - default PluginResponse onRequestData(Map allRequestParams, String contentType) { + default PluginResponse onRequestData(Map allRequestParams, String accept) { logger.info("requestDataFromPlugin method not implemented in plugin class " + getClass().getName() + " must be implemented in order to submit data."); return null; diff --git a/src/takserver-schemamanager/scripts/generic-cluster-database-configuration.sh b/src/takserver-schemamanager/scripts/generic-cluster-database-configuration.sh index 7cd8880c..97321c48 100644 --- a/src/takserver-schemamanager/scripts/generic-cluster-database-configuration.sh +++ b/src/takserver-schemamanager/scripts/generic-cluster-database-configuration.sh @@ -1,9 +1,19 @@ #!/bin/bash -java -jar SchemaManager.jar SetupGenericDatabase -java -jar SchemaManager.jar upgrade +DB_URL=jdbc:postgresql://${POSTGRES_HOST}:${POSTGRES_PORT}/${POSTGRES_DB} + +# Wait for the server to become available +RVAL=-1 +while [ $RVAL != 0 ];do + nc -zw3 ${POSTGRES_HOST} ${POSTGRES_PORT} + RVAL=$? + sleep 1 +done + +java -jar SchemaManager.jar -url ${DB_URL} -user ${POSTGRES_USER} -password ${POSTGRES_PASSWORD} SetupGenericDatabase +java -jar SchemaManager.jar -url ${DB_URL} -user ${POSTGRES_USER} -password ${POSTGRES_PASSWORD} upgrade if curl -sL --fail http://localhost:15021/healthz/ready -o /dev/null; then # Shutdown Istio sidecar if it exists curl -fsI -X POST http://localhost:15020/quitquitquit -fi \ No newline at end of file +fi diff --git a/src/takserver-schemamanager/src/main/java/com/bbn/tak/schema/SetupPostresGeneric.java b/src/takserver-schemamanager/src/main/java/com/bbn/tak/schema/SetupPostresGeneric.java index 02c99076..4f64f89c 100644 --- a/src/takserver-schemamanager/src/main/java/com/bbn/tak/schema/SetupPostresGeneric.java +++ b/src/takserver-schemamanager/src/main/java/com/bbn/tak/schema/SetupPostresGeneric.java @@ -22,6 +22,10 @@ public boolean execute() { boolean installSucceeded = false; String database = schemaManager.commonOptions.database; + String user = schemaManager.commonOptions.username; + if (user == null) { + user = "postgres"; + } try (Connection connection = schemaManager.getConnection()) { Statement installStatement = null; @@ -32,11 +36,11 @@ public boolean execute() sqlBuilder.append("create extension if not exists fuzzystrmatch;"); sqlBuilder.append("create extension if not exists postgis_tiger_geocoder;"); sqlBuilder.append("create extension if not exists postgis_topology;"); - sqlBuilder.append("alter schema tiger owner to postgres;"); - sqlBuilder.append("alter schema tiger_data owner to postgres;"); - sqlBuilder.append("alter schema topology owner to postgres;"); + sqlBuilder.append("alter schema tiger owner to " + user + ";"); + sqlBuilder.append("alter schema tiger_data owner to " + user + ";"); + sqlBuilder.append("alter schema topology owner to " + user + ";"); sqlBuilder.append("CREATE FUNCTION exec(text) returns text language plpgsql volatile AS $f$ BEGIN EXECUTE $1; RETURN $1; END; $f$;"); - sqlBuilder.append("SELECT exec('ALTER TABLE ' || quote_ident(s.nspname) || '.' || quote_ident(s.relname) || ' OWNER TO postgres;') FROM (SELECT nspname, relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace = n.oid) WHERE nspname in ('tiger','topology') AND relkind IN ('r','S','v') ORDER BY relkind = 'S') s;"); + sqlBuilder.append("SELECT exec('ALTER TABLE ' || quote_ident(s.nspname) || '.' || quote_ident(s.relname) || ' OWNER TO " + user + ";') FROM (SELECT nspname, relname FROM pg_class c JOIN pg_namespace n ON (c.relnamespace = n.oid) WHERE nspname in ('tiger','topology') AND relkind IN ('r','S','v') ORDER BY relkind = 'S') s;"); String sql = sqlBuilder.toString(); installStatement = connection.createStatement(); logger.debug(sql); diff --git a/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/deploy.sh b/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/deploy.sh index 02579d70..af8d9670 100644 --- a/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/deploy.sh +++ b/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/deploy.sh @@ -2,7 +2,7 @@ set -e -SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd ) +SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" > /dev/null 2>&1 && pwd ) CANDIDATE_COUNT=$(ls -1 ${SCRIPT_DIR}/../../../../takserver-cluster/build/distributions/takserver-cluster-*.zip | wc -l) if [[ "${1}" == "" ]];then @@ -37,7 +37,7 @@ DOCKERFILE="${SCRIPT_DIR}/files/Dockerfile" DOCKERCONTEXT="${SCRIPT_DIR}/files" docker build --file="${DOCKERFILE}" --tag=tak-deployer:latest "${DOCKERCONTEXT}" -docker run -it --name="${CONTAINER_NAME}" \ +docker run -it --name="${CONTAINER_NAME}-nocerts" \ --mount type=bind,source="${CLUSTER_PROPERTIES}",target=/cluster-properties,readonly \ --mount type=bind,source="${HOME}"/.aws,target=/aws-src,readonly \ --mount type=bind,source="${TAKSERVER_CERT_SOURCE}",target=/certs-src,readonly \ diff --git a/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/Dockerfile b/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/Dockerfile index 4a1c28cf..0025e1a4 100644 --- a/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/Dockerfile +++ b/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/Dockerfile @@ -5,7 +5,7 @@ ARG AWS_CLI_VERSION=2.7.11 ARG K8S_VERSION=v1.27.3 ARG HELM_VERSION=v3.12.2 -RUN apk add --no-cache unzip python3 py3-pip curl bash wget vim aws-cli mandoc +RUN apk add --no-cache unzip python3 py3-pip curl bash wget vim aws-cli mandoc git RUN python3 -m pip install pyyaml boto3 # Install kubectl @@ -22,6 +22,7 @@ RUN wget https://get.helm.sh/helm-${HELM_VERSION}-linux-amd64.tar.gz -O /tmp/hel # Install eksctl RUN curl --silent --location "https://github.com/weaveworks/eksctl/releases/download/${EKSCTL_VERSION}/eksctl_Linux_amd64.tar.gz" | tar xz -C /tmp && mv /tmp/eksctl /usr/local/bin/eksctl && chmod +x /usr/local/bin/eksctl +RUN /usr/local/bin/helm plugin install https://github.com/databus23/helm-diff COPY docker_entrypoint.sh / COPY collect-cluster-logs.py / diff --git a/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/docker_entrypoint.sh b/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/docker_entrypoint.sh index 6f289965..dc815afb 100644 --- a/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/docker_entrypoint.sh +++ b/src/takserver-takcl-core/scripts/cluster-tools/docker-deployer/files/docker_entrypoint.sh @@ -6,13 +6,17 @@ echo Copying read-only source directories to writable directories, please wait.. unzip takserver-cluster.zip cp -R /aws-src /root/.aws cp -R /cluster-properties /cluster/cluster-properties -cp -R /certs-src /cluster/takserver-core/certs/files cd /cluster source cluster-properties export CLUSTER_HOME_DIR=/cluster +if [[ "${TAK_CERT_SOURCE_DIR}" != "" ]];then + cp -R "${TAK_CERT_SOURCE_DIR}" /tak-cert-source-dir + export TAK_CERT_SOURCE_DIR=/tak-cert-source-dir +fi + echo Please execute \`python3 scripts/build-eks.py\` to build the aws cluster ${TAK_CLUSTER_NAME} bash diff --git a/src/takserver-takcl-core/scripts/cluster-tools/start-local-cluster.sh b/src/takserver-takcl-core/scripts/cluster-tools/start-local-cluster.sh index a4c648a2..968011ff 100644 --- a/src/takserver-takcl-core/scripts/cluster-tools/start-local-cluster.sh +++ b/src/takserver-takcl-core/scripts/cluster-tools/start-local-cluster.sh @@ -2,12 +2,13 @@ set -e -EXTERNAL_IP=10.2.10.2 -MK_DRIVER=docker # or docker, kvm2, virtualbox, qemu, etc. See https://minikube.sigs.k8s.io/docs/drivers/ -MK_CPU_COUNT=12 -MK_MEMORY=16g +EXTERNAL_IP=192.168.11.38 +EXTERNAL_IP=128.33.66.208 +MK_DRIVER=kvm2 # or docker, kvm2, virtualbox, qemu, etc. See https://minikube.sigs.k8s.io/docs/drivers/ +MK_CPU_COUNT=16 +MK_MEMORY=20g DOCKER_REGISTRY_PORT=4000 -ENABLE_INGRES=false +ENABLE_INGRESS=true if [[ "${TAKCL_SERVER_POSTGRES_PASSWORD}" == "" ]];then echo Please set the environment variable TAKCL_SERVER_POSTGRES_PASSWORD to a password to use this script! @@ -30,7 +31,7 @@ HELM_VERSION=v3.12.3 MINIKUBE_VERSION=v1.31.2 K8S_VERSION=v1.27.0 -SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" &> /dev/null && pwd 2> /dev/null; )" +SCRIPT_DIR="$( cd -- "$( dirname -- "${BASH_SOURCE[0]:-$0}"; )" > /dev/null 2>&1 && pwd 2> /dev/null; )" DB_IDENTIFIER=takserver-cluster-db POSTGRES_USER=postgres @@ -183,7 +184,9 @@ deploy_local() { ${MINIKUBE} start --memory=${MK_MEMORY} --cpus=${MK_CPU_COUNT} --kubernetes-version=${K8S_VERSION} --insecure-registry=${EXTERNAL_IP}:${DOCKER_REGISTRY_PORT} --driver=${MK_DRIVER} --apiserver-port 9210 if [[ "${ENABLE_INGRESS}" == "true" ]];then ${MINIKUBE} addons enable ingress + sleep 5 ${KUBECTL} patch deployment -n ingress-nginx ingress-nginx-controller --type='json' -p='[{"op": "add", "path": "/spec/template/spec/containers/0/args/-", "value":"--enable-ssl-passthrough"}]' + sleep 5 fi # Crate the custom certificate store. Make sure admin.pem exists to ensure the admin user is activated and startup completes successfully! @@ -241,14 +244,11 @@ delete_local_images() { setup_ingress() { if [[ "${ENABLE_INGRESS}" == "true" ]];then + echo Waiting 10 seconds before enabling ingress... + sleep 10 ${KUBECTL} apply -f minikube-ingress-loadbalancer.yaml - ${KUBECTL} patch configmap tcp-services -n ingress-nginx --patch '{"data":{"8443":"takserver/takserver-api-service:8443"}}' - ${KUBECTL} patch configmap tcp-services -n ingress-nginx --patch '{"data":{"8444":"takserver/takserver-api-service:8444"}}' - ${KUBECTL} patch configmap tcp-services -n ingress-nginx --patch '{"data":{"8446":"takserver/takserver-api-service:8446"}}' - ${KUBECTL} patch configmap tcp-services -n ingress-nginx --patch '{"data":{"8089":"takserver/takserver-messaging-service:8090"}}' - ${KUBECTL} patch configmap tcp-services -n ingress-nginx --patch '{"data":{"9000":"takserver/takserver-messaging-service:9000"}}' - ${KUBECTL} patch configmap tcp-services -n ingress-nginx --patch '{"data":{"9001":"takserver/takserver-messaging-service:9001"}}' ${KUBECTL} patch deployment ingress-nginx-controller -n ingress-nginx --patch "$(cat minikube-ingress-patch.yaml)" + ${MINIKUBE} service list fi } diff --git a/src/takserver-takcl-core/scripts/testrunner.sh b/src/takserver-takcl-core/scripts/testrunner.sh index ef36c366..49e41bc4 100755 --- a/src/takserver-takcl-core/scripts/testrunner.sh +++ b/src/takserver-takcl-core/scripts/testrunner.sh @@ -69,6 +69,10 @@ if [[ ! -d "${ARTIFACT_SRC}/federation-hub" ]];then mkdir "${ARTIFACT_SRC}/federation-hub" fi +if [[ ! -d "${ARTIFACT_SRC}/lib" ]];then + mkdir "${ARTIFACT_SRC}/lib" +fi + cp -r ${ARTIFACT_TEMPLATE_DIR}/* ${ARTIFACT_SRC}/ cp takserver-package/federation-hub/build/artifacts/jars/* "${ARTIFACT_SRC}/federation-hub/" cp -r takserver-package/federation-hub/build/artifacts/configs "${ARTIFACT_SRC}/federation-hub/configs" diff --git a/src/takserver-tool-ui/src/MissionManager.js b/src/takserver-tool-ui/src/MissionManager.js index 7748f618..f1d974b7 100644 --- a/src/takserver-tool-ui/src/MissionManager.js +++ b/src/takserver-tool-ui/src/MissionManager.js @@ -725,6 +725,8 @@ function MissionManager() { } else if(value === 1){ setResetDataFeeds(!resetDataFeeds); addOrEditMission(setValue, 2, setApiSent, setMissionChanged, missionChanged); + } else{ + addOrEditMission(setValue, 1, setApiSent, setMissionChanged, missionChanged); } setDrawerOpen(false); } diff --git a/src/takserver-usermanager/build.gradle b/src/takserver-usermanager/build.gradle index 226674fa..3d06002d 100644 --- a/src/takserver-usermanager/build.gradle +++ b/src/takserver-usermanager/build.gradle @@ -1,5 +1,3 @@ -import groovy.xml.MarkupBuilder - import java.nio.file.Files import java.nio.file.Paths @@ -19,7 +17,7 @@ shadowJar { dependsOn compileJava baseName = 'UserManager' classifier = 'all' - setZip64(true) + setZip64(true) version = version } @@ -108,8 +106,27 @@ task integrationTest(type: Test) { } dependencies { - implementation project(':takserver-takcl-core') - implementation project(':takserver-common') + implementation(project(':takserver-takcl-core')) { + exclude group: 'io.kubernetes', module: 'client-java' + exclude group: 'org.springframework', module: 'spring-context' + exclude group: 'org.springframework.security', module: 'spring-security-core' + exclude group: 'org.springframework', module: 'spring-context' + exclude group: 'org.springframework', module: 'spring-beans' + exclude group: 'org.springframework', module: 'spring-context' + exclude group: 'org.springframework.boot', module: 'spring-boot-starter-data-mongodb' + exclude group: 'org.hibernate.orm', module: 'hibernate-core' + exclude group: 'org.hibernate.orm', module: 'hibernate-entitymanager' + exclude group: 'org.apache.ignite', module: 'ignite-indexing' + exclude group: 'org.hibernate.orm', module: 'hibernate-core' + exclude group: 'org.apache.ignite', module: 'ignite-indexing' + exclude group: 'org.antlr', module: 'antlr4' + } + implementation(project(':takserver-common')) { + exclude group: 'org.hibernate.orm', module: 'hibernate-core' + exclude group: 'org.apache.ignite', module: 'ignite-indexing' + exclude group: 'org.antlr', module: 'antlr4' + } + implementation group: 'ch.qos.logback', name: 'logback-classic', version: logback_version implementation group: 'org.slf4j', name: 'slf4j-api', version: slf4j_version implementation group: 'org.apache.ignite', name: 'ignite-slf4j', version: ignite_version @@ -129,14 +146,14 @@ dependencies { integrationTestImplementation group: 'com.h2database', name: 'h2', version: h2_version } -task copyJar (type: Copy, dependsOn: shadowJar) { - from file('build/libs') - include 'UserManager-' + version + '-all.jar' - into "$buildDir/cluster" - rename('UserManager-' + version + '-all.jar', 'UserManager.jar') +task copyJar(type: Copy, dependsOn: shadowJar) { + from file('build/libs') + include 'UserManager-' + version + '-all.jar' + into "$buildDir/cluster" + rename('UserManager-' + version + '-all.jar', 'UserManager.jar') } -task setupCluster (type: Copy, dependsOn: copyJar) {} +task setupCluster(type: Copy, dependsOn: copyJar) {} clean { doFirst { diff --git a/src/testing/.classpath b/src/testing/.classpath deleted file mode 100644 index 9c951851..00000000 --- a/src/testing/.classpath +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/src/testing/.project b/src/testing/.project deleted file mode 100644 index 911b172c..00000000 --- a/src/testing/.project +++ /dev/null @@ -1,24 +0,0 @@ - - - MartiTestTools - - - - - - org.eclipse.jdt.core.javabuilder - - - - - - org.eclipse.jdt.core.javanature - - - - tomcat-bin - 2 - PARENT_LOC/apache-tomcat/bin - - - diff --git a/src/testing/fedhub_automation/README.md b/src/testing/fedhub_automation/README.md new file mode 100755 index 00000000..ff8670e2 --- /dev/null +++ b/src/testing/fedhub_automation/README.md @@ -0,0 +1,147 @@ + +# Introduction + +This is an ansible playbook for the purposes of automatically setting up federation hub, a number of TAK server instances, and a number of PyTAK instances on AWS. This allows for automated load testing of federation hub. + +# Prerequisites + +Ansible and python should be installed to run the playbook. The playbook was last tested with these versions: + +``` +ansible [core 2.13.13] + python version = 3.8.10 (default, Nov 22 2023, 10:22:35) [GCC 9.4.0] + jinja version = 3.1.3 + libyaml = True +``` + +# Configuration Before Running Playbook + +Before running the playbook, you should populate the variables.yml with the appropriate variable values and place the appropriate files in the same directory as setup_instances.yml. + +## Files Needed by Playbook + +There are three files you need to place in the same directory as setup_instances.yml to run the playbook: the key file to use for access to the various AWS instances, the rpm file for the version of federation hub to be tested, and the zip file containing the docker files of the takserver to be tested. + +For example, one might have the following files in the directory: +`tak-bbn-admin.pem, takserver-fed-hub-5.0-RELEASE96.noarch.rpm, takserver-docker-5.1-BETA-13.zip` + +## Generating AWS Security Credentials + +You should generate AWS security credentials before running the ansible script so that it has permission to provision AWS instances. To do so, you can download the AWS CLI and run the following command with a token from your MFA device: + +`aws sts get-session-token --serial-number --token-code XXXXXX` + +You can look at this page for more details: https://repost.aws/knowledge-center/authenticate-mfa-cli + +## Variables to Populate in variables.yml + +### aws_secret_key + +This is the AWS secret key used to access the AWS account that will be used for provisioning AWS instances. + +### aws_access_key + +This is the AWS access key used to access the AWS account that will be used for provisioning AWS instances. + +### aws_session_token + +This is the AWS session token used to access the AWS account that will be used for provisioning AWS instances. + +### key_name + +This is the name of the key file that will be used to access the AWS instances with the .pem extension omitted. It is assumed that all AWS instances will be accessible via the same key. + +### fedhub_version + +This is the name of the fedhub rpm that you will be using. This file should be in the same directory as the playbook when the playbook is run. + +### takserver_version + +This is the takserver version that you will be using. This should be the name of the takserver docker files zip with the .zip extension omitted. The takserver docker files zip should be in the same directory as the playbook when the playbook is run. + +### takserver_num_instances + +This is the number of takserver instances to provision. + +### pytak_num_instances + +This is the number of pytak instances to provision per takserver. + +### clients + +This is the number of clients that each pytak instance will simulate. + +### self_sa_delta + +This is the position reporting frequency in seconds that each pytak client will simulate. + +### fedhub_instance_type + +This is the AWS instance type that will be used for the fed hub instance. + +### takserver_instance_type + +This is the AWS instance type that will be used for tak server instances. + +### pytak_instance_type + +This is the AWS instance type that will be used for pytak client instances. + +### fedhub_ami_id + +This is the AMI id that will be used for the fed hub instance. The AMI id should be for a machine running Rocky Linux 8. + +### takserver_ami_id + +This is the AMI id that will be used for tak server instances. The AMI id should be for a machine running Amazon Linux. + +### pytak_ami_id + +This is the AMI id that will be used for pytak client instances. The AMI id should be for a machine running CentOS 7. + +### connection_index + +This is how many federates each federate will be connected to during the generation of the fed hub policy. If set to 0, there will be no connections. If it is greater than or equal to the total number of tak servers minus one, every federate will be connected to every other federate. The TAK servers are numbered starting at 0 and the connections will be made in increasing order starting from the next TAK server (so if there are 3 TAK servers and a connection index of 1 is configured, 0 will be connected to 1, 1 will be connected to 2, 2 will be connected to 3, and 3 will be connected to 0). + +### instance_name_prefix + +This is what the instances created by the ansible script will have their names prefixed with. The script will automatically append a "_" after this prefix. + +## An example variables.yml + +``` +aws_secret_key: VKJ!1k4jvk1lvlk1jV!lk24jvk1vjl1!V1jk +aws_access_key: @#JK$J^FK@J#FKJ@FK@ +aws_session_token: IQoJb3JpZ2luX2VjEMD//////////fjafjkjk4DfkjakdkDdkfjk126j1k4KfjakskKtSoudHxuBasZTEZYDtGzzlBt0F9bU5aukUNv5FNxVoq7wEIeRAEGgwzMTc3NjA2MTE2NjYiDJ6OckxFh82Alxpm7CrMAWqZ8E6kVqYXN6aEuiSwgJUP5MQI9W2EXnemxaoLjnOEl/o8sMvXKKNqi/PqkggD+hK6/z/MiJQRm37zsVnR1O3KyuZiQpcU96p8d7wWG5MIheK7NHMWFkaviqm6kVGayowJdepyQWGus3ppcs1OmVVttoKEc8NCMyQmTY4KrhEUl3QlDjCzQMl9AAwVdBJt/X9uRWWrx8tHAsgq+FmJhXHK0Mp+yXEhXUbAkS7scue75LALasrN/zMrljtKOgHWgpHj07kN5Wh5QhZPujDmupC0BjqYAas8Mogndeg9YIYElj+t+rRmRHBBfjcgQnxhO/0rjuFrsHhrzGiM2qe6yxlvEG+CkRPqUPxHn0L9IHRZLyBIY/MXCdaTbBgxIK/3Xdohk68OOn9nLnTAYrisulNEf8uGZvzVEfvV2f2ifjtwRK3nP2dxCXI5lcP402P0q8onvR84Buk26D2xIXYMDR08dYgiduC+Yukt1gE0 +key_name: tak-bbn-admin +fedhub_version: takserver-fed-hub-5.0-RELEASE96.noarch.rpm +takserver_version: takserver-docker-5.1-BETA-13 +takserver_num_instances: 4 # number of takserver instances to provision +pytak_num_instances: 2 # number of pytak instances to provision per takserver instance +clients: 5 # number of clients for each pytak client to simulate +self_sa_delta: .5 # frequency in seconds for position reporting by pytak simulated clients +fedhub_instance_type: c5.4xlarge +takserver_instance_type: c5.xlarge +pytak_instance_type: t2.medium +fedhub_ami_id: ami-027bcdee875d9ac1a +takserver_ami_id: ami-00fd0e043ae988157 +pytak_ami_id: ami-0f8604d85567e0aba +connection_index: 2 # this will connect every federate to two other federates +instance_name_prefix: automation_test_elu +``` + +# Running the Playbook + +To run the playbook, simply run the following command: + +ansible-playbook setup_instances.yml + +# Accessing Fed Hub After Playbook is Run + +To access the federation hub UI once the playbook is run, find the AWS instance with the name containing "fedhub_automation_test_fedhub_", and get its IP address. + +You also need to make sure to load the credentials for the federation hub into your browser. You can find these credentials in fedhub_config_files/fedhub_files. You will need to load admin.p12 into your browser to access the federation hub instance's UI. The password for the file is "atakatak". + +Once you have the credentials loaded, you can visit this url to view the federation hub UI and see traffic flowing: + +`https://:9100` \ No newline at end of file diff --git a/src/testing/fedhub_automation/ansible.cfg b/src/testing/fedhub_automation/ansible.cfg new file mode 100755 index 00000000..d6db5851 --- /dev/null +++ b/src/testing/fedhub_automation/ansible.cfg @@ -0,0 +1,3 @@ +[defaults] +host_key_checking = False +callbacks_enabled = profile_tasks \ No newline at end of file diff --git a/src/testing/fedhub_automation/fedhub_config_files/federation-hub-broker.yml b/src/testing/fedhub_automation/fedhub_config_files/federation-hub-broker.yml new file mode 100755 index 00000000..db2b29b5 --- /dev/null +++ b/src/testing/fedhub_automation/fedhub_config_files/federation-hub-broker.yml @@ -0,0 +1,46 @@ +BkeystoreType: JKS +keystoreFile: /opt/tak/federation-hub/certs/files/takserver.jks +keystorePassword: atakatak + +truststoreType: JKS +truststoreFile: /opt/tak/federation-hub/certs/files/fed-truststore.jks +truststorePassword: atakatak + +keyManagerType: SunX509 + +# v1 federation only. +v1Enabled: true +v1Port: 9101 +useEpoll: true +context: TLSv1.2 +allow128cipher: true +allowNonSuiteB: true +enableOCSP: false +tlsVersions: + - TLSv1.2 + - TLSv1.3 + +# v2 federation only. +v2Enabled: true +v2Port: 9102 +maxMessageSizeBytes: 268435456 +metricsLogIntervalSeconds: 5 +clientTimeoutTime: 15 +clientRefreshTime: 5 +enableHealthCheck: true +useCaGroups: true + +dbUsername: martiuser +dbPassword: atakatak + +dbPort: 27017 +dbHost: localhost +dbConnectionTimeoutMS: 5000 + +# mission federation DB retention days - how long to keep mission events in the database before permanently deleting them +missionFederationDBRetentionDays: 7 +# mission federation recency seconds - how far back to look for offline changes (default 12 hours) +missionFederationRecencySeconds: 43200 +missionFederationDisruptionEnabled: false +missionFederationDisruptionMaxFileSizeBytes: 200 + diff --git a/src/testing/fedhub_automation/fedhub_config_files/federation-hub-ui.yml b/src/testing/fedhub_automation/fedhub_config_files/federation-hub-ui.yml new file mode 100755 index 00000000..ed9afce5 --- /dev/null +++ b/src/testing/fedhub_automation/fedhub_config_files/federation-hub-ui.yml @@ -0,0 +1,29 @@ +keystoreType: JKS +keystoreFile: /opt/tak/federation-hub/certs/files/takserver.jks +keystorePassword: atakatak + +truststoreType: JKS +truststoreFile: /opt/tak/federation-hub/certs/files/fed-truststore.jks +truststorePassword: atakatak + +keyAlias: takserver + +authUsers: /opt/tak/federation-hub/authorized_users.yml + +port: 9100 + +allowOauth: false +oauthPort: 8446 +keycloakServerName: +keycloakDerLocation: /opt/tak/certs/keycloak.der +keycloakClientId: +keycloakSecret: +keycloakrRedirectUri: https://localhost:8446/login/redirect +keycloakAuthEndpoint: +keycloakTokenEndpoint: +keycloakAccessTokenName : access_token +keycloakRefreshTokenName : refresh_token +keycloakClaimName: +keycloakAdminClaimValue: + +enableFlowIndicators: true \ No newline at end of file diff --git a/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/config-takserver.cfg b/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/config-takserver.cfg new file mode 100755 index 00000000..f960636c --- /dev/null +++ b/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/config-takserver.cfg @@ -0,0 +1,49 @@ + +default_crl_days= 730 # how long before next CRL + +[ ca ] + default_ca = CA_default # The default ca section + +[ CA_default ] + dir = . # Where everything is kept + certs = $dir # Where the issued certs are kept + crl_dir = $dir/crl # Where the issued crl are kept + database = $dir/crl_index.txt # database index file. + default_md = default # use public key default MD + +[ req ] + default_bits = 2048 + default_keyfile = ca.pem + distinguished_name = req_distinguished_name + x509_extensions = v3_ca + +[ req_distinguished_name ] + countryName_min = 2 + countryName_max = 2 + commonName_max = 64 + +[ v3_ca ] +#basicConstraints=critical,CA:TRUE, pathlen:2 +basicConstraints=critical,CA:TRUE +keyUsage=critical, cRLSign, keyCertSign +#nameConstraints=critical,permitted;DNS:.bbn.com # this allows you to restrict a CA to only issue server certs for a particular domain + +[ client ] +basicConstraints=critical,CA:FALSE +keyUsage=critical, digitalSignature, keyEncipherment +extendedKeyUsage = critical, clientAuth +#extendedKeyUsage = critical, clientAuth, challengePassword +#authorityInfoAccess = OCSP;URI: http://localhost:4444 + +[ server ] +basicConstraints=critical,CA:FALSE +keyUsage=critical, digitalSignature, keyEncipherment +extendedKeyUsage = critical, clientAuth, serverAuth +#authorityInfoAccess = OCSP;URI: http://localhost:4444 + + +subjectAltName = @alt_names + +[alt_names] +DNS.1 = takserver + diff --git a/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/crl_index.txt b/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/crl_index.txt new file mode 100755 index 00000000..e69de29b diff --git a/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/crl_index.txt.attr b/src/testing/fedhub_automation/fedhub_config_files/fedhub_files/crl_index.txt.attr new file mode 100755 index 00000000..e69de29b diff --git a/src/testing/fedhub_automation/fedhub_config_files/setup_x11_for_tak_user.sh b/src/testing/fedhub_automation/fedhub_config_files/setup_x11_for_tak_user.sh new file mode 100755 index 00000000..244d2e46 --- /dev/null +++ b/src/testing/fedhub_automation/fedhub_config_files/setup_x11_for_tak_user.sh @@ -0,0 +1,8 @@ + +#!/bin/bash + +sudo cp .Xauthority /opt/tak/ +sudo chown tak /opt/tak/.Xauthority +sudo chmod +rx . +sudo chmod +rx visualvm_218 +sudo chmod +rx visualvm_218/bin diff --git a/src/testing/fedhub_automation/fedhub_config_files/ui_generated_policy.json b/src/testing/fedhub_automation/fedhub_config_files/ui_generated_policy.json new file mode 100755 index 00000000..6a1f758e --- /dev/null +++ b/src/testing/fedhub_automation/fedhub_config_files/ui_generated_policy.json @@ -0,0 +1,699 @@ +{ + "name": "Testing", + "federate_edges": [ + { + "source": "CN=takserver_20240702T095507284890_0,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-15b5f560e4665b9e2a3c5e20c3837a90a688fe3a5fb233737721f3d2372336b8", + "destination": "CN=takserver_20240702T095507284890_1,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-de5e2cdd11ced24e3dfa3c99d1ba8b7a8eadff4d7b723055fec1ca2aa0106bcb", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_1,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-de5e2cdd11ced24e3dfa3c99d1ba8b7a8eadff4d7b723055fec1ca2aa0106bcb", + "destination": "CN=takserver_20240702T095507284890_0,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-15b5f560e4665b9e2a3c5e20c3837a90a688fe3a5fb233737721f3d2372336b8", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_1,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-de5e2cdd11ced24e3dfa3c99d1ba8b7a8eadff4d7b723055fec1ca2aa0106bcb", + "destination": "CN=takserver_20240702T095507284890_2,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-4770bf1865f7efd6e9e6a1db4bd8f58c8cbc3294aef2a7f89a8cf1649f3dc75e", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_2,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-4770bf1865f7efd6e9e6a1db4bd8f58c8cbc3294aef2a7f89a8cf1649f3dc75e", + "destination": "CN=takserver_20240702T095507284890_1,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-de5e2cdd11ced24e3dfa3c99d1ba8b7a8eadff4d7b723055fec1ca2aa0106bcb", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_2,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-4770bf1865f7efd6e9e6a1db4bd8f58c8cbc3294aef2a7f89a8cf1649f3dc75e", + "destination": "CN=takserver_20240702T095507284890_3,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-a58ba359ab428b59c050f5aeb42820f851c182ff47e2f461156d6dc533a6b63f", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_3,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-a58ba359ab428b59c050f5aeb42820f851c182ff47e2f461156d6dc533a6b63f", + "destination": "CN=takserver_20240702T095507284890_2,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-4770bf1865f7efd6e9e6a1db4bd8f58c8cbc3294aef2a7f89a8cf1649f3dc75e", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_0,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-15b5f560e4665b9e2a3c5e20c3837a90a688fe3a5fb233737721f3d2372336b8", + "destination": "CN=takserver_20240702T095507284890_3,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-a58ba359ab428b59c050f5aeb42820f851c182ff47e2f461156d6dc533a6b63f", + "groupsFilterType": "ALL" + }, + { + "source": "CN=takserver_20240702T095507284890_3,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-a58ba359ab428b59c050f5aeb42820f851c182ff47e2f461156d6dc533a6b63f", + "destination": "CN=takserver_20240702T095507284890_0,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-15b5f560e4665b9e2a3c5e20c3837a90a688fe3a5fb233737721f3d2372336b8", + "groupsFilterType": "ALL" + } + ], + "groups": [ + { + "uid": "CN=takserver_20240702T095507284890_0,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-15b5f560e4665b9e2a3c5e20c3837a90a688fe3a5fb233737721f3d2372336b8", + "interconnected": false + }, + { + "uid": "CN=takserver_20240702T095507284890_1,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-de5e2cdd11ced24e3dfa3c99d1ba8b7a8eadff4d7b723055fec1ca2aa0106bcb", + "interconnected": false + }, + { + "uid": "CN=takserver_20240702T095507284890_2,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-4770bf1865f7efd6e9e6a1db4bd8f58c8cbc3294aef2a7f89a8cf1649f3dc75e", + "interconnected": false + }, + { + "uid": "CN=takserver_20240702T095507284890_3,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-a58ba359ab428b59c050f5aeb42820f851c182ff47e2f461156d6dc533a6b63f", + "interconnected": false + } + ], + "additionalData": { + "uiData": { + "name": "Testing", + "version": "", + "type": "", + "description": "Testing", + "thumbnail": null, + "cells": [ + { + "graphType": "GroupCell", + "id": "d5082a90-d386-4e73-8519-603b94a77d38", + "attrs": { + ".body": { + "fill": "white", + "opacity": "0.35" + }, + ".inner": { + "visibility": "hidden" + }, + "path": { + "ref": ".outer" + }, + "image": { + "ref": ".outer", + "ref-dy": "", + "ref-y": 5, + "xlink:href": "data:image/png;base64,MIIDwDCCAqigAwIBAgIUAmkmFvVf9jRkmof8wEA9g8ZuWKMwDQYJKoZIhvcNAQELBQAweDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtTaWduYWxfSGlsbDEMMAoGA1UECgwDSVNTMQwwCgYDVQQLDANCQk4xKjAoBgNVBAMMIXRha3NlcnZlcl8yMDI0MDcwMlQwOTU1MDcyODQ4OTBfMDAeFw0yNDA3MDIxNzAyMTNaFw0zNDA3MDIxNzAyMTNaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2lnbmFsX0hpbGwxDDAKBgNVBAoMA0lTUzEMMAoGA1UECwwDQkJOMSowKAYDVQQDDCF0YWtzZXJ2ZXJfMjAyNDA3MDJUMDk1NTA3Mjg0ODkwXzAwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCxNFf2XYLmrAxImqPMkPH7TsKHwWcbbFAW3bL55zeJX+uAH2byxt54387Krkc6pT3eH8xeljHJt+3aKQlSKq6q3idQbyt7IXF2GTrFryE2TFCib8fNEAqLcH9Q8aNVEEMP5UI8ts+AOl+Q7mlQF72SoVNJWTci4ZF6SL0Enq/1/R4qoYf5rtxK5nbNmIl9sQFYZ8S0EbTIGX91HIptp1/x/oYpqwmU5gU9OPCG3Ai9J6ORft1otfDW+pFJm5moXA5aLKHwRo6M8k2fKCFZCAEimm9jYQnsKEhHHFcLVmvL/y/BDrcw291sW1h3h/YvVMYrOhIEpWhQYcZF0Nt8Le1/AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRilhoMN/yJwHEz+++Rv4Ug0rdq7TANBgkqhkiG9w0BAQsFAAOCAQEAQaIUEfPaDf1x9fbGo/o0QwKnRbpDgF7ir/7xQ7/FcTCXVDBNIGKTmi1XUNSvdDwJMKLJiVmT64BMnrAD3hdASkxYQYynFcBRfu2rlwcvKpPoZNBr0XGcWxmIz/RRfNsUAT2ZtbfCTFXspDwVQrNk/+1bDXmTY83EhZVylSCW/gFqD0v95j0QrIvjQzSK2gEgV3jlV4rTKr3MDmg57kV1UnkTRNTgvoupzHPMZDo6KOIfarnUcUoXrpnUsASqY2O1aoOXUl9KDXsZTDU0e47u/TEcRh7YmeUfjZsazTc//xOue+pYfkZrz3XPRwIMjvZ0Ep+VD9zQLzOI3AZr4NtLyg==" + }, + "text": { + "ref-y": 0.5 + }, + ".content": { + "text": "BBN0" + }, + ".fobj": { + "width": 200, + "height": 150 + }, + "div": { + "style": { + "width": 200, + "height": 150 + } + }, + ".fobj div": { + "style": { + "verticalAlign": "middle", + "paddingTop": 0 + } + }, + ".outer": { + "stroke-width": 1, + "stroke-dasharray": "none" + }, + ".sub-process": { + "visibility": "hidden", + "data-sub-process": "" + } + }, + "type": "bpmn.Activity", + "roger_federation": { + "name": "CN=takserver_20240702T095507284890_0,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-15b5f560e4665b9e2a3c5e20c3837a90a688fe3a5fb233737721f3d2372336b8", + "id": null, + "description": "BBN0", + "attributes": [], + "interconnected": false, + "groupFilters": [], + "type": "Group", + "stringId": "BBN0" + }, + "size": { + "width": 200, + "height": 150 + }, + "activeConnections": [], + "icon": "circle", + "angle": 0, + "z": "0", + "position": { + "x": 1064, + "y": 1346 + }, + "activityType": "task", + "embeds": "", + "content": "BBN0" + }, + { + "graphType": "GroupCell", + "id": "86cf84ac-e706-42be-a35c-0ec25b9430f4", + "attrs": { + ".body": { + "fill": "white", + "opacity": "0.35" + }, + ".inner": { + "visibility": "hidden" + }, + "path": { + "ref": ".outer" + }, + "image": { + "ref": ".outer", + "ref-dy": "", + "ref-y": 5, + "xlink:href": "data:image/png;base64,MIIDwDCCAqigAwIBAgIUGUjHE6cu7yhflBDKuRfRk0UVO4YwDQYJKoZIhvcNAQELBQAweDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtTaWduYWxfSGlsbDEMMAoGA1UECgwDSVNTMQwwCgYDVQQLDANCQk4xKjAoBgNVBAMMIXRha3NlcnZlcl8yMDI0MDcwMlQwOTU1MDcyODQ4OTBfMTAeFw0yNDA3MDIxNzAyMThaFw0zNDA3MDIxNzAyMThaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2lnbmFsX0hpbGwxDDAKBgNVBAoMA0lTUzEMMAoGA1UECwwDQkJOMSowKAYDVQQDDCF0YWtzZXJ2ZXJfMjAyNDA3MDJUMDk1NTA3Mjg0ODkwXzEwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCojixmVdB3AmxBTfFxRS92UXchONqSO0PvO2aTnn5vJdGISi7S3S7Um3s6HSB7NHgKs2nz42uhafwLv5HL1ZAYGMlD/jW7Y43lOz2b3UsP5XnFnmPJyc20OQ6D2biS4jJVjBGq6AbVg/Lp6nmimfaRI4I4Ft+dxb6/6UWiL/f9vEY5qncAQiaEon2mW/UBSQ2TqEppoh0v/0t7XcbkXczEXrbhPI53egMOHBTs3A+w4S6NCGegZec6AdvT5MO6rMQRDLoS8gEVFm6oosoPCY5SGF05/3qmlxSeggGzrbFKKmtBCEazuBoLkGRPhsQCMJ64J5Ag4WuXw2bKsg38MEnJAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBRpPd1hPyxXZkRTrRDKKNFWkaGG1DANBgkqhkiG9w0BAQsFAAOCAQEANQgjmFZTf34Ft/8BtpEvgvqfvQUpZoWutanvdsiQ7/8Mit6X1PaeF6Sy6P+yWRBJGAMbqh2h0IfUul2dyAh07oRAOBeCN/BJQKcpohZgfTlX9bGyMg+pb/rLj2kG7+RLUj1hf3CqeBdnjtV3FMvfAl60CfvYZVmO96d/ZNAt8Rl3pe50dzp57zrWmbPSIW/XP4HNYgwrK2D5yh/gfkc6kLETTnvPIjfymVUIx3DINnK8UGfmxaroMMja2okEm0ssm2hD89FUVOPOjpBsk39EYqrlKwlxNsSP89rvjmYxjTQSwsnIYm7CgMjX+3UJcCVh7tsy4kK+RmEHDI5cLatoGg==" + }, + "text": { + "ref-y": 0.5 + }, + ".content": { + "text": "BBN1" + }, + ".fobj": { + "width": 200, + "height": 150 + }, + "div": { + "style": { + "width": 200, + "height": 150 + } + }, + ".fobj div": { + "style": { + "verticalAlign": "middle", + "paddingTop": 0 + } + }, + ".outer": { + "stroke-width": 1, + "stroke-dasharray": "none" + }, + ".sub-process": { + "visibility": "hidden", + "data-sub-process": "" + } + }, + "type": "bpmn.Activity", + "roger_federation": { + "name": "CN=takserver_20240702T095507284890_1,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-de5e2cdd11ced24e3dfa3c99d1ba8b7a8eadff4d7b723055fec1ca2aa0106bcb", + "id": null, + "description": "BBN1", + "attributes": [], + "interconnected": false, + "groupFilters": [], + "type": "Group", + "stringId": "BBN1" + }, + "size": { + "width": 200, + "height": 150 + }, + "activeConnections": [], + "icon": "circle", + "angle": 0, + "z": "1", + "position": { + "x": 1064, + "y": 1346 + }, + "activityType": "task", + "embeds": "", + "content": "BBN1" + }, + { + "graphType": "GroupCell", + "id": "b97168b4-08a5-422a-a24e-17e427c0cba3", + "attrs": { + ".body": { + "fill": "white", + "opacity": "0.35" + }, + ".inner": { + "visibility": "hidden" + }, + "path": { + "ref": ".outer" + }, + "image": { + "ref": ".outer", + "ref-dy": "", + "ref-y": 5, + "xlink:href": "data:image/png;base64,MIIDwDCCAqigAwIBAgIUPMp8keSzB3Tj//EovIaAUHqkw/swDQYJKoZIhvcNAQELBQAweDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtTaWduYWxfSGlsbDEMMAoGA1UECgwDSVNTMQwwCgYDVQQLDANCQk4xKjAoBgNVBAMMIXRha3NlcnZlcl8yMDI0MDcwMlQwOTU1MDcyODQ4OTBfMjAeFw0yNDA3MDIxNzAyMjRaFw0zNDA3MDIxNzAyMjRaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2lnbmFsX0hpbGwxDDAKBgNVBAoMA0lTUzEMMAoGA1UECwwDQkJOMSowKAYDVQQDDCF0YWtzZXJ2ZXJfMjAyNDA3MDJUMDk1NTA3Mjg0ODkwXzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCWSu+w2yRT4h2ZldmTk9CJj8RFO/ZkcNLdaiSgbPFNeg1sxtZ04mOpWXIAQYXm4bkbHETDxMe8p89ckPAs16uyJAmNdkSIPYdN6HUJNn8Ry2yygErnXrgXHpJTRAyh0nFE02hdaweGk35hfvkLISd8Lb4qb/zi4hNJPunyQh9yyuOwYk/1HshTutc9E5CB/ja9+Jg29vZDghl3n5gEwS9zImHi9azxQEUTkKnNWjSjPZXjO512TNgJ9j+2M8Grfq7sTNSxmvsoPr/wa2Ac6aMfbzLXGbDUrEdOSuZPLulmAZJ0cUtPjPAfz9lb7kfbX4US0fN+vj6YNIkJICsZijoLAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSDr07bOuWQV/26vVGiR0EEFnLi5TANBgkqhkiG9w0BAQsFAAOCAQEAY7T3eZ1VDWO1kQ9K3QmFXi0nc6qWxOAbjy8jEU5++pY1H/QRkKb5BNu6rfO4vScmLK9I05tv2jN4qFOaEF0Wn6QRHpmbjtS0hzdQhIsCKwAxxcB5m769DQNsDjt7uxwnyESwtqFhRgJv7vWotKeDWsXB8DDVh/MMBm7DKDrF95eKf/qKz6n2yMnIqjHyYAHPqj/PCNMiDqf/ITsYrdtp6S7wJ9V+agnXRKnzjyDzddbwtfRvfXr66pcmKB4+G1XU/ejlignmuxBSGSLCdcjsp1vvWPs0RnS/N4VfeVoc9vx75H7Gc5DsmHyRCgEY5fXvePHP9GVsHRXpxUhcEJ7gQw==" + }, + "text": { + "ref-y": 0.5 + }, + ".content": { + "text": "BBN2" + }, + ".fobj": { + "width": 200, + "height": 150 + }, + "div": { + "style": { + "width": 200, + "height": 150 + } + }, + ".fobj div": { + "style": { + "verticalAlign": "middle", + "paddingTop": 0 + } + }, + ".outer": { + "stroke-width": 1, + "stroke-dasharray": "none" + }, + ".sub-process": { + "visibility": "hidden", + "data-sub-process": "" + } + }, + "type": "bpmn.Activity", + "roger_federation": { + "name": "CN=takserver_20240702T095507284890_2,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-4770bf1865f7efd6e9e6a1db4bd8f58c8cbc3294aef2a7f89a8cf1649f3dc75e", + "id": null, + "description": "BBN2", + "attributes": [], + "interconnected": false, + "groupFilters": [], + "type": "Group", + "stringId": "BBN2" + }, + "size": { + "width": 200, + "height": 150 + }, + "activeConnections": [], + "icon": "circle", + "angle": 0, + "z": "2", + "position": { + "x": 1064, + "y": 1346 + }, + "activityType": "task", + "embeds": "", + "content": "BBN2" + }, + { + "graphType": "GroupCell", + "id": "00a19d37-b65a-40c7-ab8c-73ec58eac700", + "attrs": { + ".body": { + "fill": "white", + "opacity": "0.35" + }, + ".inner": { + "visibility": "hidden" + }, + "path": { + "ref": ".outer" + }, + "image": { + "ref": ".outer", + "ref-dy": "", + "ref-y": 5, + "xlink:href": "data:image/png;base64,MIIDwDCCAqigAwIBAgIUddq9bPb0RtQE/5AGIjsLpie3he0wDQYJKoZIhvcNAQELBQAweDELMAkGA1UEBhMCVVMxCzAJBgNVBAgMAkNBMRQwEgYDVQQHDAtTaWduYWxfSGlsbDEMMAoGA1UECgwDSVNTMQwwCgYDVQQLDANCQk4xKjAoBgNVBAMMIXRha3NlcnZlcl8yMDI0MDcwMlQwOTU1MDcyODQ4OTBfMzAeFw0yNDA3MDIxNzAyMjlaFw0zNDA3MDIxNzAyMjlaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2lnbmFsX0hpbGwxDDAKBgNVBAoMA0lTUzEMMAoGA1UECwwDQkJOMSowKAYDVQQDDCF0YWtzZXJ2ZXJfMjAyNDA3MDJUMDk1NTA3Mjg0ODkwXzMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCbMKnbFlt2ucLxnW5LBRX4TxIBl43wBm+/PYVh4mLrNHdG3o5WK2XBGPTW/ymqi4I9XqguTkmTArYRNM/HDor9Q5EK9esxgGvO6oAUn7DaFuCmyElDrb5ZSikV0Bt8f/Ic2C6piDeCJhc+vqFAr4U3OOrqe7P4fQGZwP6KYpdjA/yeqh6Ku4PMMV8T9v0e09LtFeD6kEr0vustTJKYoiUQaWKVUL1e1wNpEiZv15dKPPVSFtSYJf7PPf8wu+KrzwIneCbx5MpZgtbP6iPIWnLaN7p7nn8aDLOs0rkxPLiYos0dmQ5O2DV8GJWY4yFlhccii43T0iHf5auNx5bXvcghAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBS4n0uRWVcPSZTDla55ShYN8VgmlTANBgkqhkiG9w0BAQsFAAOCAQEAYmJPcIb3s6kQoBpGxp7oZ/urWAqaa/QtjIP49/Q+TE7fQ7uWdFAemlDC55bMIzYmpT47FQ4oTQjlfT2Cg97yP3XpC5VXV2dgmevPEdC/GoUKU/4M33K7ZJIUJ2RZLYjMiUroqBfpGYL6k3AHgwzuwLGv/KrTXzT81o52LKyivxvnNxYdV0kcvJ63ZImEeTejBq4hqJ64bcEyn7Ib+XWF7c3uQI5DOoWZWnIqYQotXWrQmEwrL1Ojr5XCeueKzCYT6p03y/P/9jTwGz8tf3+lELEDJbBsQ8CFzonJEX/53ixXAFL5VbXCSx9y3XSDhqee8TipuPZ7G9SWuPWnT2oUyQ==" + }, + "text": { + "ref-y": 0.5 + }, + ".content": { + "text": "BBN3" + }, + ".fobj": { + "width": 200, + "height": 150 + }, + "div": { + "style": { + "width": 200, + "height": 150 + } + }, + ".fobj div": { + "style": { + "verticalAlign": "middle", + "paddingTop": 0 + } + }, + ".outer": { + "stroke-width": 1, + "stroke-dasharray": "none" + }, + ".sub-process": { + "visibility": "hidden", + "data-sub-process": "" + } + }, + "type": "bpmn.Activity", + "roger_federation": { + "name": "CN=takserver_20240702T095507284890_3,OU=BBN,O=ISS,L=Signal_Hill,ST=CA,C=US-a58ba359ab428b59c050f5aeb42820f851c182ff47e2f461156d6dc533a6b63f", + "id": null, + "description": "BBN3", + "attributes": [], + "interconnected": false, + "groupFilters": [], + "type": "Group", + "stringId": "BBN3" + }, + "size": { + "width": 200, + "height": 150 + }, + "activeConnections": [], + "icon": "circle", + "angle": 0, + "z": "3", + "position": { + "x": 1064, + "y": 1346 + }, + "activityType": "task", + "embeds": "", + "content": "BBN3" + }, + { + "graphType": "EdgeCell", + "id": "5dc0a5c2-9a61-4e8a-8e33-5a985cf5a4a3", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN0 -> BBN1", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "4", + "source": { + "id": "d5082a90-d386-4e73-8519-603b94a77d38" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "86cf84ac-e706-42be-a35c-0ec25b9430f4" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN0 -> BBN1" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "639f728f-623c-42e8-adfa-9b1c60df7002", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN1 -> BBN0", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "5", + "source": { + "id": "86cf84ac-e706-42be-a35c-0ec25b9430f4" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "d5082a90-d386-4e73-8519-603b94a77d38" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN1 -> BBN0" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "f0560bb6-a2f6-4a0b-b00e-a8776d0fe983", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN1 -> BBN2", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "6", + "source": { + "id": "86cf84ac-e706-42be-a35c-0ec25b9430f4" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "b97168b4-08a5-422a-a24e-17e427c0cba3" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN1 -> BBN2" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "01f0acf0-0389-48e3-b95e-ccdc1fb48d18", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN2 -> BBN1", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "7", + "source": { + "id": "b97168b4-08a5-422a-a24e-17e427c0cba3" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "86cf84ac-e706-42be-a35c-0ec25b9430f4" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN2 -> BBN1" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "b6c429ee-1624-4ad0-86ef-5e6005606800", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN2 -> BBN3", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "8", + "source": { + "id": "b97168b4-08a5-422a-a24e-17e427c0cba3" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "00a19d37-b65a-40c7-ab8c-73ec58eac700" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN2 -> BBN3" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "41658263-fad5-4c71-8ffa-3a22d2a53168", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN3 -> BBN2", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "9", + "source": { + "id": "00a19d37-b65a-40c7-ab8c-73ec58eac700" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "b97168b4-08a5-422a-a24e-17e427c0cba3" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN3 -> BBN2" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "58f9b4e2-d62c-4c1d-9c89-9d8f680a4976", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN0 -> BBN3", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "10", + "source": { + "id": "d5082a90-d386-4e73-8519-603b94a77d38" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "00a19d37-b65a-40c7-ab8c-73ec58eac700" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN0 -> BBN3" + } + } + } + ] + }, + { + "graphType": "EdgeCell", + "id": "254f40be-5a98-4514-9cb2-374235c9e580", + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": "BBN3 -> BBN0", + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": "11", + "source": { + "id": "00a19d37-b65a-40c7-ab8c-73ec58eac700" + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": "d5082a90-d386-4e73-8519-603b94a77d38" + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": "BBN3 -> BBN0" + } + } + } + ] + } + ], + "diagramType": "Federation" + } + } +} diff --git a/src/testing/fedhub_automation/setup_instances.yaml b/src/testing/fedhub_automation/setup_instances.yaml new file mode 100755 index 00000000..af974b81 --- /dev/null +++ b/src/testing/fedhub_automation/setup_instances.yaml @@ -0,0 +1,450 @@ + +- name: aws var setup + hosts: localhost + tasks: + - set_fact: + region: us-east-1 + availability_zone: us-east-1a + timestamp: "{{ ansible_date_time.iso8601_basic }}" + +- name: fedhub var setup + hosts: localhost + vars_files: + - variables.yml + tasks: + - set_fact: + fedhub_instance_name_prefix: "{{ instance_name_prefix }}_fedhub_" + fedhub_config_files_dir: "fedhub_config_files" + fedhub_instances_group: fedhub_instances + fedhub_user_name: rocky + fedhub_generated_files_folder: "fedhub_gen_files" + +- name: takserver var setup + hosts: localhost + vars_files: + - variables.yml + tasks: + - set_fact: + takserver_instance_name_prefix: "{{ instance_name_prefix }}_takserver_" + takserver_config_files_dir: "takserver_config_files" + takserver_user_name: ec2-user + takserver_instances_group: takserver_instances + - set_fact: + takserver_certs_dir: "certs" + takserver_base_config_dir: "base_takserver_config" + pytak_config_files_dir: "pytak_config_files" + load_testing_cfgs_dir: "load_testing_cfgs" + +- name: pytak var setup + hosts: localhost + vars_files: + - variables.yml + tasks: + - set_fact: + pytak_instances_group: pytak_instances + pytak_user_name: centos + pytak_instance_name_prefix: "{{ instance_name_prefix }}_pytak_" + +- name: copy files to appropriate places + hosts: localhost + vars_files: + - variables.yml + gather_facts: true + remote_user: "{{ takserver_user_name }}" + tasks: + - name: copy files to appropriate places + ansible.builtin.shell: + cmd: | + rm {{ takserver_config_files_dir }}/{{ takserver_base_config_dir }}/*.pem + rm {{ takserver_config_files_dir }}/{{ takserver_base_config_dir }}/*.zip + rm {{ fedhub_config_files_dir }}/*.rpm + cp -n {{ key_name}}.pem {{ takserver_config_files_dir }}/{{ takserver_base_config_dir }}/ + cp -n {{ takserver_version }}.zip {{ takserver_config_files_dir }}/{{ takserver_base_config_dir }}/ + cp -n {{ fedhub_version }} {{ fedhub_config_files_dir }}/ + delegate_to: localhost + +- name: provisioning fedhub instance + hosts: localhost + vars_files: + - variables.yml + gather_facts: true + collections: + - community.aws + - amazon.aws + remote_user: "{{ fedhub_user_name }}" + tasks: + - name: provision + ec2_instance: + name: "{{ fedhub_instance_name_prefix + timestamp }}" + key_name: "{{ key_name }}" + instance_type: "{{ fedhub_instance_type }}" + image_id: "{{ fedhub_ami_id }}" + security_group: sg-b721f0f8 + region: "{{ region }}" + aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key }}" + aws_session_token: "{{ aws_session_token }}" + state: present + wait: true + count: 1 + detailed_monitoring: true + register: ec2_output + - set_fact: + ec2_fedhubs: "{{ ec2_output }}" + - name: Wait for EC2 Instance to be Running + wait_for: + host: "{{ item.public_ip_address }}" + port: 22 + delay: 10 + timeout: 300 + state: started + loop: "{{ ec2_fedhubs.instances }}" + when: ec2_fedhubs.instances | length > 0 + delegate_to: localhost + - name: Add EC2 Instance IP to Inventory + add_host: + groups: "{{ fedhub_instances_group }}" + name: "{{ item.public_ip_address }}" + ansible_host: "{{ item.public_ip_address }}" + ansible_user: "{{ fedhub_user_name }}" + ansible_ssh_private_key_file: "{{ key_name }}.pem" + loop: "{{ ec2_fedhubs.instances }}" + when: ec2_fedhubs.instances | length > 0 + delegate_to: localhost + +- name: provisioning takserver instances + hosts: localhost + vars_files: + - variables.yml + gather_facts: true + collections: + - community.aws + - amazon.aws + remote_user: "{{ takserver_user_name }}" + tasks: + - name: provision + ec2_instance: + name: "{{ takserver_instance_name_prefix + timestamp }}" + key_name: "{{ key_name }}" + instance_type: "{{ takserver_instance_type }}" + image_id: "{{ takserver_ami_id }}" + security_group: sg-0a127f08b303384b3 + region: "{{ region }}" + aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key }}" + aws_session_token: "{{ aws_session_token }}" + state: present + wait: true + count: "{{ takserver_num_instances }}" + detailed_monitoring: true + register: ec2_output + - set_fact: + ec2_takservers: "{{ ec2_output }}" + - name: Wait for EC2 Instance to be Running + wait_for: + host: "{{ item.public_ip_address }}" + port: 22 + delay: 10 + timeout: 300 + state: started + loop: "{{ ec2_takservers.instances }}" + when: ec2_takservers.instances | length > 0 + delegate_to: localhost + - name: Add EC2 Instance IP to Inventory + add_host: + groups: "{{ takserver_instances_group }}" + name: "{{ item.public_ip_address }}" + ansible_host: "{{ item.public_ip_address }}" + ansible_user: "{{ takserver_user_name }}" + ansible_ssh_private_key_file: "{{ key_name }}.pem" + loop: "{{ ec2_takservers.instances }}" + when: ec2_takservers.instances | length > 0 + delegate_to: localhost + - name: Generate CoreConfig.xml + template: + src: "{{ takserver_config_files_dir }}/{{ takserver_base_config_dir }}/CoreConfig.xml.jn2" + dest: "{{ takserver_config_files_dir }}/{{ takserver_base_config_dir }}/CoreConfig.xml" + vars: + fedhub_ip_address: "{{ hostvars.localhost.ec2_fedhubs.instances[0].public_ip_address }}" + - name: Generate load testing configs + template: + src: "{{ takserver_config_files_dir }}/{{ load_testing_cfgs_dir }}/config_elu_load_testing.yml.jn2" + dest: "{{ takserver_config_files_dir }}/{{ load_testing_cfgs_dir }}/config_elu_load_testing_{{ iteration }}.yml" + vars: + takserver_host: "{{ item.public_ip_address }}" + pytak_self_sa_delta: "{{ self_sa_delta }}" + pytak_clients: "{{ clients }}" + loop: "{{ ec2_takservers.instances }}" + loop_control: + index_var: iteration + when: ec2_takservers.instances | length > 0 + delegate_to: localhost + - name: copy takserver config files to first instance + ansible.builtin.shell: scp -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "{{ key_name }}".pem "{{ takserver_config_files_dir }}"/* "{{ takserver_user_name }}"@"{{ ec2_takservers.instances[0].public_ip_address }}":/home/"{{ takserver_user_name }}"/ + when: ec2_takservers.instances | length > 0 # Check if an instance was created + delegate_to: localhost + - name: Generate certificates on first takserver instance + ansible.builtin.shell: + cmd: | + sudo yum install java-1.8.0-amazon-corretto -y + cd {{ takserver_certs_dir }} + ./makeRootCa.sh takserver_{{ timestamp }}_{{ iteration }} + ./makeCert.sh server takserver + ./makeCert.sh client user + ./makeCert.sh client admin + rm files/fed-truststore.jks + cd ~ + cp original-fed-truststore.jks {{ takserver_certs_dir }}/files/fed-truststore.jks + cp -n -r {{ takserver_base_config_dir }} takserver_config_{{ iteration }} + cp -n -r {{ takserver_certs_dir }}/files takserver_config_{{ iteration }}/files + mv --backup=t {{ takserver_certs_dir }}/files takserver_config_{{ iteration }}/{{ pytak_config_files_dir }}/load_test/certs/ + cp {{ load_testing_cfgs_dir}}/config_elu_load_testing_{{ iteration}}.yml takserver_config_{{ iteration }}/{{ pytak_config_files_dir }}/load_test/config_elu_load_testing.yml + keytool -importcert -file takserver_config_{{ iteration }}/files/ca.pem -alias takserver_{{ timestamp }}_{{ iteration }} -keystore fed-truststore.jks -storepass atakatak -noprompt + chdir: /home/{{ takserver_user_name }} + loop: "{{ query('sequence', 'start=1 end='+(takserver_num_instances|string)) }}" + loop_control: + index_var: iteration + delegate_to: "{{ ec2_takservers.instances[0].public_ip_address }}" + - name: Generate fedhub policy on first takserver instance + ansible.builtin.shell: + cmd: | + mkdir {{ fedhub_generated_files_folder }} + python3 generate_fedhub_policy.py /home/{{ takserver_user_name }}/ {{ connection_index }} > ui_generated_policy.json + mv fed-truststore.jks {{ fedhub_generated_files_folder }}/ + mv ui_generated_policy.json {{ fedhub_generated_files_folder }}/ + chdir: /home/{{ takserver_user_name }} + delegate_to: "{{ ec2_takservers.instances[0].public_ip_address }}" + - name: copy files from first instance to itself + ansible.builtin.shell: + cmd: | + sudo mv --backup=t takserver_config_0/* ~ + cp -r pytak_config_files takserver_config_0/ + chdir: /home/{{ takserver_user_name }} + delegate_to: "{{ ec2_takservers.instances[0].public_ip_address }}" + - name: copy files from first instance to other instances + ansible.builtin.shell: + cmd: scp -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "{{ key_name }}".pem takserver_config_"{{ iteration + 1 }}"/* "{{ takserver_user_name }}"@"{{ item.public_ip_address }}":/home/"{{ takserver_user_name }}"/ + chdir: /home/{{ takserver_user_name }} + when: ec2_takservers.instances | length > 1 # Ensure there is more than one instance + loop: "{{ ec2_takservers.instances[1:] }}" # Start from the second instance + loop_control: + index_var: iteration + delegate_to: "{{ ec2_takservers.instances[0].public_ip_address }}" + +- name: transfer generated fedhub related files from first takserver instance to fedhub instance + hosts: localhost + vars_files: + - variables.yml + tasks: + - name: pull fedhub related generated files from first takserver instance to localhost + ansible.builtin.shell: + cmd: | + scp -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "{{ key_name }}".pem "{{ takserver_user_name }}"@"{{ ec2_takservers.instances[0].public_ip_address }}":/home/"{{ takserver_user_name }}"/"{{ fedhub_generated_files_folder }}"/* "{{ fedhub_config_files_dir }}"/ + mv "{{ fedhub_config_files_dir }}"/fed-truststore.jks "{{ fedhub_config_files_dir }}"/fedhub_files/ + when: ec2_takservers.instances | length > 0 # Check if an instance was created + delegate_to: localhost + - name: copy fedhub config files to fedhub instance + ansible.builtin.shell: scp -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "{{ key_name }}".pem "{{ fedhub_config_files_dir }}"/* "{{ fedhub_user_name }}"@"{{ ec2_fedhubs.instances[0].public_ip_address }}":/home/"{{ fedhub_user_name }}"/ + when: ec2_fedhubs.instances | length > 0 # Check if an instance was created + delegate_to: localhost + +- name: setup fedhub instance + hosts: "{{ hostvars.localhost.fedhub_instances_group }}" + vars_files: + - variables.yml + remote_user: "{{ hostvars.localhost.fedhub_user_name }}" + tasks: + - name: install and start fedhub + ansible.builtin.shell: + cmd: | + sudo dnf --disablerepo=* -y install https://download.postgresql.org/pub/repos/yum/reporpms/EL-9-x86_64/pgdg-redhat-repo-latest.noarch.rpm + sudo dnf update -y + sudo dnf install java-17-openjdk-devel -y + sudo yum install takserver-fed-hub-*.noarch.rpm -y + sudo cp federation-hub-broker.yml /opt/tak/federation-hub/configs/federation-hub-broker.yml + sudo cp federation-hub-ui.yml /opt/tak/federation-hub/configs/federation-hub-ui.yml + sudo dnf install checkpolicy + cd /opt/tak/federation-hub && sudo ./apply-selinux.sh && sudo semodule -l | grep takserver + cd /home/{{ hostvars.localhost.fedhub_user_name }} + sudo cp /opt/tak/federation-hub/scripts/db/mongodb-org.repo /etc/yum.repos.d/mongodb-org.repo + sudo yum install -y mongodb-org + sudo systemctl daemon-reload + sudo systemctl enable mongod + sudo systemctl restart mongod + sudo /opt/tak/federation-hub/scripts/db/configure.sh + sudo alternatives --set java /usr/lib/jvm/java-17-openjdk-17.0.12.0.7-2.el8.x86_64/bin/java + sudo cp -r fedhub_files /opt/tak/federation-hub/certs/files + sudo mv ui_generated_policy.json /opt/tak/federation-hub/ + sudo chmod 777 -R /opt/tak/federation-hub + sudo systemctl restart mongod + sudo systemctl enable federation-hub + sudo systemctl restart federation-hub + sudo java -jar /opt/tak/federation-hub/jars/federation-hub-manager.jar /opt/tak/federation-hub/certs/files/admin.pem + sudo dnf install xorg-x11-xauth xorg-x11-utils xorg-x11-fonts* -y + sudo dnf install unzip -y + chdir: /home/{{ hostvars.localhost.fedhub_user_name }}/ + +- name: provisioning pytak instances + hosts: localhost + vars_files: + - variables.yml + gather_facts: true + collections: + - community.aws + - amazon.aws + remote_user: "{{ pytak_user_name }}" + tasks: + - name: provision + ec2_instance: + name: "{{ pytak_instance_name_prefix + timestamp }}" + key_name: "{{ key_name }}" + instance_type: "{{ pytak_instance_type }}" + image_id: "{{ pytak_ami_id }}" + security_group: sg-b721f0f8 + region: "{{ region }}" + aws_secret_key: "{{ aws_secret_key }}" + aws_access_key: "{{ aws_access_key }}" + aws_session_token: "{{ aws_session_token }}" + state: present + wait: true + count: "{{ pytak_num_instances * takserver_num_instances }}" + detailed_monitoring: true + register: ec2_output + - set_fact: + ec2_pytaks: "{{ ec2_output }}" + - name: Wait for EC2 Instance to be Running + wait_for: + host: "{{ item.public_ip_address }}" + port: 22 + delay: 10 + timeout: 300 + state: started + loop: "{{ ec2_pytaks.instances }}" + when: ec2_pytaks.instances | length > 0 + delegate_to: localhost + - name: Add EC2 Instance IP to Inventory + add_host: + groups: "{{ pytak_instances_group }}" + name: "{{ item.public_ip_address }}" + ansible_host: "{{ item.public_ip_address }}" + ansible_user: "{{ pytak_user_name }}" + ansible_ssh_private_key_file: "{{ key_name }}.pem" + loop: "{{ ec2_pytaks.instances }}" + when: ec2_pytaks.instances | length > 0 + delegate_to: localhost + - name: copy files from first takserver instance to pytak instances + ansible.builtin.shell: + cmd: scp -r -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -i "{{ key_name }}".pem takserver_config_{{ iteration // pytak_num_instances }}/{{ pytak_config_files_dir }}/load_test "{{ pytak_user_name }}"@"{{ item.public_ip_address }}":/home/"{{ pytak_user_name }}"/ + chdir: /home/{{ takserver_user_name }} + when: ec2_pytaks.instances | length > 1 # Ensure there is more than one instance + loop: "{{ ec2_pytaks.instances }}" # Start from the second instance + loop_control: + index_var: iteration + delegate_to: "{{ ec2_takservers.instances[0].public_ip_address }}" + +- name: global var setup + hosts: takserver_instances + vars_files: + - variables.yml + tasks: + - set_fact: + takserver_version: "{{ takserver_version }}" + takserver_user_name: "{{ hostvars.localhost.takserver_user_name }}" + ec2_takservers: "{{ hostvars.localhost.ec2_takservers }}" + +- name: setup takserver instances + hosts: " {{ hostvars.localhost.takserver_instances_group }}" + vars_files: + - variables.yml + remote_user: "{{ takserver_user_name }}" + tasks: + - name: install and start takserver + ansible.builtin.shell: + cmd: | + sudo yum install -y docker + sudo service docker start + sudo chown "{{ takserver_user_name }}" /var/run/docker.sock + unzip "{{ takserver_version }}".zip + cp CoreConfig.xml "{{ takserver_version }}"/tak/CoreConfig.xml + cp -r files "{{ takserver_version }}"/tak/certs/ + cp UserAuthenticationFile.xml "{{ takserver_version }}"/tak/UserAuthenticationFile.xml + cp pg_hba.conf "{{ takserver_version }}"/tak/db-utils/pg_hba.conf + cd {{ takserver_version }} + docker build -t takserver-db:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver-db . + docker network create takserver-"$(cat tak/version.txt)" + docker build -t takserver:"$(cat tak/version.txt)" -f docker/Dockerfile.takserver . + docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 5432:5432 --network takserver-"$(cat tak/version.txt)" --network-alias tak-database --name takserver-db-"$(cat tak/version.txt)" takserver-db:"$(cat tak/version.txt)" + docker run -d -v $(pwd)/tak:/opt/tak:z -it -p 8089:8089 -p 8443:8443 -p 8444:8444 -p 8446:8446 -p 8087:8087/tcp -p 8087:8087/udp -p 8088:8088 -p 9000:9000 -p 9001:9001 --network takserver-"$(cat tak/version.txt)" --name takserver-"$(cat tak/version.txt)" takserver:"$(cat tak/version.txt)" + chdir: /home/{{ takserver_user_name }}/ + +- name: global var setup + hosts: "{{ hostvars.localhost.pytak_instances_group }}" + vars_files: + - variables.yml + tasks: + - set_fact: + pytak_user_name: "{{ hostvars.localhost.pytak_user_name }}" + +- name: setup pytak instances + hosts: "{{ hostvars.localhost.pytak_instances_group }}" + vars_files: + - variables.yml + remote_user: "{{ pytak_user_name }}" + tasks: + - name: Pause for 2.5 minutes to give takservers a chance to start before pytak clients are started, since pytak clients will not reattempt connection + pause: + seconds: 150 + - set_fact: + ec2_pytaks: "{{ hostvars.localhost.ec2_pytaks }}" + - name: install and start pytak + ansible.builtin.shell: + cmd: | + nohup bash runload.sh > pytak_output.log & + chdir: /home/{{ hostvars.localhost.pytak_user_name }}/load_test + async: 2592000 # 60*60*24*30 – 1 month + poll: 0 + +# - name: clean up fedhub instance +# hosts: localhost +# remote_user: "{{ fedhub_user_name }}" +# tasks: +# - name: Prompt user before deleting instances +# ansible.builtin.pause: +# prompt: "Press enter to delete all instances" +# - name: delete fedhub instance +# ec2_instance: +# state: absent +# region: "{{ region }}" +# aws_secret_key: "{{ aws_secret_key }}" +# aws_access_key: "{{ aws_access_key }}" +# instance_ids: "{{ item.instance_id }}" +# loop: "{{ ec2_fedhubs.instances }}" +# when: ec2_fedhubs.instances | length > 0 # Check if there are instances to delete + +# - name: clean up takserver instances +# hosts: localhost +# remote_user: "{{ takserver_user_name }}" +# tasks: +# - name: delete takserver instances +# ec2_instance: +# state: absent +# region: "{{ region }}" +# aws_secret_key: "{{ aws_secret_key }}" +# aws_access_key: "{{ aws_access_key }}" +# instance_ids: "{{ item.instance_id }}" +# loop: "{{ ec2_takservers.instances }}" +# when: ec2_takservers.instances | length > 0 # Check if there are instances to delete + +# - name: clean up pytak instances +# hosts: localhost +# remote_user: "{{ pytak_user_name }}" +# tasks: +# - name: delete pytak instances +# ec2_instance: +# state: absent +# region: "{{ region }}" +# aws_secret_key: "{{ aws_secret_key }}" +# aws_access_key: "{{ aws_access_key }}" +# instance_ids: "{{ item.instance_id }}" +# loop: "{{ ec2_pytaks.instances }}" +# when: ec2_pytaks.instances | length > 0 # Check if there are instances to delete diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/CoreConfig.xml b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/CoreConfig.xml new file mode 100755 index 00000000..866a8423 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/CoreConfig.xml @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test:Test + Test2:Test2 + Test + Test2 + + + pref + + + + + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/CoreConfig.xml.jn2 b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/CoreConfig.xml.jn2 new file mode 100755 index 00000000..f9298573 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/CoreConfig.xml.jn2 @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Test:Test + Test2:Test2 + Test + Test2 + + + pref + + + + + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/UserAuthenticationFile.xml b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/UserAuthenticationFile.xml new file mode 100755 index 00000000..8fa0dfe7 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/UserAuthenticationFile.xml @@ -0,0 +1,9 @@ + + + + Test + + + Test2 + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pg_hba.conf b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pg_hba.conf new file mode 100755 index 00000000..84372c46 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pg_hba.conf @@ -0,0 +1,91 @@ +# PostgreSQL Client Authentication Configuration File +# =================================================== +# +# Refer to the "Client Authentication" section in the PostgreSQL +# documentation for a complete description of this file. A short +# synopsis follows. +# +# This file controls: which hosts are allowed to connect, how clients +# are authenticated, which PostgreSQL user names they can use, which +# databases they can access. Records take one of these forms: +# +# local DATABASE USER METHOD [OPTIONS] +# host DATABASE USER ADDRESS METHOD [OPTIONS] +# hostssl DATABASE USER ADDRESS METHOD [OPTIONS] +# hostnossl DATABASE USER ADDRESS METHOD [OPTIONS] +# +# (The uppercase items must be replaced by actual values.) +# +# The first field is the connection type: "local" is a Unix-domain +# socket, "host" is either a plain or SSL-encrypted TCP/IP socket, +# "hostssl" is an SSL-encrypted TCP/IP socket, and "hostnossl" is a +# plain TCP/IP socket. +# +# DATABASE can be "all", "sameuser", "samerole", "replication", a +# database name, or a comma-separated list thereof. The "all" +# keyword does not match "replication". Access to replication +# must be enabled in a separate record (see example below). +# +# USER can be "all", a user name, a group name prefixed with "+", or a +# comma-separated list thereof. In both the DATABASE and USER fields +# you can also write a file name prefixed with "@" to include names +# from a separate file. +# +# ADDRESS specifies the set of hosts the record matches. It can be a +# host name, or it is made up of an IP address and a CIDR mask that is +# an integer (between 0 and 32 (IPv4) or 128 (IPv6) inclusive) that +# specifies the number of significant bits in the mask. A host name +# that starts with a dot (.) matches a suffix of the actual host name. +# Alternatively, you can write an IP address and netmask in separate +# columns to specify the set of hosts. Instead of a CIDR-address, you +# can write "samehost" to match any of the server's own IP addresses, +# or "samenet" to match any address in any subnet that the server is +# directly connected to. +# +# METHOD can be "trust", "reject", "md5", "password", "gss", "sspi", +# "krb5", "ident", "peer", "pam", "ldap", "radius" or "cert". Note that +# "password" sends passwords in clear text; "md5" is preferred since +# it sends encrypted passwords. +# +# OPTIONS are a set of options for the authentication in the format +# NAME=VALUE. The available options depend on the different +# authentication methods -- refer to the "Client Authentication" +# section in the documentation for a list of which options are +# available for which authentication methods. +# +# Database and user names containing spaces, commas, quotes and other +# special characters must be quoted. Quoting one of the keywords +# "all", "sameuser", "samerole" or "replication" makes the name lose +# its special character, and just match a database or username with +# that name. +# +# This file is read on server startup and when the postmaster receives +# a SIGHUP signal. If you edit the file on a running system, you have +# to SIGHUP the postmaster for the changes to take effect. You can +# use "pg_ctl reload" to do that. + +# Put your actual configuration here +# ---------------------------------- +# +# If you want to allow non-local connections, you need to add more +# "host" records. In that case you will also need to make PostgreSQL +# listen on a non-local interface via the listen_addresses +# configuration parameter, or via the -i or -h command line switches. + + + +# TYPE DATABASE USER ADDRESS METHOD + +# "local" is for Unix domain socket connections only +local all all peer +# IPv4 local connections: +host all all 127.0.0.1/32 md5 +host all all 172.0.0.0/8 md5 +# IPv6 local connections: +host all all ::1/128 md5 +# Allow replication connections from localhost, by a user with the +# replication privilege. +#local replication postgres peer +#host replication postgres 127.0.0.1/32 ident +#host replication postgres ::1/128 ident +host all all 192.168.0.0/16 md5 \ No newline at end of file diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/README b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/README new file mode 100755 index 00000000..fc101e55 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/README @@ -0,0 +1,93 @@ +The load tests are fairly easy to run. They require python 3.6.1+ to run. In order to make sure you have the right dependencies, use the python pip utility and the requirements.txt file to + +pip install -r requirements.txt + + +There are two main ways to use the tool, with the config file (base_config.yml): +>$ python tak_tester.py —config base_config.yml —-test-pytak + +or using only command line arguments: + +>$ python tak_tester.py --host --cert --password --test-pytak + +The --clients option will override the option set in the config file, if you want to run the test with a different number without changing the file. + +Use -h or —help to get information about all possible command line arguments. + +In order to use the config file, you will have to change a few things, mainly the host address and cert path. + +connection: + host: "" + tls: 8089 + https: 8443 + udp: 8087 + +authentication: + cert: "" + password: "atakatak" # if no password, use empty string + + +MissionApi config: +In order to determine which missions to create, --test-pytak and --test-websocket-proto look at the Missions section of the config: +Missions: + creatorUid: PyTAK-0 + group: __ANON__ + tool: PyTAK + + size_files: + - 1mb.txt: + 1000000 + - 5mb.txt: + 5000000 + + missions: + - mission_1: + files: + - 1mb.txt + - 5mb.txt + + - mission_2: + keywords: + - cool + + description: "A cool mission!" + + - mission_3: + files: + - 1mb.txt + + - mission_4: + tool: OtherPyTAK + +The size files are automatically created with random data to be of the indicated size before uploading them to the server. +All the missions listed below are then created. If a mission parameter is not specified, then it uses one of the defaults +of creatorUid, group, and tool listed at the top of this section. The size_files section can be omitted entirely if desired, +and there is no error for including a file in a mission if the file doesn't exist. + +In the PyTAK section of the config, there is a subsection for configuring the mission api behaviour. +In the missions subsection, if you want all the clients to subscribe to the same missions, then set random +to False, subscribe should be a list of the missions the clients subscribe to. Then, you can set whether the +clients add cot tracks to the missions, the frequency of sending this data, and whether they should react +to mission change announcements by making the appropriate datasync call. In the provided config file, +the non-random scenario is provided, and the random configuration is commented out below, should you wish to use it instead. +The main difference is that instead of list of missions to subscribe to, you should provide the number +of missions the client should randomly select to subscribe. + + +CloudWatch Configuration: + +To configure pyTAK to send metric data to CloudWatch, follow the instructions in the "Configuration" section in the link: +https://boto3.amazonaws.com/v1/documentation/api/latest/guide/quickstart.html + +>> vim ~/.aws/credentials +[default] +aws_access_key_id = YOUR_ACCESS_KEY +aws_secret_access_key = YOUR_SECRET_KEY + +>> vim ~/.aws/config +[default] +region=us-east-2 + +You can run the main method in cloud_watch.py to check your configuration. + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/__init__.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/cloud_watch.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/cloud_watch.py new file mode 100755 index 00000000..70114f23 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/cloud_watch.py @@ -0,0 +1,180 @@ +import boto3 + +from datetime import datetime, timedelta +import logging +from pprint import pprint +import random +import time +from botocore.exceptions import ClientError +import multiprocessing + +logger = logging.getLogger(__name__) + +class CloudWatchWrapper: + """Encapsulates Amazon CloudWatch functions.""" + def __init__(self, cloudwatch_resource): + """ + :param cloudwatch_resource: A Boto3 CloudWatch resource. + """ + self.cloudwatch_resource = cloudwatch_resource + + + def list_metrics(self, namespace, name, recent=False): + """ + Gets the metrics within a namespace that have the specified name. + If the metric has no dimensions, a single metric is returned. + Otherwise, metrics for all dimensions are returned. + + :param namespace: The namespace of the metric. + :param name: The name of the metric. + :param recent: When True, only metrics that have been active in the last + three hours are returned. + :return: An iterator that yields the retrieved metrics. + """ + try: + kwargs = {'Namespace': namespace, 'MetricName': name} + if recent: + kwargs['RecentlyActive'] = 'PT3H' # List past 3 hours only + metric_iter = self.cloudwatch_resource.metrics.filter(**kwargs) + logger.info("Got metrics for %s.%s.", namespace, name) + except ClientError: + logger.exception("Couldn't get metrics for %s.%s.", namespace, name) + raise + else: + return metric_iter + + def put_metric_data(self, namespace, uid, name, value, unit): + """ + Sends a single data value to CloudWatch for a metric. This metric is given + a timestamp of the current UTC time. + + :param namespace: The namespace of the metric. + :param name: The name of the metric. + :param value: The value of the metric. + :param unit: The unit of the metric. + """ + try: + metric = self.cloudwatch_resource.Metric(namespace, name) + metric.put_data( + Namespace=namespace, + MetricData=[{ + 'Dimensions': [{'Name': 'uid','Value': uid}], + 'MetricName': name, + 'Value': value, + 'Unit': unit + }] + ) + logger.info("Put data for metric %s.%s", namespace, name) + except ClientError: + logger.exception("Couldn't put data for metric %s.%s", namespace, name) + raise + + def put_metric_pyTAK_stats(self, namespace, uid, stats): + + try: + self.put_metric_data(namespace=namespace, uid = uid, name='messages_sent', value=stats['messages_sent'], unit='Count') + self.put_metric_data(namespace=namespace, uid = uid, name='messages_received', value=stats['messages_received'], unit='Count') + self.put_metric_data(namespace=namespace, uid = uid, name='connect_event_count', value=stats['connect_event_count'], unit='Count') + self.put_metric_data(namespace=namespace, uid = uid, name='disconnect_event_count', value=stats['disconnect_event_count'], unit='Count') + self.put_metric_data(namespace=namespace, uid = uid, name='ping_count', value=stats['ping_count'], unit='Count') + self.put_metric_data(namespace=namespace, uid = uid, name='pong_count', value=stats['pong_count'], unit='Count') + self.put_metric_data(namespace=namespace, uid = uid, name='time_between_ping_pong', value=stats['time_between_ping_pong'], unit='Milliseconds') + + logger.info("Done sending pyTAK stats data to namespace %s", namespace) + # print(f"Done sending pyTAK stats data to namespace {namespace}") + except ClientError: + logger.exception("Couldn't put data set for metric %s.", namespace) + raise + + +def cloud_watch_process(uid, arr, metric_namespace, send_metrics_interval): + + print(f"Starting a cloud_watch_process for uid {uid}. Will send metrics to namespace: {metric_namespace} every {send_metrics_interval} seconds") + + cw_wrapper = CloudWatchWrapper(boto3.resource('cloudwatch')) + + while True: + time.sleep(send_metrics_interval) + try: + stats_for_uid = { + 'messages_sent': arr[0], + 'messages_received': arr[1], + 'connect_event_count': arr[2], + 'disconnect_event_count': arr[3], + 'ping_count': arr[4], + 'pong_count': arr[5], + 'time_between_ping_pong': arr[6] + } + + print(f"Putting pyTAK stat data for uid {uid} into metric namespace: {metric_namespace}, stats: {stats_for_uid}") + + cw_wrapper.put_metric_pyTAK_stats(metric_namespace, uid, stats_for_uid) + + except KeyboardInterrupt: + logger.info("KeyboardInterrupt") + break + except: + logger.exception("Error occurs when pushing stats data to CloudWatch") + finally: + # reset stats value after sending to cloud_watch + for i in range(len(arr)): + arr[i] = 0 + + print("cloud_watch_process has stopped") + + +def print_info_without_sending_to_cloud_watch_process(uid, arr, send_metrics_interval): + + while True: + time.sleep(send_metrics_interval) + try: + stats_for_uid = { + 'messages_sent': arr[0], + 'messages_received': arr[1], + 'connect_event_count': arr[2], + 'disconnect_event_count': arr[3], + 'ping_count': arr[4], + 'pong_count': arr[5], + 'time_between_ping_pong': arr[6] + } + + print(f"Info that would be sent to CloudWatch if it was enabled, uid: {uid}, stats: {stats_for_uid}") + + except KeyboardInterrupt: + logger.info("KeyboardInterrupt") + break + finally: + # reset stats value after sending to cloud_watch + for i in range(len(arr)): + arr[i] = 0 + + +# This main method below can be used to test CloudWatch configuration on ~/.aws/credentials and ~/.aws/config +if __name__ == "__main__": + + print("-"*88) + + logging.basicConfig(level=logging.INFO, format='%(levelname)s: %(message)s') + + cw_wrapper = CloudWatchWrapper(boto3.resource('cloudwatch')) + metric_namespace = 'doc-example-metric' + print(f"Putting pyTAK stat data into metric namespace {metric_namespace}") + + cw_wrapper.put_metric_pyTAK_stats( + metric_namespace, "uid_temp_123", + { + 'messages_sent': 10, + 'messages_received': 20, + 'connect_event_count': 1, + 'disconnect_event_count': 0, + 'ping_count': 6, + 'pong_count': 5, + 'time_between_ping_pong': 40 + }) + + print("-"*88) + metric_iter = cw_wrapper.list_metrics(metric_namespace, 'messages_sent') + for metric in metric_iter: + print(f"metric: {metric}") + print("Done") + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/create_cot.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/create_cot.py new file mode 100755 index 00000000..dcfb5d8b --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/create_cot.py @@ -0,0 +1,212 @@ +import random +import string +import time + + +from datetime import datetime, timedelta +from lxml import etree + + +def id_gen(size=10): + chars = string.ascii_letters + string.digits + return "".join(random.choice(chars) for s in range(size)) + +def cot_time_string(now=None): + if now is None: + now = datetime.utcnow() + + cur_time = str(now.year).zfill(4)+"-"+str(now.month).zfill(2)+"-"+str(now.day).zfill(2) + \ + "T"+str(now.hour).zfill(2)+":"+str(now.minute).zfill(2)+":"+str(now.second).zfill(2)+"."+str(int(now.microsecond/1000)).zfill(3)+"Z" + + return cur_time + + +class CotMessage: + + def __init__(self, + uid=None, + how="h-g-i-g-o", + type="a-f-G-U-C-I", + version="2.0", + lat="0", lon="0", + msg=None): + + self.detail = None + + if msg is not None: + self.deserialize(msg) + return + + if uid == None: + uid = "PyTAK-" + id_gen() + + self.uid = uid + + self.event = etree.Element("event", + how=how, + stale=cot_time_string(datetime.utcnow()+timedelta(minutes=1)), + start=cot_time_string(), + time=cot_time_string(), + type=type, + uid=self.uid, + version=version) + + self.point = etree.SubElement(self.event, "point") + + self.point.attrib.update({"ce": "500", + "hae": "262.0", + "lat": lat, + "le": "500", + "lon": lon}) + + self.detail = etree.SubElement(self.event, "detail") + + + def add_callsign_detail(self, + callsign=None, + endpoint="*:-1:stcp", + group_name="Black", + group_role="Team Member", + platform="PyTAK", + version="0.0.1"): + + if callsign is None: + callsign = self.uid + + contact_attrib = {"callsign": callsign, + "endpoint": endpoint} + self.add_detail("contact", contact_attrib) + + __group_attrib = {"name": group_name, + "role": group_role} + self.add_detail("__group", __group_attrib) + + takv_attrib = {"platform": platform, + "version": version} + self.add_detail("takv", takv_attrib) + + return True + + def add_detail(self, detail_name, detail_attributes, detail_text=None, duplicate=False, edit=False): + if detail_name in (c.tag for c in self.detail) and not duplicate: + return False + + if edit: + for c in self.detail: + if c.tag == detail_name: + sub_detail = c + else: + return False # we are trying to edit a tag that doesn't exist + + else: + sub_detail = etree.SubElement(self.detail, detail_name) + + sub_detail.attrib.update(detail_attributes) + + if detail_text is not None: + sub_detail.text = detail_text + + return True + + def add_sub_detail(self, detail_name, sub_detail_name, sub_detail_attributes, sub_detail_text=None): + for c in self.detail: + if detail_name == c.tag: + detail = c + + else: + detail = etree.SubElement(self.detail, detail_name) + + sub_detail = etree.SubElement(detail, sub_detail_name) + + sub_detail.attrib.update(sub_detail_attributes) + if sub_detail_text is not None: + sub_detail.text = sub_detail_text + + return True + + def is_sa(self): + if self.detail is None: # Pong messages might not have a detail tag + return False + for c in self.detail: + if c.tag == "contact": + return True + return False + + def is_pong(self): + if self.event.attrib.get("type") == "t-x-c-t-r": + return True + return False + + def mission_change(self): + for c in self.detail: + if c.tag == "mission": + change_type = c.attrib.get("type") + mission_name = c.attrib.get("name") + file_hashes = list() + + for node in c.findall('.//MissionChanges/MissionChange/contentResource/hash'): + file_hashes.append(node.text) + + if file_hashes: + return "download_mission_files", file_hashes + + if change_type in {"CREATE", "INVITE", "DELETE"}: + return change_type, mission_name + + return False, False + + def fileshare(self): + for c in self.detail: + if c.tag == "fileshare": + file_hash = c.attrib.get("sha256") + filename = c.attrib.get("filename") + return file_hash, filename + + + def to_string(self, pretty_print=False): + return etree.tostring(self.event, pretty_print=pretty_print) + + def deserialize(self, msg): + self.event = etree.fromstring(msg) + self.uid = self.event.attrib.get("uid") + for elem in self.event: + if elem.tag == 'detail': + self.detail = elem + elif elem.tag == 'point': + self.point = elem + + def server_protocol_version_support(self): + tak_control = self.detail.find("TakControl") + if tak_control is not None: + protocol_message = tak_control.find("TakProtocolSupport") + if protocol_message is not None: + return protocol_message.attrib.get('version') + return False + + def server_protocol_negotiation_handshake(self): + tak_control = self.detail.find("TakControl") + if tak_control is not None: + tak_response = tak_control.find("TakResponse") + if tak_response is not None and tak_response.attrib.get("status") == "true": + return True + return False + + def protocol_response_message(self, version="1"): + self.point.attrib.update({"lat": "0.0", + "lon": "0.0", + "hae": "0.0", + "ce": "999999", + "le": "999999"}) + self.event.attrib.update({"type": "t-x-takp-q", + "how": "m-g"}) + self.add_sub_detail("TakControl", "TakRequest", sub_detail_attributes={"version": version}) + + def __str__(self): + return etree.tostring(self.event) + +if __name__ == "__main__": + event = CotMessage() + + event.add_callsign_detail() + + print(event.to_string(pretty_print=True)) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/create_proto.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/create_proto.py new file mode 100755 index 00000000..3f0a2e99 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/create_proto.py @@ -0,0 +1,193 @@ +from datetime import datetime +from protobuf_msg.takmessage_pb2 import TakMessage +from lxml import etree + +from create_cot import id_gen + +MAGIC_BYTE = b'\xbf' + + +class CotProtoMessage: + + def __init__(self, + uid=None, + how="h-g-i-g-o", + type="a-f-G-U-C-I", + version="2.0", + lat="42", lon="-71", + msg=None): + self.message = TakMessage() + + if msg is not None: + self.deserialize_message(msg) + self.uid = self.message.cotEvent.uid + return + + if uid == None: + uid = "PyTAK-" + id_gen() + + self.uid = uid + + # + event = self.message.cotEvent + event.type = type + now = int(datetime.now().timestamp()) * 1000 + event.staleTime = (now + 60) * 1000 # timestamp in seconds, so this adds a minute + event.startTime = (now) * 1000 + event.sendTime = now + event.how = how + event.uid = self.uid + + # + event.lat = float(lat) + event.lon = float(lon) + event.hae = 262.0 + event.ce = 500 + event.le = 500 + + def add_callsign_detail(self, + callsign=None, + endpoint="*:-1:stcp", + group_name="Black", + group_role="Team Member", + platform="PyTAKProto", + version="0.0.2"): + + if callsign is None: + callsign = self.uid + + detail = self.message.cotEvent.detail + + contact = detail.contact + contact.callsign = callsign + contact.endpoint = endpoint + + group = detail.group + group.name = group_name + group.role = group_role + + takv = detail.takv + takv.platform = platform + takv.version = version + + return True + + def add_sub_detail(self, detail_name, sub_detail_name, sub_detail_attributes, sub_detail_text=None): + details = etree.fromstring("" + self.message.cotEvent.detail.xmlDetail + "") + for c in details: + if detail_name == c.tag: + detail = c + else: + detail = etree.SubElement(details, detail_name) + + sub_detail = etree.SubElement(detail, sub_detail_name) + + sub_detail.attrib.update(sub_detail_attributes) + if sub_detail_text is not None: + sub_detail.text = sub_detail_text + + xml_details = list() + for elem in details.getchildren(): + xml_details.append(etree.tostring(elem).decode('utf-8')) + self.message.cotEvent.detail.xmlDetail = "".join(xml_details) + + + def deserialize_message(self, msg): + if not type(msg) == bytes: + raise TypeError("msg needs to be of type bytes") + + first_byte = msg[0] + if first_byte.to_bytes(1, byteorder="big") != MAGIC_BYTE: + raise Exception("Magic byte is wrong: " + str(first_byte)) + + msg_size, msg = get_size_and_truncate(msg[1:]) + self.message.ParseFromString(msg) + + def serialize(self): + msg = self.message.SerializeToString() + size_bytes = get_size_bytes(msg) + return MAGIC_BYTE + size_bytes + msg + + def is_sa(self): + return self.message.cotEvent.detail.HasField("contact") + + def is_pong(self): + if self.message.cotEvent.type == "t-x-c-t-r": + return True + return False + + def mission_change(self): + xml_details = etree.fromstring(""+self.message.cotEvent.detail.xmlDetail+"") + for xml_detail in xml_details.getchildren(): + if xml_detail.tag == "_flow-tags_": + continue + if xml_detail.tag == "mission": + change_type = xml_detail.attrib.get("type") + name = xml_detail.attrib.get("name") + file_hashes = list() + + for node in xml_details.findall('.//mission/MissionChanges/MissionChange/contentResource/hash'): + file_hashes.append(node.text) + + if file_hashes: + return "download_mission_files", file_hashes + + if change_type in {"INVITE", "CREATE", "DELETE"}: + return change_type, name + + return False, False + + def fileshare(self): + xml_details = etree.fromstring("
    "+self.message.cotEvent.detail.xmlDetail+"
    ") + for xml_detail in xml_details: + if xml_detail.tag == "fileshare": + file_hash = xml_detail.attrib.get("sha256") + filename = xml_detail.attrib.get("filename") + return file_hash, filename + + return False + +def get_size_and_truncate(msg): + size = 0 + shift = 0 + while True: + byte = msg[0] + if byte & 128 == 0: + size = size | (byte << shift) + return size, msg[1:] + else: + size = size | ((byte & 127) << shift) + shift = shift + 7 + msg = msg[1:] + +def get_msg_size(msg): + size = 0 + shift = 0 + hdr_size = 2 + while len(msg) > 0: + byte = msg[0] + if byte & 128 == 0: + size = size | (byte << shift) + return size + hdr_size + else: + size = size | ((byte & 127) << shift) + shift = shift + 7 + msg = msg[1:] + hdr_size += 1 + return False + +def get_size_bytes(msg): + size_bytes = bytearray() + msg_size = len(msg) + while True: + if msg_size & ~127 == 0: + size_bytes.append(msg_size) + return bytes(size_bytes) + else: + size_byte = (msg_size & 127) | 128 + size_bytes.append(size_byte) + msg_size = msg_size >> 7 + + + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/mission_api.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/mission_api.py new file mode 100755 index 00000000..2416d13e --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/mission_api.py @@ -0,0 +1,718 @@ +import cgi +import json +import multiprocessing +import os +import random +import requests +import time + +import zmq + +from pprint import pprint +from tempfile import NamedTemporaryFile + +from requests import Session +from requests.adapters import HTTPAdapter + +from OpenSSL import SSL + +from utils import p12_to_pem + + +class IgnoreHostNameAdapter(HTTPAdapter): + def init_poolmanager(self, *args, **kwargs): + kwargs['assert_hostname'] = False + return super(IgnoreHostNameAdapter, self).init_poolmanager(*args, **kwargs) + + +class MissionApiSession: + + def __init__(self, host, port, certfile=None, password=None, uid=None): + self.base_url = "https://{host}:{port}/Marti/".format(host=host, port=port) + self.base_mission_api = self.base_url + "api/missions/" + self.base_sync_api = self.base_url + "sync/" + + self.session = Session() + + self.session.mount("https://", IgnoreHostNameAdapter()) + + self.certfile = certfile + self.password = password + + self.all_missions = dict() + self.missions_created = dict() + + self.file_hash_map = dict() + + self.uid = uid + + def request(self, *args, **kwargs): + with p12_to_pem(self.certfile, self.password) as cert_file: + self.session.cert = cert_file + self.session.verify = cert_file + + if "timeout" not in kwargs: + kwargs['timeout'] = 60 + + sslErrorRetry = 0 + requestExceptionRetry = 0 + exc = None + while (sslErrorRetry < 10): + try: + return self.session.request(*args, **kwargs) + except SSL.Error as e: # not really sure why this happens, but it seems to be maybe that the temp file isn't yet available? + # print(e) + # print(self.uid, "SSL error, I think its a timing thing?", args, kwargs) + # print("trying again") + exc = e + sslErrorRetry += 1 + time.sleep(0.1) + except requests.Timeout as e: + exc = e + requestExceptionRetry += 1 + time.sleep(0.1) + except requests.RequestException as e: + exc = e + requestExceptionRetry += 1 + time.sleep(0.1) + if requestExceptionRetry > 2: + r = requests.Response() + r.status_code = 400 + r._content = str(e).encode() + return r + + #print("failed to request:", args, kwargs) + raise Exception("Failed to request: " + str(args) + str(kwargs)) + + def get(self, *args, **kwargs): + return self.request("get", *args, **kwargs) + + def post(self, *args, **kwargs): + return self.request("post", *args, **kwargs) + + def put(self, *args, **kwargs): + return self.request("put", *args, **kwargs) + + def delete(self, *args, **kwargs): + return self.request("delete", *args, **kwargs) + + ############### Mission Information/Subscription ##################### + def get_all_missions(self): + + response = self.get(self.base_mission_api) + + if response.status_code != 200: + print("ERROR: ", response.text) + return + + mission_data = response.json()['data'] + + for mission in mission_data: + self.all_missions[mission['name']] = mission + + def get_mission(self, mission): + + response = self.get(self.base_mission_api + mission) + + if response.status_code != 200: + print(("ERROR: ", response.text)) + return + + mission_data = response.json()['data'] + self.all_missions[mission] = mission_data + + def get_mission_cot(self, mission): + + response = self.get(self.base_mission_api + mission + "/cot") + + if response.status_code != 200: + print(("ERROR: ", response.text)) + return + + def get_client_endpoints(self): + + cep_url = self.base_url + "api/clientEndPoints" + + #print("client endpoints url: " + cep_url) + + response = self.get(cep_url) + + #print(("clientEndPoints response: ", response.text)) + + if response.status_code != 200: + print(("ERROR: ", response.text)) + return + + def create_mission(self, mission_name, + creator_uid=None, + group=None, + tool=None, + description=None): + + ret_value = True + + creator_uid = creator_uid or self.uid or None + if not creator_uid: + print("ERROR: missing creatorUID need to create mission") + return + + if not group or not isinstance(group, str): + group = "__ANON__" + + query_params = {'creatorUid': creator_uid, + 'group': group} + + if tool is not None: + query_params['tool'] = tool + if description is not None: + query_params['description'] = description + + response = self.put(self.base_mission_api + mission_name, params=query_params) + #pprint(query_params) + if response.status_code == 201: + self.all_missions[mission_name] = response.json()['data'] + self.missions_created[mission_name] = response.json()['data'] + + elif 300 > response.status_code >= 200: + #print(response.status_code, "Mission already exists") + ret_value = False + self.all_missions[mission_name] = response.json()['data'] + + elif response.status_code >= 400: + print("Failed to create mission", response.status_code) + ret_value = False + + return ret_value + + def delete_mission(self, mission_name): + + response = self.delete(self.base_mission_api + mission_name) + + print(("Delete successful:", response.ok)) + + return response + + def add_mission_content(self, mission_name, content_hash=None, content_uid=None): + if not content_hash and not content_uid: + return + + hashes = [content_hash] if content_hash is not None else [] + uids = [content_uid] if content_uid is not None else [] + + data = json.dumps({"hashes": hashes, + "uids": uids}) + + headers = {"Content-Type": "application/json"} + + response = self.put(self.base_mission_api + mission_name + "/contents", data=data, headers=headers) + + return response + + def subscribe_to_mission(self, mission_name, subscriber_uid=None): + subscriber_uid = subscriber_uid or self.uid or None + if not subscriber_uid: + print("ERROR: Missing a subscriber uid. Who is subscribing to the mission?!?") + return + + subscribe_url = self.base_mission_api + mission_name + "/subscription" + params = {'uid': subscriber_uid} + + response = self.put(subscribe_url, params=params) + return response + + def unsubscribe_from_mission(self, mission_name, subscriber_uid=None): + subscriber_uid = subscriber_uid or self.uid + if not subscriber_uid: + print("ERROR: Missing a subscriber uid. Who is unsubscibing from the mission?!?") + return + + unsubscribe_url = self.base_mission_api + mission_name + "/subscription" + params = {'uid': subscriber_uid} + + response = self.delete(unsubscribe_url, params=params) + return response + + def add_datafeed_to_mission(self, mission_name, datafeed_uuid, creator_uid): + my_url = self.base_mission_api + mission_name + "/feed" + params = {'creatorUid': creator_uid, 'dataFeedUid': datafeed_uuid} + + response = self.post(my_url, params=params) + return response + + ########### End Mission Information/Subscription ##################### + + ############## Enterprise Sync ############################################# + def download_file(self, file_hash, filename=None, save=False): + params = {"hash": file_hash} + response = self.get(self.base_sync_api + "content", params=params) + + if filename is None: + value, params = cgi.parse_header(response.headers['Content-Disposition']) + + filename = params['filename'] + + self.file_hash_map[filename] = file_hash + + if save: + with open(filename, 'wb') as down: + down.write(response.content) + + return response + + def upload_file(self, + file_name, + file_object, + content_type, + upload_file_name=None, + creatorUid=None, + uid=None, + latitude=None, + longitude=None, + altitude=None, + keywords=None): + + if upload_file_name is None: + upload_file_name = file_name + + files = {"file": file_object} + params = {"name": upload_file_name} + if creatorUid: + params["creatorUid"] = creatorUid + if uid: + params["uid"] = uid + if latitude: + params["latitude"] = latitude + if longitude: + params["longitude"] = longitude + if altitude: + params["altitude"] = altitude + params['keywords'] = ['pyTakLoadTest'] + if keywords: + params["keywords"].extend(keywords) + + headers = {"Content-Type": content_type} + + try: + #pprint(params) + #pprint(headers) + response = self.post(self.base_sync_api + "upload", params=params, files=files, headers=headers) + #print(response.status_code, response.request.url, response.content) + except requests.exceptions.ConnectionError: + "File {} too big to upload. Limit is 200mb".format(upload_file_name) + return + else: + if response.status_code == 200: + self.file_hash_map[upload_file_name] = str(response.json()['Hash']) + + #print(self.file_hash_map) + return response + + def upload_size_file(self, + file_name, + size, + creatorUid="size-file-creator"): + + with NamedTemporaryFile() as f_object: + f_file = f_object.file + f_file.write(os.urandom(size)) + #print((f_file.tell() / 1000000)) + f_file.seek(0) + keywords = ["size_file"] + self.upload_file(file_name, + f_file, + "application/txt", + creatorUid=creatorUid, + keywords=keywords) + + def delete_file(self, file_name=None, file_hash=None): + file_hash = file_hash or self.file_hash_map.get(file_name) + if not file_hash: + return False + + params = {"hash": file_hash} + response = self.get(self.base_sync_api + "delete", params=params) + if response.status_code == 200: + try: + self.file_hash_map.pop(file_name) + except KeyError: + pass + return True + + return False + + def delete_all_files(self): + for file_name, file_hash in list(self.file_hash_map.items()): + success = self.delete_file(file_name=file_name, file_hash=file_hash) + if not success: + return False + + return True + + def search_files(self, keywords=[]): + params = dict() + if keywords: + params['keywords'].extend(keywords) + res = self.get(self.base_sync_api + "search", params=params) + if res.status_code >= 400: + return [] + files = json.loads(res.content).get('results') + return files + + def delete_all_test_files(self): + files = self.search_files() + if files is None: + return + for f in files: + self.delete_file(file_name=f['Name'], file_hash=f['Hash']) + + +########### End Enterprise Sync ############################################ + + +#################### Initialization Classes ############################### +class MissionApiSetup(MissionApiSession): + + def initialize_takserver(self, config_dict): + + creatorUid = config_dict.get("creatorUid") + group = config_dict.get("group") + tool = config_dict.get("tool") + + files = config_dict.get("files", {}) + size_files = config_dict.get("size_files", {}) + + for f in files: + name, attrs = list(f.items())[0] + if "content_type" not in attrs: + print("ERROR: need to specify a content type for each file") + exit() + print("uploading file", name) + try: + file_object = open(name, "rb") + except (FileExistsError, FileNotFoundError): + print(("couldn't open file: {}".format(name))) + continue + + self.upload_file(name, + file_object, + attrs.get("content_type"), + creatorUid=attrs.get("creatorUid", creatorUid), + uid=attrs.get("uid"), + latitude=attrs.get("latitude"), + longitude=attrs.get("longitude"), + altitude=attrs.get("altitude"), + keywords=attrs.get("keywords")) + print() + print(name, "uploaded") + print() + + for size_file in size_files: + print(size_file.items()) + f_name, f_size = list(size_file.items())[0] + print(("uploading size_file {}".format(f_name))) + self.upload_size_file(f_name, f_size) + print() + print(f_name, "uploaded") + print() + + # pull existing missions + get_all_missions = self.get(self.base_mission_api) + existing_missions = {} + for mission_entry in get_all_missions.json()['data']: + existing_missions[mission_entry['name']] = mission_entry + + missions = config_dict.get("missions", {}) + pprint(missions) + print(type(missions)) + + for m in missions: + # skip mission init if it exists + name = '' + if not type(m) == dict: + name = m + else: + name, attrs = list(m.items())[0] + + if name in existing_missions: + self.all_missions[name] = existing_missions[name] + self.missions_created[name] = existing_missions[name] + print('\nexisting mission, skipping create', name, '\n') + continue + + + print("creating mission", m) + if not type(m) == dict: + self.create_mission(m, creator_uid=creatorUid, group=group) + continue + + name, attrs = list(m.items())[0] # because there is only one mission per mission dict + if attrs is None: + attrs = {} + self.create_mission(name, + creator_uid=attrs.get("creatorUid", creatorUid), + group=attrs.get("group", group)) + + self.create_mission(name, + creator_uid=attrs.get("creatorUid", creatorUid), + group=attrs.get("group", group), + tool=attrs.get("tool", tool), + description=attrs.get("description")) + self.unsubscribe_from_mission(name, subscriber_uid=attrs.get("creatorUid", creatorUid)) + print() + print("mission created") + print() + + if "files" in attrs: + for f in attrs.get("files"): + self.add_mission_content(name, content_hash=self.file_hash_map.get(f)) + + # Add datafeed to mission: + if "datafeed_uuids" in attrs: + print("Adding datafeeds to mission " + name) + for datafeed_uuid in attrs.get("datafeed_uuids"): + self.add_datafeed_to_mission(name, datafeed_uuid = datafeed_uuid, creator_uid=attrs.get("creatorUid", creatorUid)) + print("Added datafeed " + datafeed_uuid + " to mission " + name) + print("Done adding datafeeds to mission " + name) + print() + if "datafeed_uuid_pattern" in attrs: + pattern = attrs.get("datafeed_uuid_pattern").get("pattern") + start_i = attrs.get("datafeed_uuid_pattern").get("start_i") + end_i = attrs.get("datafeed_uuid_pattern").get("end_i") + if (pattern is None or start_i is None or end_i is None): + print("ERROR: missing params in datafeed_uuid_pattern") + exit(1) + print("Adding datafeeds with pattern " + pattern + " to mission " + name) + for i in range(int(start_i), int(end_i)+1): + datafeed_uuid = pattern.replace("[i]", str(i)) + self.add_datafeed_to_mission(name, datafeed_uuid = datafeed_uuid, creator_uid=attrs.get("creatorUid", creatorUid)) + print("Added datafeed " + datafeed_uuid + " to mission " + name) + print("Done adding datafeeds with pattern " + pattern + " to mission" + name) + + + def cleanup(self): + print("cleaning up initialization data on server") + self.delete_all_test_files() + for m in self.all_missions: + self.delete_mission(m) + + +class MissionApiSetupProcess(multiprocessing.Process): + + def __init__(self, address, cert, password, config_dict, lock=None): + multiprocessing.Process.__init__(self) + self.setup_session = MissionApiSetup(address[0], + address[1], + cert, password) + + self.lock = None + if lock is not None: + self.lock = lock + self.lock.acquire() + self.config_dict = config_dict + self.uid = "Init Process" + + def run(self): + print("starting init") + try: + self.setup_session.initialize_takserver(self.config_dict) + print("init started") + if self.lock: + self.lock.release() + while True: + time.sleep(1) + except KeyboardInterrupt: + if self.lock: + try: + self.lock.release() + except: + pass + self.setup_session.cleanup() + +###################### End Initialization Classes ################################## + + +class MissionApiPyTAKHelper(MissionApiSession): + + def __init__(self, host, port, certfile=None, password=None, uid=None, + mission_config=None): + + MissionApiSession.__init__(self, host, port, certfile, password, uid) + + if mission_config is not None: + self.write_delta = mission_config.get("mission_write_interval", 1.0) + self.last_write = time.time() - 2 * self.write_delta + self.do_write = False + + self.upload_config = mission_config.get("uploads", {}) + self.upload_probability = self.upload_config.get("probability", 0) + self.upload_size = self.upload_config.get("size", 0) + self.upload_interval = self.upload_config.get("interval", 0) + self.do_upload = False + + self.last_upload = time.time() + self.next_upload = random.uniform(0, self.upload_interval) + self.last_upload + self.did_upload = False + self.upload_count = {} + + self.missions_subscribed = set() + self.missions_to_subscribe = mission_config.get("subscribe", 0) + self.select_random = mission_config.get("random", False) + + download_mission_content = mission_config.get("download_mission_content", 1.0) + self.download_mission_content = max(0, min(1, download_mission_content)) + + self.download_existing_content = mission_config.get("download_existing_content", False) + + self.num_missions_created = 0 + + context = zmq.Context() + self.request_queue = context.socket(zmq.PULL) + self.port = self.request_queue.bind_to_random_port('tcp://*') + self.poll = False + + else: + self.write_delta = None + self.poll = False + self.do_write = False + self.do_upload = False + self.upload_interval = 0 + + + def ready_to_request(self): + if (self.write_delta is None): + return False + self.poll = self.request_queue.poll(1) == zmq.POLLIN + now = time.time() + self.do_write = (now - self.last_write > self.write_delta) + if self.upload_interval > 0: + self.do_upload = (not self.did_upload) and (now > self.next_upload) + if now - self.last_upload > self.upload_interval: + self.last_upload = now + self.next_upload = random.uniform(0, self.upload_interval) + self.last_upload + self.did_upload = False + return self.do_write or self.do_upload or self.poll + + + + def get_and_subscribe_to_mission(self, mission_name): + self.get_mission(mission_name) + self.get_mission_cot(mission_name) + if self.download_existing_content: + for c in self.all_missions[mission_name][0].get('contents', {}): + file_data = c['data'] + if file_data['hash'] == self.file_hash_map.get(file_data['name']): + continue + self.download_file(c['data']['hash']) + c['data']['downloaded'] = True + + def make_requests(self): + + if self.do_write: + if len(self.all_missions) == 0: + self.get_all_missions() + for mission in self.all_missions.keys(): + self.unsubscribe_from_mission(mission, subscriber_uid=self.uid) + + if self.select_random and len(self.missions_subscribed) < self.missions_to_subscribe: + if len(self.missions_subscribed) == len(self.all_missions): + remaining = 0 + elif self.missions_to_subscribe > len(self.all_missions): + remaining = len(self.all_missions) - len(self.missions_subscribed) + else: + remaining = self.missions_to_subscribe - len(self.missions_subscribed) + + missions_to_sub = random.sample(set(self.all_missions.keys()).difference(self.missions_subscribed), + remaining) + for m in missions_to_sub: + if not self.subscribe_to_mission(m): + self.missions_to_subscribe.remove(m) + continue + self.get_and_subscribe_to_mission(m) + + elif (not self.select_random) and len(set(self.missions_to_subscribe).difference(self.missions_subscribed)) > 0: + remaining = True + if len(self.missions_subscribed) == len(self.all_missions): + remaining = False + self.missions_to_subscribe = self.missions_subscribed + + if remaining: + missions_to_sub = set(self.missions_to_subscribe).difference(self.missions_subscribed) + # print("missions left to sub", missions_to_sub) + for m in missions_to_sub: + if not self.subscribe_to_mission(m): + self.missions_to_subscribe.remove(m) + continue + self.get_and_subscribe_to_mission(m) + self.last_write = time.time() + self.do_write = False + + if self.do_upload: + for mission in self.missions_subscribed: + if self.upload_probability > random.uniform(0, 1): + mission_upload_count = self.upload_count.get(mission, 0) + file_name = self.uid + "_mission_" + mission + "_" + str(mission_upload_count) + self.upload_size_file(file_name, self.upload_size, creatorUid=self.uid) + file_hash = self.file_hash_map.get(file_name) + if file_hash is not None: + self.add_mission_content(mission, content_hash=file_hash) + if mission_upload_count == 0: + self.upload_count[mission] = 1 + else: + self.upload_count[mission] += 1 + self.did_upload = True + self.do_upload = False + + if self.poll: + data = self.request_queue.recv_json() + req_type = data['req_type'] + req_data = data['req_data'] + + if req_type == "CREATE": + self.get_mission(req_data) + elif req_type == "INVITE": + if not req_data in self.all_missions: + self.get_mission(req_data) + success = self.subscribe_to_mission(req_data) + if success: + self.get_mission_cot(req_data) + elif req_type == "DELETE": + try: + self.all_missions.pop(req_data, None) + self.missions_subscribed.remove(req_data) + self.missions_created.pop(req_data, None) + except Exception as e: + print(e) + + elif req_type == "download_mission_files": + if self.download_mission_content > random.uniform(0, 1): + for file_hash in req_data: + if file_hash not in self.file_hash_map.values(): + self.download_file(file_hash=file_hash) + + elif req_type == "download_file": + file_hash, filename = req_data + self.download_file(file_hash=file_hash, filename=filename) + + def create_dynamic_mission(self): + mission_name = self.uid + "_mission_" + str(self.num_missions_created) + self.num_missions_created += 1 + if not self.create_mission(mission_name, + creator_uid=self.uid, + group="__ANON__"): + self.delete_mission(mission_name) + else: + self.create_mission(mission_name, + creator_uid=self.uid, + group="__ANON__", + tool="PyTAK", + description="dynamically created mission by " + self.uid) + + def subscribe_to_mission(self, mission_name, subscriber_uid=None): + response = MissionApiSession.subscribe_to_mission(self, mission_name, subscriber_uid) + if response.status_code >= 300: + print(self.uid, "failed to subscribe to mission {}:".format(mission_name), response.status_code, response.text) + return False + self.missions_subscribed.add(mission_name) + + return True + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/__init__.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/__init__.py new file mode 100755 index 00000000..e69de29b diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/contact_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/contact_pb2.py new file mode 100755 index 00000000..4dd196e4 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/contact_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: contact.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='contact.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\rcontact.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\"-\n\x07\x43ontact\x12\x10\n\x08\x65ndpoint\x18\x01 \x01(\t\x12\x10\n\x08\x63\x61llsign\x18\x02 \x01(\tb\x06proto3' +) + + + + +_CONTACT = _descriptor.Descriptor( + name='Contact', + full_name='atakmap.commoncommo.protobuf.v1.Contact', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='endpoint', full_name='atakmap.commoncommo.protobuf.v1.Contact.endpoint', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='callsign', full_name='atakmap.commoncommo.protobuf.v1.Contact.callsign', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=50, + serialized_end=95, +) + +DESCRIPTOR.message_types_by_name['Contact'] = _CONTACT +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Contact = _reflection.GeneratedProtocolMessageType('Contact', (_message.Message,), { + 'DESCRIPTOR' : _CONTACT, + '__module__' : 'contact_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.Contact) + }) +_sym_db.RegisterMessage(Contact) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/cotevent_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/cotevent_pb2.py new file mode 100755 index 00000000..3f3263ab --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/cotevent_pb2.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: cotevent.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import protobuf_msg.detail_pb2 as detail__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='cotevent.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x0e\x63otevent.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\x1a\x0c\x64\x65tail.proto\"\x8d\x02\n\x08\x43otEvent\x12\x0c\n\x04type\x18\x01 \x01(\t\x12\x0e\n\x06\x61\x63\x63\x65ss\x18\x02 \x01(\t\x12\x0b\n\x03qos\x18\x03 \x01(\t\x12\x0c\n\x04opex\x18\x04 \x01(\t\x12\x0b\n\x03uid\x18\x05 \x01(\t\x12\x10\n\x08sendTime\x18\x06 \x01(\x04\x12\x11\n\tstartTime\x18\x07 \x01(\x04\x12\x11\n\tstaleTime\x18\x08 \x01(\x04\x12\x0b\n\x03how\x18\t \x01(\t\x12\x0b\n\x03lat\x18\n \x01(\x01\x12\x0b\n\x03lon\x18\x0b \x01(\x01\x12\x0b\n\x03hae\x18\x0c \x01(\x01\x12\n\n\x02\x63\x65\x18\r \x01(\x01\x12\n\n\x02le\x18\x0e \x01(\x01\x12\x37\n\x06\x64\x65tail\x18\x0f \x01(\x0b\x32\'.atakmap.commoncommo.protobuf.v1.Detailb\x06proto3' + , + dependencies=[detail__pb2.DESCRIPTOR,]) + + + + +_COTEVENT = _descriptor.Descriptor( + name='CotEvent', + full_name='atakmap.commoncommo.protobuf.v1.CotEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.type', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='access', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.access', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='qos', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.qos', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='opex', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.opex', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='uid', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.uid', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sendTime', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.sendTime', index=5, + number=6, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='startTime', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.startTime', index=6, + number=7, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='staleTime', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.staleTime', index=7, + number=8, type=4, cpp_type=4, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='how', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.how', index=8, + number=9, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='lat', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.lat', index=9, + number=10, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='lon', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.lon', index=10, + number=11, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='hae', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.hae', index=11, + number=12, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ce', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.ce', index=12, + number=13, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='le', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.le', index=13, + number=14, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='detail', full_name='atakmap.commoncommo.protobuf.v1.CotEvent.detail', index=14, + number=15, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=66, + serialized_end=335, +) + +_COTEVENT.fields_by_name['detail'].message_type = detail__pb2._DETAIL +DESCRIPTOR.message_types_by_name['CotEvent'] = _COTEVENT +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +CotEvent = _reflection.GeneratedProtocolMessageType('CotEvent', (_message.Message,), { + 'DESCRIPTOR' : _COTEVENT, + '__module__' : 'cotevent_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.CotEvent) + }) +_sym_db.RegisterMessage(CotEvent) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/detail_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/detail_pb2.py new file mode 100755 index 00000000..73fe3943 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/detail_pb2.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: detail.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import protobuf_msg.contact_pb2 as contact__pb2 +import protobuf_msg.group_pb2 as group__pb2 +import protobuf_msg.precisionlocation_pb2 as precisionlocation__pb2 +import protobuf_msg.status_pb2 as status__pb2 +import protobuf_msg.takv_pb2 as takv__pb2 +import protobuf_msg.track_pb2 as track__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='detail.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x0c\x64\x65tail.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\x1a\rcontact.proto\x1a\x0bgroup.proto\x1a\x17precisionlocation.proto\x1a\x0cstatus.proto\x1a\ntakv.proto\x1a\x0btrack.proto\"\x81\x03\n\x06\x44\x65tail\x12\x11\n\txmlDetail\x18\x01 \x01(\t\x12\x39\n\x07\x63ontact\x18\x02 \x01(\x0b\x32(.atakmap.commoncommo.protobuf.v1.Contact\x12\x35\n\x05group\x18\x03 \x01(\x0b\x32&.atakmap.commoncommo.protobuf.v1.Group\x12M\n\x11precisionLocation\x18\x04 \x01(\x0b\x32\x32.atakmap.commoncommo.protobuf.v1.PrecisionLocation\x12\x37\n\x06status\x18\x05 \x01(\x0b\x32\'.atakmap.commoncommo.protobuf.v1.Status\x12\x33\n\x04takv\x18\x06 \x01(\x0b\x32%.atakmap.commoncommo.protobuf.v1.Takv\x12\x35\n\x05track\x18\x07 \x01(\x0b\x32&.atakmap.commoncommo.protobuf.v1.Trackb\x06proto3' + , + dependencies=[contact__pb2.DESCRIPTOR,group__pb2.DESCRIPTOR,precisionlocation__pb2.DESCRIPTOR,status__pb2.DESCRIPTOR,takv__pb2.DESCRIPTOR,track__pb2.DESCRIPTOR,]) + + + + +_DETAIL = _descriptor.Descriptor( + name='Detail', + full_name='atakmap.commoncommo.protobuf.v1.Detail', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='xmlDetail', full_name='atakmap.commoncommo.protobuf.v1.Detail.xmlDetail', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='contact', full_name='atakmap.commoncommo.protobuf.v1.Detail.contact', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='group', full_name='atakmap.commoncommo.protobuf.v1.Detail.group', index=2, + number=3, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='precisionLocation', full_name='atakmap.commoncommo.protobuf.v1.Detail.precisionLocation', index=3, + number=4, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='status', full_name='atakmap.commoncommo.protobuf.v1.Detail.status', index=4, + number=5, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='takv', full_name='atakmap.commoncommo.protobuf.v1.Detail.takv', index=5, + number=6, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='track', full_name='atakmap.commoncommo.protobuf.v1.Detail.track', index=6, + number=7, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=142, + serialized_end=527, +) + +_DETAIL.fields_by_name['contact'].message_type = contact__pb2._CONTACT +_DETAIL.fields_by_name['group'].message_type = group__pb2._GROUP +_DETAIL.fields_by_name['precisionLocation'].message_type = precisionlocation__pb2._PRECISIONLOCATION +_DETAIL.fields_by_name['status'].message_type = status__pb2._STATUS +_DETAIL.fields_by_name['takv'].message_type = takv__pb2._TAKV +_DETAIL.fields_by_name['track'].message_type = track__pb2._TRACK +DESCRIPTOR.message_types_by_name['Detail'] = _DETAIL +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Detail = _reflection.GeneratedProtocolMessageType('Detail', (_message.Message,), { + 'DESCRIPTOR' : _DETAIL, + '__module__' : 'detail_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.Detail) + }) +_sym_db.RegisterMessage(Detail) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/fig_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/fig_pb2.py new file mode 100755 index 00000000..f9ce717b --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/fig_pb2.py @@ -0,0 +1,917 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: fig.proto + +from google.protobuf.internal import enum_type_wrapper +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='fig.proto', + package='com.atakmap', + syntax='proto3', + serialized_options=b'\n\017com.atakmap.TakB\010FigProtoP\001\242\002\003TAK', + serialized_pb=b'\n\tfig.proto\x12\x0b\x63om.atakmap\"~\n\x0e\x46\x65\x64\x65ratedEvent\x12$\n\x05\x65vent\x18\x01 \x01(\x0b\x32\x15.com.atakmap.GeoEvent\x12.\n\x07\x63ontact\x18\x02 \x01(\x0b\x32\x1d.com.atakmap.ContactListEntry\x12\x16\n\x0e\x66\x65\x64\x65rateGroups\x18\x03 \x03(\t\"\xa5\x03\n\x08GeoEvent\x12\x10\n\x08sendTime\x18\x01 \x01(\x03\x12\x11\n\tstartTime\x18\x02 \x01(\x03\x12\x11\n\tstaleTime\x18\x03 \x01(\x03\x12\x0b\n\x03lat\x18\x04 \x01(\x01\x12\x0b\n\x03lon\x18\x05 \x01(\x01\x12\x0b\n\x03hae\x18\x06 \x01(\x01\x12\n\n\x02\x63\x65\x18\x07 \x01(\x01\x12\n\n\x02le\x18\x08 \x01(\x01\x12\x0b\n\x03uid\x18\t \x01(\t\x12\x0c\n\x04type\x18\n \x01(\t\x12\x13\n\x0b\x63oordSource\x18\x0b \x01(\t\x12\r\n\x05other\x18\x0c \x01(\t\x12\x0f\n\x07\x62\x61ttery\x18\r \x01(\x05\x12\x0c\n\x04ploc\x18\x0e \x01(\t\x12\x0c\n\x04palt\x18\x0f \x01(\t\x12\x12\n\nscreenName\x18\x10 \x01(\t\x12\x11\n\tgroupName\x18\x11 \x01(\t\x12\x11\n\tgroupRole\x18\x12 \x01(\t\x12\r\n\x05phone\x18\x13 \x01(\t\x12\r\n\x05speed\x18\x14 \x01(\x01\x12\x0e\n\x06\x63ourse\x18\x15 \x01(\x01\x12\'\n\x06\x62inary\x18\x16 \x01(\x0b\x32\x17.com.atakmap.BinaryBlob\x12\x0f\n\x07ptpUids\x18\x17 \x03(\t\x12\x14\n\x0cptpCallsigns\x18\x18 \x03(\t\"}\n\nBinaryBlob\x12\'\n\x04type\x18\x01 \x01(\x0e\x32\x19.com.atakmap.BINARY_TYPES\x12\x0c\n\x04\x64\x61ta\x18\x02 \x01(\x0c\x12\x10\n\x08\x66ilename\x18\x03 \x01(\t\x12\x11\n\ttimestamp\x18\x04 \x01(\x03\x12\x13\n\x0b\x64\x65scription\x18\x05 \x01(\t\"\x8a\x01\n\x10\x43ontactListEntry\x12$\n\toperation\x18\x01 \x01(\x0e\x32\x11.com.atakmap.CRUD\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x10\n\x08\x63\x61llsign\x18\x03 \x01(\t\x12\r\n\x05phone\x18\x04 \x01(\t\x12\x0b\n\x03sip\x18\x05 \x01(\t\x12\x15\n\rdirectConnect\x18\x06 \x01(\t\"\x07\n\x05\x45mpty\":\n\x08Identity\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0b\n\x03uid\x18\x02 \x01(\t\x12\x13\n\x0b\x64\x65scription\x18\x03 \x01(\t\"G\n\x0cSubscription\x12\'\n\x08identity\x18\x01 \x01(\x0b\x32\x15.com.atakmap.Identity\x12\x0e\n\x06\x66ilter\x18\x02 \x01(\t\"\x83\x01\n\x0c\x43lientHealth\x12\x37\n\x06status\x18\x01 \x01(\x0e\x32\'.com.atakmap.ClientHealth.ServingStatus\":\n\rServingStatus\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SERVING\x10\x01\x12\x0f\n\x0bNOT_SERVING\x10\x02\"\x96\x01\n\x0cServerHealth\x12\x37\n\x06status\x18\x01 \x01(\x0e\x32\'.com.atakmap.ServerHealth.ServingStatus\"M\n\rServingStatus\x12\x0b\n\x07UNKNOWN\x10\x00\x12\x0b\n\x07SERVING\x10\x01\x12\x0f\n\x0bNOT_SERVING\x10\x02\x12\x11\n\rNOT_CONNECTED\x10\x03\"@\n\x03ROL\x12\x0f\n\x07program\x18\x01 \x01(\t\x12(\n\x07payload\x18\x02 \x03(\x0b\x32\x17.com.atakmap.BinaryBlob*A\n\x04\x43RUD\x12\x0b\n\x07INVALID\x10\x00\x12\n\n\x06\x43REATE\x10\x01\x12\x08\n\x04READ\x10\x02\x12\n\n\x06UPDATE\x10\x03\x12\n\n\x06\x44\x45LETE\x10\x04*/\n\x0c\x42INARY_TYPES\x12\t\n\x05\x45MPTY\x10\x00\x12\t\n\x05OTHER\x10\x01\x12\t\n\x05IMAGE\x10\x02\x32\x88\x05\n\x10\x46\x65\x64\x65ratedChannel\x12\x41\n\x0cSendOneEvent\x12\x1b.com.atakmap.FederatedEvent\x1a\x12.com.atakmap.Empty\"\x00\x12\x46\n\x13\x42inaryMessageStream\x12\x17.com.atakmap.BinaryBlob\x1a\x12.com.atakmap.Empty\"\x00(\x01\x12<\n\x0bSendOneBlob\x12\x17.com.atakmap.BinaryBlob\x1a\x12.com.atakmap.Empty\"\x00\x12:\n\x0bgetIdentity\x12\x12.com.atakmap.Empty\x1a\x15.com.atakmap.Identity\"\x00\x12O\n\x11\x43lientEventStream\x12\x19.com.atakmap.Subscription\x1a\x1b.com.atakmap.FederatedEvent\"\x00\x30\x01\x12O\n\x11ServerEventStream\x12\x1b.com.atakmap.FederatedEvent\x1a\x19.com.atakmap.Subscription\"\x00(\x01\x12\x45\n\x0bHealthCheck\x12\x19.com.atakmap.ClientHealth\x1a\x19.com.atakmap.ServerHealth\"\x00\x12\x42\n\x0f\x43lientROLStream\x12\x19.com.atakmap.Subscription\x1a\x10.com.atakmap.ROL\"\x00\x30\x01\x12\x42\n\x0fServerROLStream\x12\x10.com.atakmap.ROL\x1a\x19.com.atakmap.Subscription\"\x00(\x01\x42#\n\x0f\x63om.atakmap.TakB\x08\x46igProtoP\x01\xa2\x02\x03TAKb\x06proto3' +) + +_CRUD = _descriptor.EnumDescriptor( + name='CRUD', + full_name='com.atakmap.CRUD', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='INVALID', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='CREATE', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='READ', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='UPDATE', index=3, number=3, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='DELETE', index=4, number=4, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=1341, + serialized_end=1406, +) +_sym_db.RegisterEnumDescriptor(_CRUD) + +CRUD = enum_type_wrapper.EnumTypeWrapper(_CRUD) +_BINARY_TYPES = _descriptor.EnumDescriptor( + name='BINARY_TYPES', + full_name='com.atakmap.BINARY_TYPES', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='EMPTY', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='OTHER', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='IMAGE', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=1408, + serialized_end=1455, +) +_sym_db.RegisterEnumDescriptor(_BINARY_TYPES) + +BINARY_TYPES = enum_type_wrapper.EnumTypeWrapper(_BINARY_TYPES) +INVALID = 0 +CREATE = 1 +READ = 2 +UPDATE = 3 +DELETE = 4 +EMPTY = 0 +OTHER = 1 +IMAGE = 2 + + +_CLIENTHEALTH_SERVINGSTATUS = _descriptor.EnumDescriptor( + name='ServingStatus', + full_name='com.atakmap.ClientHealth.ServingStatus', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='UNKNOWN', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVING', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='NOT_SERVING', index=2, number=2, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=1062, + serialized_end=1120, +) +_sym_db.RegisterEnumDescriptor(_CLIENTHEALTH_SERVINGSTATUS) + +_SERVERHEALTH_SERVINGSTATUS = _descriptor.EnumDescriptor( + name='ServingStatus', + full_name='com.atakmap.ServerHealth.ServingStatus', + filename=None, + file=DESCRIPTOR, + values=[ + _descriptor.EnumValueDescriptor( + name='UNKNOWN', index=0, number=0, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='SERVING', index=1, number=1, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='NOT_SERVING', index=2, number=2, + serialized_options=None, + type=None), + _descriptor.EnumValueDescriptor( + name='NOT_CONNECTED', index=3, number=3, + serialized_options=None, + type=None), + ], + containing_type=None, + serialized_options=None, + serialized_start=1196, + serialized_end=1273, +) +_sym_db.RegisterEnumDescriptor(_SERVERHEALTH_SERVINGSTATUS) + + +_FEDERATEDEVENT = _descriptor.Descriptor( + name='FederatedEvent', + full_name='com.atakmap.FederatedEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='event', full_name='com.atakmap.FederatedEvent.event', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='contact', full_name='com.atakmap.FederatedEvent.contact', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='federateGroups', full_name='com.atakmap.FederatedEvent.federateGroups', index=2, + number=3, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=26, + serialized_end=152, +) + + +_GEOEVENT = _descriptor.Descriptor( + name='GeoEvent', + full_name='com.atakmap.GeoEvent', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='sendTime', full_name='com.atakmap.GeoEvent.sendTime', index=0, + number=1, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='startTime', full_name='com.atakmap.GeoEvent.startTime', index=1, + number=2, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='staleTime', full_name='com.atakmap.GeoEvent.staleTime', index=2, + number=3, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='lat', full_name='com.atakmap.GeoEvent.lat', index=3, + number=4, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='lon', full_name='com.atakmap.GeoEvent.lon', index=4, + number=5, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='hae', full_name='com.atakmap.GeoEvent.hae', index=5, + number=6, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ce', full_name='com.atakmap.GeoEvent.ce', index=6, + number=7, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='le', full_name='com.atakmap.GeoEvent.le', index=7, + number=8, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='uid', full_name='com.atakmap.GeoEvent.uid', index=8, + number=9, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='type', full_name='com.atakmap.GeoEvent.type', index=9, + number=10, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='coordSource', full_name='com.atakmap.GeoEvent.coordSource', index=10, + number=11, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='other', full_name='com.atakmap.GeoEvent.other', index=11, + number=12, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='battery', full_name='com.atakmap.GeoEvent.battery', index=12, + number=13, type=5, cpp_type=1, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ploc', full_name='com.atakmap.GeoEvent.ploc', index=13, + number=14, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='palt', full_name='com.atakmap.GeoEvent.palt', index=14, + number=15, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='screenName', full_name='com.atakmap.GeoEvent.screenName', index=15, + number=16, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='groupName', full_name='com.atakmap.GeoEvent.groupName', index=16, + number=17, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='groupRole', full_name='com.atakmap.GeoEvent.groupRole', index=17, + number=18, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='phone', full_name='com.atakmap.GeoEvent.phone', index=18, + number=19, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='speed', full_name='com.atakmap.GeoEvent.speed', index=19, + number=20, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='course', full_name='com.atakmap.GeoEvent.course', index=20, + number=21, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='binary', full_name='com.atakmap.GeoEvent.binary', index=21, + number=22, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ptpUids', full_name='com.atakmap.GeoEvent.ptpUids', index=22, + number=23, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='ptpCallsigns', full_name='com.atakmap.GeoEvent.ptpCallsigns', index=23, + number=24, type=9, cpp_type=9, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=155, + serialized_end=576, +) + + +_BINARYBLOB = _descriptor.Descriptor( + name='BinaryBlob', + full_name='com.atakmap.BinaryBlob', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='type', full_name='com.atakmap.BinaryBlob.type', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='data', full_name='com.atakmap.BinaryBlob.data', index=1, + number=2, type=12, cpp_type=9, label=1, + has_default_value=False, default_value=b"", + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='filename', full_name='com.atakmap.BinaryBlob.filename', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='timestamp', full_name='com.atakmap.BinaryBlob.timestamp', index=3, + number=4, type=3, cpp_type=2, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='com.atakmap.BinaryBlob.description', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=578, + serialized_end=703, +) + + +_CONTACTLISTENTRY = _descriptor.Descriptor( + name='ContactListEntry', + full_name='com.atakmap.ContactListEntry', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='operation', full_name='com.atakmap.ContactListEntry.operation', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='uid', full_name='com.atakmap.ContactListEntry.uid', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='callsign', full_name='com.atakmap.ContactListEntry.callsign', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='phone', full_name='com.atakmap.ContactListEntry.phone', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='sip', full_name='com.atakmap.ContactListEntry.sip', index=4, + number=5, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='directConnect', full_name='com.atakmap.ContactListEntry.directConnect', index=5, + number=6, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=706, + serialized_end=844, +) + + +_EMPTY = _descriptor.Descriptor( + name='Empty', + full_name='com.atakmap.Empty', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=846, + serialized_end=853, +) + + +_IDENTITY = _descriptor.Descriptor( + name='Identity', + full_name='com.atakmap.Identity', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='com.atakmap.Identity.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='uid', full_name='com.atakmap.Identity.uid', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='description', full_name='com.atakmap.Identity.description', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=855, + serialized_end=913, +) + + +_SUBSCRIPTION = _descriptor.Descriptor( + name='Subscription', + full_name='com.atakmap.Subscription', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='identity', full_name='com.atakmap.Subscription.identity', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='filter', full_name='com.atakmap.Subscription.filter', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=915, + serialized_end=986, +) + + +_CLIENTHEALTH = _descriptor.Descriptor( + name='ClientHealth', + full_name='com.atakmap.ClientHealth', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='com.atakmap.ClientHealth.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _CLIENTHEALTH_SERVINGSTATUS, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=989, + serialized_end=1120, +) + + +_SERVERHEALTH = _descriptor.Descriptor( + name='ServerHealth', + full_name='com.atakmap.ServerHealth', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='status', full_name='com.atakmap.ServerHealth.status', index=0, + number=1, type=14, cpp_type=8, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + _SERVERHEALTH_SERVINGSTATUS, + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1123, + serialized_end=1273, +) + + +_ROL = _descriptor.Descriptor( + name='ROL', + full_name='com.atakmap.ROL', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='program', full_name='com.atakmap.ROL.program', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='payload', full_name='com.atakmap.ROL.payload', index=1, + number=2, type=11, cpp_type=10, label=3, + has_default_value=False, default_value=[], + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=1275, + serialized_end=1339, +) + +_FEDERATEDEVENT.fields_by_name['event'].message_type = _GEOEVENT +_FEDERATEDEVENT.fields_by_name['contact'].message_type = _CONTACTLISTENTRY +_GEOEVENT.fields_by_name['binary'].message_type = _BINARYBLOB +_BINARYBLOB.fields_by_name['type'].enum_type = _BINARY_TYPES +_CONTACTLISTENTRY.fields_by_name['operation'].enum_type = _CRUD +_SUBSCRIPTION.fields_by_name['identity'].message_type = _IDENTITY +_CLIENTHEALTH.fields_by_name['status'].enum_type = _CLIENTHEALTH_SERVINGSTATUS +_CLIENTHEALTH_SERVINGSTATUS.containing_type = _CLIENTHEALTH +_SERVERHEALTH.fields_by_name['status'].enum_type = _SERVERHEALTH_SERVINGSTATUS +_SERVERHEALTH_SERVINGSTATUS.containing_type = _SERVERHEALTH +_ROL.fields_by_name['payload'].message_type = _BINARYBLOB +DESCRIPTOR.message_types_by_name['FederatedEvent'] = _FEDERATEDEVENT +DESCRIPTOR.message_types_by_name['GeoEvent'] = _GEOEVENT +DESCRIPTOR.message_types_by_name['BinaryBlob'] = _BINARYBLOB +DESCRIPTOR.message_types_by_name['ContactListEntry'] = _CONTACTLISTENTRY +DESCRIPTOR.message_types_by_name['Empty'] = _EMPTY +DESCRIPTOR.message_types_by_name['Identity'] = _IDENTITY +DESCRIPTOR.message_types_by_name['Subscription'] = _SUBSCRIPTION +DESCRIPTOR.message_types_by_name['ClientHealth'] = _CLIENTHEALTH +DESCRIPTOR.message_types_by_name['ServerHealth'] = _SERVERHEALTH +DESCRIPTOR.message_types_by_name['ROL'] = _ROL +DESCRIPTOR.enum_types_by_name['CRUD'] = _CRUD +DESCRIPTOR.enum_types_by_name['BINARY_TYPES'] = _BINARY_TYPES +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +FederatedEvent = _reflection.GeneratedProtocolMessageType('FederatedEvent', (_message.Message,), { + 'DESCRIPTOR' : _FEDERATEDEVENT, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.FederatedEvent) + }) +_sym_db.RegisterMessage(FederatedEvent) + +GeoEvent = _reflection.GeneratedProtocolMessageType('GeoEvent', (_message.Message,), { + 'DESCRIPTOR' : _GEOEVENT, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.GeoEvent) + }) +_sym_db.RegisterMessage(GeoEvent) + +BinaryBlob = _reflection.GeneratedProtocolMessageType('BinaryBlob', (_message.Message,), { + 'DESCRIPTOR' : _BINARYBLOB, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.BinaryBlob) + }) +_sym_db.RegisterMessage(BinaryBlob) + +ContactListEntry = _reflection.GeneratedProtocolMessageType('ContactListEntry', (_message.Message,), { + 'DESCRIPTOR' : _CONTACTLISTENTRY, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.ContactListEntry) + }) +_sym_db.RegisterMessage(ContactListEntry) + +Empty = _reflection.GeneratedProtocolMessageType('Empty', (_message.Message,), { + 'DESCRIPTOR' : _EMPTY, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.Empty) + }) +_sym_db.RegisterMessage(Empty) + +Identity = _reflection.GeneratedProtocolMessageType('Identity', (_message.Message,), { + 'DESCRIPTOR' : _IDENTITY, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.Identity) + }) +_sym_db.RegisterMessage(Identity) + +Subscription = _reflection.GeneratedProtocolMessageType('Subscription', (_message.Message,), { + 'DESCRIPTOR' : _SUBSCRIPTION, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.Subscription) + }) +_sym_db.RegisterMessage(Subscription) + +ClientHealth = _reflection.GeneratedProtocolMessageType('ClientHealth', (_message.Message,), { + 'DESCRIPTOR' : _CLIENTHEALTH, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.ClientHealth) + }) +_sym_db.RegisterMessage(ClientHealth) + +ServerHealth = _reflection.GeneratedProtocolMessageType('ServerHealth', (_message.Message,), { + 'DESCRIPTOR' : _SERVERHEALTH, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.ServerHealth) + }) +_sym_db.RegisterMessage(ServerHealth) + +ROL = _reflection.GeneratedProtocolMessageType('ROL', (_message.Message,), { + 'DESCRIPTOR' : _ROL, + '__module__' : 'fig_pb2' + # @@protoc_insertion_point(class_scope:com.atakmap.ROL) + }) +_sym_db.RegisterMessage(ROL) + + +DESCRIPTOR._options = None + +_FEDERATEDCHANNEL = _descriptor.ServiceDescriptor( + name='FederatedChannel', + full_name='com.atakmap.FederatedChannel', + file=DESCRIPTOR, + index=0, + serialized_options=None, + serialized_start=1458, + serialized_end=2106, + methods=[ + _descriptor.MethodDescriptor( + name='SendOneEvent', + full_name='com.atakmap.FederatedChannel.SendOneEvent', + index=0, + containing_service=None, + input_type=_FEDERATEDEVENT, + output_type=_EMPTY, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='BinaryMessageStream', + full_name='com.atakmap.FederatedChannel.BinaryMessageStream', + index=1, + containing_service=None, + input_type=_BINARYBLOB, + output_type=_EMPTY, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='SendOneBlob', + full_name='com.atakmap.FederatedChannel.SendOneBlob', + index=2, + containing_service=None, + input_type=_BINARYBLOB, + output_type=_EMPTY, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='getIdentity', + full_name='com.atakmap.FederatedChannel.getIdentity', + index=3, + containing_service=None, + input_type=_EMPTY, + output_type=_IDENTITY, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='ClientEventStream', + full_name='com.atakmap.FederatedChannel.ClientEventStream', + index=4, + containing_service=None, + input_type=_SUBSCRIPTION, + output_type=_FEDERATEDEVENT, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='ServerEventStream', + full_name='com.atakmap.FederatedChannel.ServerEventStream', + index=5, + containing_service=None, + input_type=_FEDERATEDEVENT, + output_type=_SUBSCRIPTION, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='HealthCheck', + full_name='com.atakmap.FederatedChannel.HealthCheck', + index=6, + containing_service=None, + input_type=_CLIENTHEALTH, + output_type=_SERVERHEALTH, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='ClientROLStream', + full_name='com.atakmap.FederatedChannel.ClientROLStream', + index=7, + containing_service=None, + input_type=_SUBSCRIPTION, + output_type=_ROL, + serialized_options=None, + ), + _descriptor.MethodDescriptor( + name='ServerROLStream', + full_name='com.atakmap.FederatedChannel.ServerROLStream', + index=8, + containing_service=None, + input_type=_ROL, + output_type=_SUBSCRIPTION, + serialized_options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_FEDERATEDCHANNEL) + +DESCRIPTOR.services_by_name['FederatedChannel'] = _FEDERATEDCHANNEL + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/group_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/group_pb2.py new file mode 100755 index 00000000..84beb31c --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/group_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: group.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='group.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x0bgroup.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\"#\n\x05Group\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04role\x18\x02 \x01(\tb\x06proto3' +) + + + + +_GROUP = _descriptor.Descriptor( + name='Group', + full_name='atakmap.commoncommo.protobuf.v1.Group', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='atakmap.commoncommo.protobuf.v1.Group.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='role', full_name='atakmap.commoncommo.protobuf.v1.Group.role', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=48, + serialized_end=83, +) + +DESCRIPTOR.message_types_by_name['Group'] = _GROUP +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Group = _reflection.GeneratedProtocolMessageType('Group', (_message.Message,), { + 'DESCRIPTOR' : _GROUP, + '__module__' : 'group_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.Group) + }) +_sym_db.RegisterMessage(Group) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/precisionlocation_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/precisionlocation_pb2.py new file mode 100755 index 00000000..9004e637 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/precisionlocation_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: precisionlocation.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='precisionlocation.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x17precisionlocation.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\"8\n\x11PrecisionLocation\x12\x13\n\x0bgeopointsrc\x18\x01 \x01(\t\x12\x0e\n\x06\x61ltsrc\x18\x02 \x01(\tb\x06proto3' +) + + + + +_PRECISIONLOCATION = _descriptor.Descriptor( + name='PrecisionLocation', + full_name='atakmap.commoncommo.protobuf.v1.PrecisionLocation', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='geopointsrc', full_name='atakmap.commoncommo.protobuf.v1.PrecisionLocation.geopointsrc', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='altsrc', full_name='atakmap.commoncommo.protobuf.v1.PrecisionLocation.altsrc', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=60, + serialized_end=116, +) + +DESCRIPTOR.message_types_by_name['PrecisionLocation'] = _PRECISIONLOCATION +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +PrecisionLocation = _reflection.GeneratedProtocolMessageType('PrecisionLocation', (_message.Message,), { + 'DESCRIPTOR' : _PRECISIONLOCATION, + '__module__' : 'precisionlocation_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.PrecisionLocation) + }) +_sym_db.RegisterMessage(PrecisionLocation) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/status_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/status_pb2.py new file mode 100755 index 00000000..ab72bc6c --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/status_pb2.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: status.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='status.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x0cstatus.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\"\x19\n\x06Status\x12\x0f\n\x07\x62\x61ttery\x18\x01 \x01(\rb\x06proto3' +) + + + + +_STATUS = _descriptor.Descriptor( + name='Status', + full_name='atakmap.commoncommo.protobuf.v1.Status', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='battery', full_name='atakmap.commoncommo.protobuf.v1.Status.battery', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=49, + serialized_end=74, +) + +DESCRIPTOR.message_types_by_name['Status'] = _STATUS +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Status = _reflection.GeneratedProtocolMessageType('Status', (_message.Message,), { + 'DESCRIPTOR' : _STATUS, + '__module__' : 'status_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.Status) + }) +_sym_db.RegisterMessage(Status) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takcontrol_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takcontrol_pb2.py new file mode 100755 index 00000000..d79661b6 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takcontrol_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: takcontrol.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='takcontrol.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x10takcontrol.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\">\n\nTakControl\x12\x17\n\x0fminProtoVersion\x18\x01 \x01(\r\x12\x17\n\x0fmaxProtoVersion\x18\x02 \x01(\rb\x06proto3' +) + + + + +_TAKCONTROL = _descriptor.Descriptor( + name='TakControl', + full_name='atakmap.commoncommo.protobuf.v1.TakControl', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='minProtoVersion', full_name='atakmap.commoncommo.protobuf.v1.TakControl.minProtoVersion', index=0, + number=1, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='maxProtoVersion', full_name='atakmap.commoncommo.protobuf.v1.TakControl.maxProtoVersion', index=1, + number=2, type=13, cpp_type=3, label=1, + has_default_value=False, default_value=0, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=53, + serialized_end=115, +) + +DESCRIPTOR.message_types_by_name['TakControl'] = _TAKCONTROL +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +TakControl = _reflection.GeneratedProtocolMessageType('TakControl', (_message.Message,), { + 'DESCRIPTOR' : _TAKCONTROL, + '__module__' : 'takcontrol_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.TakControl) + }) +_sym_db.RegisterMessage(TakControl) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takmessage_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takmessage_pb2.py new file mode 100755 index 00000000..2bbe4fcc --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takmessage_pb2.py @@ -0,0 +1,80 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: takmessage.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import protobuf_msg.cotevent_pb2 as cotevent__pb2 +import protobuf_msg.takcontrol_pb2 as takcontrol__pb2 + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='takmessage.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x10takmessage.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\x1a\x0e\x63otevent.proto\x1a\x10takcontrol.proto\"\x8a\x01\n\nTakMessage\x12?\n\ntakControl\x18\x01 \x01(\x0b\x32+.atakmap.commoncommo.protobuf.v1.TakControl\x12;\n\x08\x63otEvent\x18\x02 \x01(\x0b\x32).atakmap.commoncommo.protobuf.v1.CotEventb\x06proto3' + , + dependencies=[cotevent__pb2.DESCRIPTOR,takcontrol__pb2.DESCRIPTOR,]) + + + + +_TAKMESSAGE = _descriptor.Descriptor( + name='TakMessage', + full_name='atakmap.commoncommo.protobuf.v1.TakMessage', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='takControl', full_name='atakmap.commoncommo.protobuf.v1.TakMessage.takControl', index=0, + number=1, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='cotEvent', full_name='atakmap.commoncommo.protobuf.v1.TakMessage.cotEvent', index=1, + number=2, type=11, cpp_type=10, label=1, + has_default_value=False, default_value=None, + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=88, + serialized_end=226, +) + +_TAKMESSAGE.fields_by_name['takControl'].message_type = takcontrol__pb2._TAKCONTROL +_TAKMESSAGE.fields_by_name['cotEvent'].message_type = cotevent__pb2._COTEVENT +DESCRIPTOR.message_types_by_name['TakMessage'] = _TAKMESSAGE +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +TakMessage = _reflection.GeneratedProtocolMessageType('TakMessage', (_message.Message,), { + 'DESCRIPTOR' : _TAKMESSAGE, + '__module__' : 'takmessage_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.TakMessage) + }) +_sym_db.RegisterMessage(TakMessage) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takv_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takv_pb2.py new file mode 100755 index 00000000..ce5bd1ea --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/takv_pb2.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: takv.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='takv.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\ntakv.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\"E\n\x04Takv\x12\x0e\n\x06\x64\x65vice\x18\x01 \x01(\t\x12\x10\n\x08platform\x18\x02 \x01(\t\x12\n\n\x02os\x18\x03 \x01(\t\x12\x0f\n\x07version\x18\x04 \x01(\tb\x06proto3' +) + + + + +_TAKV = _descriptor.Descriptor( + name='Takv', + full_name='atakmap.commoncommo.protobuf.v1.Takv', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='device', full_name='atakmap.commoncommo.protobuf.v1.Takv.device', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='platform', full_name='atakmap.commoncommo.protobuf.v1.Takv.platform', index=1, + number=2, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='os', full_name='atakmap.commoncommo.protobuf.v1.Takv.os', index=2, + number=3, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='version', full_name='atakmap.commoncommo.protobuf.v1.Takv.version', index=3, + number=4, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=b"".decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=47, + serialized_end=116, +) + +DESCRIPTOR.message_types_by_name['Takv'] = _TAKV +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Takv = _reflection.GeneratedProtocolMessageType('Takv', (_message.Message,), { + 'DESCRIPTOR' : _TAKV, + '__module__' : 'takv_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.Takv) + }) +_sym_db.RegisterMessage(Takv) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/track_pb2.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/track_pb2.py new file mode 100755 index 00000000..5cd8c036 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/protobuf_msg/track_pb2.py @@ -0,0 +1,75 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: track.proto + +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='track.proto', + package='atakmap.commoncommo.protobuf.v1', + syntax='proto3', + serialized_options=None, + serialized_pb=b'\n\x0btrack.proto\x12\x1f\x61takmap.commoncommo.protobuf.v1\"&\n\x05Track\x12\r\n\x05speed\x18\x01 \x01(\x01\x12\x0e\n\x06\x63ourse\x18\x02 \x01(\x01\x62\x06proto3' +) + + + + +_TRACK = _descriptor.Descriptor( + name='Track', + full_name='atakmap.commoncommo.protobuf.v1.Track', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='speed', full_name='atakmap.commoncommo.protobuf.v1.Track.speed', index=0, + number=1, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + _descriptor.FieldDescriptor( + name='course', full_name='atakmap.commoncommo.protobuf.v1.Track.course', index=1, + number=2, type=1, cpp_type=5, label=1, + has_default_value=False, default_value=float(0), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + serialized_options=None, file=DESCRIPTOR), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + serialized_options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=48, + serialized_end=86, +) + +DESCRIPTOR.message_types_by_name['Track'] = _TRACK +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +Track = _reflection.GeneratedProtocolMessageType('Track', (_message.Message,), { + 'DESCRIPTOR' : _TRACK, + '__module__' : 'track_pb2' + # @@protoc_insertion_point(class_scope:atakmap.commoncommo.protobuf.v1.Track) + }) +_sym_db.RegisterMessage(Track) + + +# @@protoc_insertion_point(module_scope) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTak.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTak.py new file mode 100755 index 00000000..08a7ab37 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTak.py @@ -0,0 +1,277 @@ +""" +SSL/TLS negotiation. +""" + +import asyncore +import multiprocessing +import random +import socket +import ssl + + +from collections import defaultdict + +import time + +from create_cot import CotMessage +from mission_api import MissionApiPyTAKHelper +from utils import p12_to_pem, is_sa_message, mission_change + + +class TLSHandshake(asyncore.dispatcher): + """ + Negotiates a SSL/TLS connection before handing itself spawning a + dispatcher that can deal with the overlying protocol as soon as the + handshake has been completed. + `address` is a tuple consisting of hostname/address and port to connect to + if nothing is passed in `sock`, which can take an already-connected socket. + `certfile` can take a path to a certificate bundle, and `server_side` + indicates whether the socket is intended to be a server-side or client-side + socket. + """ + + want_read = want_write = True + + def __init__(self, + address=None, + sock=None, + certfile=None, + password=None, + uid=None, + self_sa_delta=5.0, + mission_config=None, + data_dict=None): + + + asyncore.dispatcher.__init__(self, sock) + self.certfile = certfile + self.password = password + self.address = address + + self.uid = uid or CotMessage().uid + self.self_sa_delta = self_sa_delta + + self.self_sa_delta = 1.0 # Set self_sa_delta to 1.0 for self status updates every second + + self.last_self_sa = time.time() - self.self_sa_delta + self.write_self_sa = lambda: (time.time() - self.last_self_sa > self.self_sa_delta) + + if mission_config is not None: + self.mission_config = mission_config + self.last_mission_delta = self.mission_config['mission_write_interval'] + self.last_mission_write = time.time() + self.last_mission_delta + else: + self.mission_config = dict() + + if self.mission_config.get('send_mission_cot', False): + self.write_mission_cot = lambda: (time.time() - self.last_mission_write > self.last_mission_delta) + else: + self.write_mission_cot = lambda: False + + self.really_close = False + + mission_api_host = self.mission_config.get('address', (self.address[0], 8443))[0] + mission_api_port = self.mission_config.get('address', (self.address[0], 8443))[1] + self.data_sync_sess = MissionApiPyTAKHelper(mission_api_host, mission_api_port, + certfile=self.certfile, + password=self.password, + uid=self.uid, + mission_config=mission_config) + + self.reconnect(sock=sock) + self.handshake_happened = False + + self.location = (random.uniform(-90, 90), random.uniform(-180, 180)) + self.mission_locations = dict() + self.mission_cot_count = defaultdict(int) + + self.data_dict = data_dict if data_dict is not None else {} + self.data_dict[self.uid] = {'write': 0, 'read': 0, 'connected': False} + + def reconnect(self, sock=None): + self.handshake_happened = False + self.want_read = self.want_write = True + if sock is None: + self.create_socket(socket.AF_INET, socket.SOCK_STREAM) + print(('Connecting to {}:{} {}'.format(self.address[0], self.address[1], self.uid))) + tries = 0 + self.connected = False + while not self.connected: + tries += 1 + print("attempt number {} for uid:{}".format(tries, self.uid)) + try: + self.connect(self.address) + except socket.error: + pass + time.sleep(2) + print("finally connected?", self.uid) + + elif self.connected: + # Initiate the handshake for an already-connected socket. + self.handle_connect() + + def handle_connect(self): + # Once the connection has been established, it's safe to wrap the + # socket. + with p12_to_pem(self.certfile, self.password) as certfile: + self.socket = ssl.wrap_socket(self.socket, + certfile=certfile, + ca_certs=certfile, + cert_reqs=ssl.CERT_REQUIRED, + do_handshake_on_connect=False) + + def writable(self): + if self.want_write: + return self.write_self_sa() or self.data_sync_sess.ready_to_request() or self.write_mission_cot() + return False + + def readable(self): + return self.want_read + + def _handshake(self): + """ + Perform the handshake. + """ + try: + self.socket.do_handshake() + print("handshake success", self.want_read, self.want_write, self.uid) + except ssl.SSLError as err: + self.want_read = self.want_write = False + if err.args[0] == ssl.SSL_ERROR_WANT_READ: + self.want_read = True + elif err.args[0] == ssl.SSL_ERROR_WANT_WRITE: + self.want_write = True + else: + print(err) + raise + else: + self.handshake_happened = True + self.want_read = self.want_write = True + connection_data = self.data_dict[self.uid] + connection_data['connected'] = True + self.data_dict[self.uid] = connection_data + + def handle_read(self): + if not self.handshake_happened: + try: + self._handshake() + except Exception as e: + print("haha", self.uid) + print(e) + self.really_close = True + self.handle_close() + + try: + data = self.recv(81920) + except Exception: + pass + else: + if is_sa_message(data): + return + if not self.mission_config.get("react_to_change_message", False): + return + action, mission_name = mission_change(data) + if action: + self.data_sync_sess.request_queue.append((action, mission_name)) + + def handle_write(self): + if not self.handshake_happened: + self._handshake() + + if self.write_self_sa(): + self_sa = CotMessage(uid=self.uid, lat=str(self.location[0]), lon=str(self.location[1])) + self_sa.add_callsign_detail() + try: + self.send(self_sa.to_string()) + print(f"Self SA sent for UID: {self.uid}, Content: {self_sa.to_string()}") + except Exception as e: + print("write error", self.uid) + print(e) + else: + self.last_self_sa = time.time() + + if self.write_mission_cot(): + for mission in self.data_sync_sess.missions_subscribed: + mission_loc = self.mission_locations.get(mission) + if mission_loc is None: + mission_loc = (str(random.uniform(-90, 90)), str(random.uniform(-180, 180))) + self.mission_locations[mission] = mission_loc + else: + lat = float(mission_loc[0]) + random.choice([-1, 0, 1]) + if lat < -90: + lat = -90 + elif lat > 90: + lat = 90 + lon = float(mission_loc[1]) + random.choice([-1, 0, 1]) + if lon < -180: + lon = 180 + elif lon > 180: + lon = -180 + mission_loc = (str(lat), str(lon)) + self.mission_locations[mission] = mission_loc + + send_only_new_tracks = self.mission_config.get("send_only_new_tracks", False) + mission_cot_uid = self.uid + "_mission_" + mission + if send_only_new_tracks: + mission_cot_uid += "_" + str(self.mission_cot_count[mission]) + mission_cot = CotMessage(uid=mission_cot_uid, lat=mission_loc[0], + lon=mission_loc[1]) + if self.mission_cot_count[mission] == 0 or send_only_new_tracks: + dest_attr = {"mission": mission} + mission_cot.add_sub_detail("marti", "dest", dest_attr) + self.mission_cot_count[mission] += 1 + self.send(mission_cot.to_string()) + self.last_mission_write = time.time() + + if self.data_sync_sess.ready_to_request(): + self.data_sync_sess.make_requests() + + def handle_close(self): + print("----------------------------------------------------------------") + print("------------------ {} trying to close --------------------".format(self.uid)) + print("----------------------------------------------------------------") + self.close() + connection_data = self.data_dict[self.uid] + connection_data['connected'] = False + if not self.really_close: + print("trying again", self.uid) + time.sleep(1) + self.reconnect() + else: + print(self.uid, "not really closing", self.uid) + self.really_close = False + time.sleep(10) + print(self.uid, "ok, now lets try again") + self.reconnect() + + +class TLSClientProcess(multiprocessing.Process): + + async_tls = None + def __init__(self, address=None, uid=None, cert=None, password=None, self_sa_delta=5.0, mission_config=None): + + multiprocessing.Process.__init__(self) + self.address = address + self.uid = uid + self.cert = cert + self.password = password + self.self_sa_delta = self_sa_delta + self.mission_config = mission_config + + def run(self): + try: + print("starting", self.name, "id:", self.uid) + + self.async_tls = TLSHandshake(address=self.address, + certfile=self.cert, + password=self.password, + uid=self.uid, + self_sa_delta=self.self_sa_delta, + mission_config=self.mission_config) + + asyncore.loop(0) + + except KeyboardInterrupt: + # if we close all connections, then the asycnore loop for this process will end + print("trying to close") + asyncore.close_all() diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakStreamingCot.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakStreamingCot.py new file mode 100755 index 00000000..f14ff92c --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakStreamingCot.py @@ -0,0 +1,422 @@ +import asyncio +import logging +import multiprocessing +import random +import ssl +from collections import defaultdict + +from multiprocessing import Process, Pool +import threading +import requests +from concurrent.futures import ThreadPoolExecutor +from functools import partial + +import zmq +import zmq.asyncio as azmq + +import sys +import time + +from create_cot import CotMessage +from mission_api import MissionApiPyTAKHelper +from utils import p12_to_pem + +import stats + +def generate_random_latitude(initial_latitude): + # Generate a random latitude within 10 degrees of the initial value + return round(random.uniform(initial_latitude - 10, initial_latitude + 10), 6) + +def generate_random_longitude(initial_longitude): + # Generate a random longitude within 10 degrees of the initial value + return round(random.uniform(initial_longitude - 10, initial_longitude + 10), 6) + +class PyTAKStreamingCot: + + def __init__(self, + address=None, + certfile=None, + password=None, + uid=None, + self_sa_delta=1.0, + mission_config=None, + data_dict=None, + ping=False, + ping_interval=1000, # in ms + arr=None, + debug=False): + + self.certfile = certfile + self.password = password + self.address = address + + self.uid = uid or CotMessage().uid + self.self_sa_delta = self_sa_delta + self.last_sa_write = time.time() + self.self_sa_delta + + self.ping = ping + self.ping_interval = ping_interval # in ms + self.last_ping_write = time.time()*1000 + self.ping_interval # in ms + self.arr = arr + + if mission_config is not None: + self.mission_config = mission_config + self.mission_cot_count = defaultdict(int) + self.mission_locations = dict() + self.mission_cot_delta = self.mission_config.get("mission_write_interval") + self.mission_cot_prob = self.mission_config.get("send_mission_cot_probability") + self.last_mission_write = time.time() + self.next_mission_write = self.last_mission_write + random.uniform(0, self.mission_cot_delta) + self.did_mission_write = False + else: + self.mission_config = dict() + self.mission_config['address'] = self.address + + mission_api_host = self.mission_config.get('address')[0] + mission_api_port = self.mission_config.get('address')[1] + self.data_sync_sess = MissionApiPyTAKHelper(mission_api_host, mission_api_port, + certfile=self.certfile, + password=self.password, + uid=self.uid, + mission_config=mission_config) + + self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) + + with p12_to_pem(self.certfile, self.password) as cert: + self.ssl_context.load_cert_chain(cert) + + self.location = (random.uniform(-90, 90), random.uniform(-180, 180)) + self.data_dict = data_dict if data_dict is not None else dict() + self.data_dict[self.uid] = {'write': 0, 'read': 0, 'bytes': 0, 'connected': False} + + self.logger = logging.getLogger(self.uid) + if debug: + self.logger.setLevel(logging.DEBUG) + + if sys.version_info >= (3, 8): + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s:\t %(message)s') + handler = logging.StreamHandler() + handler.setFormatter(formatter) + handler.setStream(sys.stdout) + handler.setLevel(logging.INFO) + self.logger.addHandler(handler) + + self.negotiated = False + + self.pool = ThreadPoolExecutor(max_workers=2) + + self.read_thread: Process = None + self.read_socket: zmq.Socket = None + self.read_pool: Pool = None + + + async def read_handler(self, reader): + while True: + data = await reader.readuntil(b'
    ') + + # try: + # data = await asyncio.wait_for(reader.readuntil(b''), timeout=1.0) + # except asyncio.TimeoutError: + # print('read_handler timeout!') + + if self.arr is not None: + self.arr[stats.MESSAGES_RECEIVED_INDEX] += 1 + # deal with pong messages here, not send it to read_socket + cot_message = CotMessage(msg=data) + + #self.logger.info("Client receives an event message: " + str(data)) + + if cot_message.is_pong(): + # print("~~~Streaming cot: Received a pong message") + self.arr[stats.MESSAGES_PONG_COUNT_INDEX] += 1 + self.arr[stats.TIME_BETWEEN_PING_PONG] += round(time.time()*1000 - self.last_ping_write) # in ms + + elif self.mission_config.get("react_to_change_message", False): + await self.read_socket.send(data) + + connection_data = self.data_dict[self.uid] + connection_data['read'] += 1 + #connection_data['bytes'] += len(data) + self.data_dict[self.uid] = connection_data + + async def send_self_sa(self, writer): + self_sa_message = CotMessage(uid=self.uid, lat=str(generate_random_latitude(0)), lon=str(generate_random_longitude(0))) + self_sa_message.add_callsign_detail(group_name="Red", platform="PyTAKStreamingCot") + #self.logger.info("Client sends self_sa message: " + str(self_sa_message.to_string())) + writer.write(self_sa_message.to_string()) + await writer.drain() + #writer.write(str.encode("<__chat senderCallsign=\"BRONCO947\" chatroom=\"anonymous\" id=\"fa00f719-4167-e378-3f04-4f963bedbde8\">asdf")) + #await writer.drain() + self.last_sa_write = time.time() + + async def send_mission_cot(self, writer): + self.logger.debug("Client sending mission_cot") + for mission in self.data_sync_sess.missions_subscribed: + if self.mission_cot_prob < random.uniform(0, 1): + continue + mission_loc = self.mission_locations.get(mission) + if mission_loc is None: + mission_loc = (str(random.uniform(-90, 90)), str(random.uniform(-180, 180))) + self.mission_locations[mission] = mission_loc + else: + lat = float(mission_loc[0]) + random.choice([-1, 0, 1]) + if lat < -90: + lat = -90 + elif lat > 90: + lat = 90 + lon = float(mission_loc[1]) + random.choice([-1, 0, 1]) + if lon < -180: + lon = 180 + elif lon > 180: + lon = -180 + mission_loc = (str(lat), str(lon)) + self.mission_locations[mission] = mission_loc + + send_only_new_tracks = self.mission_config.get("send_only_new_tracks", False) + mission_cot_uid = self.uid + "_mission_" + mission + if send_only_new_tracks: + mission_cot_uid += "_" + str(self.mission_cot_count[mission]) + mission_cot = CotMessage(uid=mission_cot_uid, lat=mission_loc[0], lon=mission_loc[1]) + if self.mission_cot_count[mission] == 0 or send_only_new_tracks: + dest_attr = {"mission": mission} + mission_cot.add_sub_detail("marti", "dest", dest_attr) + writer.write(mission_cot.to_string()) + await writer.drain() + self.mission_cot_count[mission] += 1 + self.did_mission_write = True + + async def send_ping(self, writer): + ping_message = CotMessage(uid=self.uid, lat=str(self.location[0]), lon=str(self.location[1]), type="t-x-c-t") + writer.write(ping_message.to_string()) + #print(f"sending Ping: {ping_message.to_string()}") + await writer.drain() + self.last_ping_write = time.time()*1000 # in ms + + async def time_to_write(self): + while True: + now = time.time() # in seconds + if now - self.last_sa_write > self.self_sa_delta: + return "self_sa" + if self.mission_config.get("send_mission_cot", False): + if (not self.did_mission_write) and (now > self.next_mission_write): + return "mission_cot" + if now - self.last_mission_write > self.mission_cot_delta: + self.last_mission_write = now + self.next_mission_write = self.last_mission_write + random.uniform(0, self.mission_cot_delta) + self.did_mission_write = False + # if self.data_sync_sess.ready_to_request(): + # return "data_sync" + if (now * 1000 - self.last_ping_write) > self.ping_interval: # in ms + return "ping" + await asyncio.sleep(0.1) + + def data_sync_write_target(self, event): + while True: + if event.is_set(): + return + if self.data_sync_sess.ready_to_request(): + try: + self.data_sync_sess.make_requests() + except requests.Timeout as e: + self.logger.error(str(type(e)) + ": " + str(e)) + raise e + time.sleep(0.1) + + async def write_handler(self, writer): + try: + data_sync_event = threading.Event() + data_sync_thread = threading.Thread(target=self.data_sync_write_target, args=(data_sync_event,)) + data_sync_thread.start() + self.last_sa_write = time.time() - self.self_sa_delta + self.last_ping_write = time.time()*1000 - self.ping_interval # in ms + while True: + write_action = await self.time_to_write() + + if write_action == "ping": + await self.send_ping(writer) + # print("~~~Streaming cot: Sent a ping message") + if self.arr is not None: + self.arr[stats.MESSAGES_PING_COUNT_INDEX] += 1 + self.arr[stats.MESSAGES_SENT_INDEX] += 1 + + elif write_action == "self_sa": + await self.send_self_sa(writer) + # print("~~~Streaming cot: Sent send_self_sa") + if self.arr is not None: + self.arr[stats.MESSAGES_SENT_INDEX] += 1 + + elif write_action == "mission_cot": + await self.send_mission_cot(writer) + # print("~~~Streaming cot: Sent send_mission_cot") + if self.arr is not None: + self.arr[stats.MESSAGES_SENT_INDEX] += 1 + + # elif write_action == "data_sync": + # await asyncio.get_event_loop().run_in_executor(self.pool, self.data_sync_sess.make_requests) + + await asyncio.get_event_loop().run_in_executor(self.pool, partial(data_sync_thread.join, timeout=0.1)) + if not data_sync_thread.is_alive(): + raise RuntimeError("There was a mission api exception") + await asyncio.sleep(0.01) + connection_data = self.data_dict[self.uid] + connection_data['write'] += 1 + self.data_dict[self.uid] = connection_data + except asyncio.CancelledError: + data_sync_event.set() + data_sync_thread.join() + raise + + async def connect_socket(self): + times_connected = 0 + + while True: + try: + reader, writer = await asyncio.open_connection(self.address[0], self.address[1], ssl=self.ssl_context) + + if self.arr is not None: + self.arr[stats.CONNECT_EVENT_COUNT_INDEX] += 1 + + times_connected += 1 + connection_data = self.data_dict[self.uid] + connection_data['connected'] = True + self.data_dict[self.uid] = connection_data + + # if we dont react to change messages, then we dont need to parse any data + if self.mission_config.get("react_to_change_message", False): + context = azmq.Context() + self.read_socket = context.socket(zmq.PUSH) + port = self.read_socket.bind_to_random_port('tcp://*') + data_sync_port = self.data_sync_sess.port + POOL_SIZE = 3 + self.read_pool = Pool(POOL_SIZE) + for i in range(POOL_SIZE): + self.read_pool.apply_async(read_thread_zmq, args=(port, data_sync_port)) + await asyncio.sleep(1) + + self.data_sync_sess.get_client_endpoints() + read_task = asyncio.ensure_future(self.read_handler(reader)) + write_task = asyncio.ensure_future(self.write_handler(writer)) + + done, pending = await asyncio.wait([read_task, write_task], return_when=asyncio.FIRST_COMPLETED) + + for task in pending: + self.logger.error("read/write task in pending: {}".format(task)) + task.cancel() + for task in done: + self.logger.error("read/write task done: {}".format(task)) + task.exception() + + except Exception as e: + + self.logger.error("Exception occurs " + str(type(e)) + ": " + str(e)) + + finally: + if self.mission_config.get("react_to_change_message", False): + self.read_socket.close() + self.read_pool.terminate() + self.read_pool.join() + + if writer is not None: + writer.close() + writer.transport.abort() + + if self.arr is not None: + self.arr[stats.DISCONNECT_EVENT_COUNT_INDEX] += 1 + + connection_data = self.data_dict[self.uid] + connection_data['connected'] = False + self.data_dict[self.uid] = connection_data + self.logger.error("retry # %d" % times_connected) + await asyncio.sleep(1) + + +def read_thread_zmq(port: int, data_sync_port): + context = zmq.Context() + socket = context.socket(zmq.PULL) + data_sync_sock = context.socket(zmq.PUSH) + addr = 'tcp://localhost:' + str(port) + data_sync_addr = 'tcp://localhost:' + str(data_sync_port) + socket.connect(addr) + data_sync_sock.connect(data_sync_addr) + + try: + while True: + data = socket.recv() + cot_message = CotMessage(msg=data) + + if cot_message.is_sa(): + continue + + change_type, change_data = cot_message.mission_change() + if change_type: + send_data = {'req_type': change_type, 'req_data': change_data} + data_sync_sock.send_json(send_data, flags=zmq.NOBLOCK) + + fileshare_data = cot_message.fileshare() + if fileshare_data: + send_data = {'req_type': 'download_file', 'req_data': fileshare_data} + data_sync_sock.send_json(send_data, flags=zmq.NOBLOCK) + + except Exception as e: + print("read_thread_zmq error: " + str(type(e)) + " -> " + str(e.args)) + raise e + + +class PyTAKStreamingCotProcess(multiprocessing.Process): + def __init__(self, address=None, uid=None, cert=None, password=None, self_sa_delta=5.0, + mission_config=None, data_dict=None, debug=None, ping=False, ping_interval=1000, arr=None): + multiprocessing.Process.__init__(self) + self.address = address + self.uid = uid + self.cert = cert + self.password = password + self.self_sa_delta = self_sa_delta + self.mission_config = mission_config + self.data_dict = data_dict + self.debug = debug + self.agent = None + self.ping = ping + self.ping_interval = ping_interval + self.arr = arr # multiprocessing.Array to store metric data for this client + + def run(self): + try: + self.agent = PyTAKStreamingCot(address=self.address, + uid=self.uid, + certfile=self.cert, + password=self.password, + self_sa_delta=self.self_sa_delta, + mission_config=self.mission_config, + data_dict=self.data_dict, + debug=self.debug, + ping=self.ping, + ping_interval=self.ping_interval, + arr = self.arr + ) + if self.uid is None: + self.uid = self.agent.uid + + asyncio.get_event_loop().run_until_complete(self.agent.connect_socket()) + + except KeyboardInterrupt: + pass + finally: + if self.agent.read_pool is not None: + self.agent.read_pool.terminate() + self.agent.read_pool.join() + + return + + +if __name__ == "__main__": + logging.basicConfig(level=logging.ERROR, + format='%(asctime)s - %(name)s - %(levelname)s:\t %(message)s', + stream=sys.stdout) + log = logging.getLogger('main') + log.setLevel(logging.INFO) + socket = PyTAKStreamingCot(address=("3.82.2.223", 8089), + certfile="certs/takserver.p12", password="atakatak", + debug=True) + + asyncio.get_event_loop().run_until_complete(socket.connect_socket()) + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakStreamingProto.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakStreamingProto.py new file mode 100755 index 00000000..7740ca75 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakStreamingProto.py @@ -0,0 +1,468 @@ +import asyncio +import logging +import multiprocessing +import random +import ssl +from collections import defaultdict + +from multiprocessing import Process, Pool +import threading +import requests +from concurrent.futures import ThreadPoolExecutor +from functools import partial + +import zmq +import zmq.asyncio as azmq + +import sys +import time + +from create_cot import CotMessage +from create_proto import CotProtoMessage, get_msg_size +from mission_api import MissionApiPyTAKHelper +from utils import p12_to_pem + +import stats + +class PyTAKStreamingProto: + + def __init__(self, + address=None, + certfile=None, + password=None, + uid=None, + self_sa_delta=1.0, + mission_config=None, + data_dict=None, + ping=False, + ping_interval=1000, # in ms + arr=None, + debug=False): + + self.certfile = certfile + self.password = password + self.address = address + + self.uid = uid or CotProtoMessage().uid + self.self_sa_delta = self_sa_delta + self.last_sa_write = time.time() + self.self_sa_delta + + self.ping = ping + self.ping_interval = ping_interval # in ms + self.last_ping_write = time.time()*1000 + self.ping_interval # in ms + self.arr = arr + + if mission_config is not None: + self.mission_config = mission_config + self.mission_cot_count = defaultdict(int) + self.mission_locations = dict() + self.mission_cot_delta = self.mission_config.get("mission_write_interval") + self.mission_cot_prob = self.mission_config.get("send_mission_cot_probability") + self.last_mission_write = time.time() + self.next_mission_write = self.last_mission_write + random.uniform(0, self.mission_cot_delta) + self.did_mission_write = False + else: + self.mission_config = dict() + self.mission_config['address'] = self.address + + mission_api_host = self.mission_config.get('address')[0] + mission_api_port = self.mission_config.get('address')[1] + self.data_sync_sess = MissionApiPyTAKHelper(mission_api_host, mission_api_port, + certfile=self.certfile, + password=self.password, + uid=self.uid, + mission_config=mission_config) + + self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLS) + + with p12_to_pem(self.certfile, self.password) as cert: + self.ssl_context.load_cert_chain(cert) + + self.location = (random.uniform(-90, 90), random.uniform(-180, 180)) + self.data_dict = data_dict if data_dict is not None else dict() + self.data_dict[self.uid] = {'write': 0, 'read': 0, 'bytes': 0, 'connected': False} + + self.logger = logging.getLogger(self.uid) + if debug: + self.logger.setLevel(logging.DEBUG) + + if sys.version_info >= (3, 8): + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s:\t %(message)s') + handler = logging.StreamHandler() + handler.setFormatter(formatter) + handler.setStream(sys.stdout) + handler.setLevel(logging.INFO) + self.logger.addHandler(handler) + + self.negotiated = False + self.buffer = None + + self.pool = ThreadPoolExecutor(max_workers=2) + + self.read_socket: azmq.Socket = None + self.read_pool: Pool = None + + async def negotiate_protocol(self, reader, writer): + self.negotiated = False + self.logger.debug("negotiating protocol") + sent_request = False + while not self.negotiated: + try: + data = await reader.readuntil(b'') + except asyncio.LimitOverrunError as e: + if sent_request: + data = await reader.read(reader._limit) + if data[0].to_bytes(1, byteorder='big') == b'\xbf': + self.buffer = data + self.negotiated = True + return + raise e + + self.logger.debug(data) + + msg = CotMessage(msg=data) + if msg.server_protocol_version_support() == '1': + response = CotMessage(uid=msg.uid) + response.protocol_response_message(version='1') + writer.write(response.to_string()) + await writer.drain() + sent_request = True + + elif msg.server_protocol_negotiation_handshake(): + self.negotiated = True + + self.logger.debug("done negotiating") + + async def read_handler(self, reader): + leftovers = None + msg_size = 0 + if self.buffer is not None: + data = self.buffer + else: + data = await reader.read(reader._limit) + while True: + + if leftovers is None: + fullbuffer = data + else: + fullbuffer = leftovers + data + leftovers = None + while len(fullbuffer) > 0: + if msg_size == 0: # get the message size + first_byte = fullbuffer[0].to_bytes(1, byteorder="big") + if first_byte != b'\xbf': + raise Exception("Magic byte is wrong: " + str(first_byte)) + msg_size = get_msg_size(fullbuffer[1:]) + if not msg_size: + leftovers = fullbuffer + break + if len(fullbuffer) < msg_size: + leftovers = fullbuffer + break + + next_msg = fullbuffer[:msg_size] + fullbuffer = fullbuffer[msg_size:] + + if self.arr is not None: + self.arr[stats.MESSAGES_RECEIVED_INDEX] += 1 + # deal with pong messages here, not send it to read_socket + proto_message = CotProtoMessage(msg=next_msg) + if proto_message.is_pong(): + # print("~~~Streaming proto: Received a pong message") + self.arr[stats.MESSAGES_PONG_COUNT_INDEX] += 1 + self.arr[stats.TIME_BETWEEN_PING_PONG] += round(time.time()*1000 - self.last_ping_write) # in ms + else: + if self.mission_config.get("react_to_change_message", False): + await self.read_socket.send(next_msg) + + connection_data = self.data_dict[self.uid] + connection_data['read'] += 1 + #connection_data['bytes'] += len(data) + self.data_dict[self.uid] = connection_data + + leftovers = None + msg_size = 0 + + data = await reader.read(reader._limit) + if data == b'': + await asyncio.sleep(0.0001) + + + async def send_self_sa(self, writer): + self.location = (random.uniform(-90, 90), random.uniform(-180, 180)) + self_sa_message = CotProtoMessage(uid=self.uid, lat=str(self.location[0]), lon=str(self.location[1])) + self_sa_message.add_callsign_detail(group_name="Red", platform="PyTAKStreamingProto") + writer.write(self_sa_message.serialize()) + await writer.drain() + self.last_sa_write = time.time() + + async def send_mission_cot(self, writer): + for mission in self.data_sync_sess.missions_subscribed: + if self.mission_cot_prob < random.uniform(0, 1): + continue + mission_loc = self.mission_locations.get(mission) + if mission_loc is None: + mission_loc = (str(random.uniform(-90, 90)), str(random.uniform(-180, 180))) + self.mission_locations[mission] = mission_loc + else: + lat = float(mission_loc[0]) + random.choice([-1, 0, 1]) + if lat < -90: + lat = -90 + elif lat > 90: + lat = 90 + lon = float(mission_loc[1]) + random.choice([-1, 0, 1]) + if lon < -180: + lon = 180 + elif lon > 180: + lon = -180 + mission_loc = (str(lat), str(lon)) + self.mission_locations[mission] = mission_loc + + send_only_new_tracks = self.mission_config.get("send_only_new_tracks", False) + mission_cot_uid = self.uid + "_mission_" + mission + if send_only_new_tracks: + mission_cot_uid += "_" + str(self.mission_cot_count[mission]) + mission_cot_proto = CotProtoMessage(uid=mission_cot_uid, lat=mission_loc[0], + lon=mission_loc[1]) + if self.mission_cot_count[mission] == 0 or send_only_new_tracks: + dest_attr = {"mission": mission} + mission_cot_proto.add_sub_detail("marti", "dest", dest_attr) + writer.write(mission_cot_proto.serialize()) + await writer.drain() + self.mission_cot_count[mission] += 1 + self.did_mission_write = True + + async def send_ping(self, writer): + ping_message = CotProtoMessage(uid=self.uid, lat=str(self.location[0]), lon=str(self.location[1]), type="t-x-c-t") + writer.write(ping_message.serialize()) + # print(f"sending Ping: {ping_message.serialize()}") + await writer.drain() + self.last_ping_write = time.time()*1000 # in ms + + async def time_to_write(self): + while True: + now = time.time() + if now - self.last_sa_write > self.self_sa_delta: + return "self_sa" + if self.mission_config.get("send_mission_cot", False): + if (not self.did_mission_write) and (now > self.next_mission_write): + return "mission_cot" + if now - self.last_mission_write > self.mission_cot_delta: + self.last_mission_write = now + self.next_mission_write = self.last_mission_write + random.uniform(0, self.mission_cot_delta) + self.did_mission_write = False + # if self.data_sync_sess.ready_to_request(): + # return "data_sync" + if (now * 1000 - self.last_ping_write) > self.ping_interval: # in ms + return "ping" + await asyncio.sleep(0.1) + + def data_sync_write_target(self, event): + while True: + if event.is_set(): + return + if self.data_sync_sess.ready_to_request(): + try: + self.data_sync_sess.make_requests() + except requests.Timeout as e: + self.logger.error(str(type(e)) + ": " + str(e)) + raise e + time.sleep(0.1) + + async def write_handler(self, writer): + try: + data_sync_event = threading.Event() + data_sync_thread = threading.Thread(target=self.data_sync_write_target, args=(data_sync_event,)) + data_sync_thread.start() + self.last_sa_write = time.time() - self.self_sa_delta + self.last_ping_write = time.time()*1000 - self.ping_interval + while True: + write_action = await self.time_to_write() + + if write_action == "ping": + await self.send_ping(writer) + # print("~~~Streaming proto: Sent a ping message") + if self.arr is not None: + self.arr[stats.MESSAGES_PING_COUNT_INDEX] += 1 + self.arr[stats.MESSAGES_SENT_INDEX] += 1 + + elif write_action == "self_sa": + await self.send_self_sa(writer) + # print("~~~Streaming proto: Sent send_self_sa") + if self.arr is not None: + self.arr[stats.MESSAGES_SENT_INDEX] += 1 + + elif write_action == "mission_cot": + await self.send_mission_cot(writer) + # print("~~~Streaming proto: Sent send_mission_cot") + if self.arr is not None: + self.arr[stats.MESSAGES_SENT_INDEX] += 1 + + # elif write_action == "data_sync": + # await asyncio.get_event_loop().run_in_executor(self.pool, self.data_sync_sess.make_requests) + + await asyncio.get_event_loop().run_in_executor(self.pool, partial(data_sync_thread.join, timeout=0.1)) + if not data_sync_thread.is_alive(): + raise RuntimeError("There was a mission api exception") + await asyncio.sleep(0.01) + connection_data = self.data_dict[self.uid] + connection_data['write'] += 1 + self.data_dict[self.uid] = connection_data + except asyncio.CancelledError: + data_sync_event.set() + data_sync_thread.join() + raise + + + async def connect_socket(self): + times_connected = 0 + while True: + try: + self.logger.debug('trying') + reader, writer = await asyncio.open_connection(self.address[0], self.address[1], ssl=self.ssl_context) + if self.arr is not None: + self.arr[stats.CONNECT_EVENT_COUNT_INDEX] += 1 + + times_connected += 1 + await self.negotiate_protocol(reader, writer) + connection_data = self.data_dict[self.uid] + connection_data['connected'] = True + self.data_dict[self.uid] = connection_data + + # if we dont react to change messages, then we dont need to parse any data + if self.mission_config.get("react_to_change_message", False): + context = azmq.Context() + self.read_socket = context.socket(zmq.PUSH) + port = self.read_socket.bind_to_random_port('tcp://*') + data_sync_port = self.data_sync_sess.port + POOL_SIZE = 3 + self.read_pool = Pool(POOL_SIZE) + + for i in range(POOL_SIZE): + self.read_pool.apply_async(read_thread_zmq, args=(port, data_sync_port)) + await asyncio.sleep(1) + + # get list of recent client connect / disconnect events + self.data_sync_sess.get_client_endpoints() + + read_task = asyncio.ensure_future(self.read_handler(reader)) + write_task = asyncio.ensure_future(self.write_handler(writer)) + + done, pending = await asyncio.wait([read_task, write_task], return_when=asyncio.FIRST_COMPLETED) + for task in pending: + self.logger.error("read/write task in pending: {}".format(task)) + task.cancel() + for task in done: + self.logger.error("read/write task done: {}".format(task)) + task.exception() + except Exception as e: + self.logger.error(str(type(e)) + ": " + str(e)) + + + finally: + self.logger.error("closing connection in order to restart") + if self.mission_config.get("react_to_change_message", False): + self.read_socket.close() + self.read_pool.terminate() + self.read_pool.join() + + if writer is not None: + writer.close() + writer.transport.abort() + + if self.arr is not None: + self.arr[stats.DISCONNECT_EVENT_COUNT_INDEX] += 1 + + connection_data = self.data_dict[self.uid] + connection_data['connected'] = False + self.data_dict[self.uid] = connection_data + self.logger.error("retry # %d" % times_connected) + await asyncio.sleep(1) + +def read_thread_zmq(port: int, data_sync_port): + context = zmq.Context() + socket = context.socket(zmq.PULL) + data_sync_sock = context.socket(zmq.PUSH) + addr = 'tcp://localhost:' + str(port) + data_sync_addr = 'tcp://localhost:' + str(data_sync_port) + socket.connect(addr) + data_sync_sock.connect(data_sync_addr) + + try: + while True: + data = socket.recv() + proto_message = CotProtoMessage(msg=data) + + if proto_message.is_sa(): + continue + + change_type, change_data = proto_message.mission_change() + if change_type: + send_data = {'req_type': change_type, 'req_data': change_data} + data_sync_sock.send_json(send_data, flags=zmq.NOBLOCK) + + fileshare_data = proto_message.fileshare() + if fileshare_data: + send_data = {'req_type': 'download_file', 'req_data': fileshare_data} + data_sync_sock.send_json(send_data, flags=zmq.NOBLOCK) + + except Exception as e: + print("read_thread error: " + str(type(e)) + " -> " + str(e.args)) + raise e + + +class PyTAKStreamingProtoProcess(multiprocessing.Process): + def __init__(self, address=None, uid=None, cert=None, password=None, self_sa_delta=5.0, + mission_config=None, data_dict=None, + ping=False, ping_interval=1000, arr=None): + multiprocessing.Process.__init__(self) + self.address = address + self.uid = uid + self.cert = cert + self.password = password + self.self_sa_delta = self_sa_delta + self.mission_config = mission_config + self.data_dict = data_dict + self.agent = None + self.ping = ping + self.ping_interval = ping_interval + self.arr = arr # multiprocessing.Array to store metric data for this client + + def run(self): + try: + self.agent = PyTAKStreamingProto(address=self.address, + uid=self.uid, + certfile=self.cert, + password=self.password, + self_sa_delta=self.self_sa_delta, + mission_config=self.mission_config, + data_dict=self.data_dict, + ping=self.ping, + ping_interval=self.ping_interval, + arr = self.arr + ) + if self.uid is None: + self.uid = self.agent.uid + asyncio.get_event_loop().run_until_complete(self.agent.connect_socket()) + + except KeyboardInterrupt: + pass + finally: + if self.agent.read_pool is not None: + self.agent.read_pool.terminate() + self.agent.read_pool.join() + return + + +if __name__ == "__main__": + logging.basicConfig(level=logging.ERROR, + format='%(asctime)s - %(name)s - %(levelname)s:\t %(message)s', + stream=sys.stdout) + log = logging.getLogger('main') + log.setLevel(logging.INFO) + + socket = PyTAKStreamingProto(address=("3.82.2.223", 8089), + certfile="certs/takserver.p12", password="atakatak", + debug=True) + + asyncio.get_event_loop().run_until_complete(socket.connect_socket()) diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakWebsocket.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakWebsocket.py new file mode 100755 index 00000000..5840b24d --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/pyTakWebsocket.py @@ -0,0 +1,262 @@ +import asyncio +import multiprocessing +import random +import ssl +import time +from collections import defaultdict + +import websockets +from requests.exceptions import ConnectionError, ConnectTimeout + +from create_proto import CotProtoMessage +from mission_api import MissionApiPyTAKHelper +from utils import p12_to_pem + +import zmq +import zmq.asyncio as azmq + +from multiprocessing import Process, Pool + + +class PyTAKWebsocket: + + def __init__(self, + address=None, + websocket_path=None, + certfile=None, + password=None, + uid=None, + self_sa_delta=None, + mission_config=None, + data_dict=None): + + self.certfile = certfile + self.password = password + self.address = address + self.websocket_path = websocket_path + self.uri = "wss://{host}:{port}/{path}".format(host=self.address[0], + port=self.address[1], + path=self.websocket_path) + + self.uid = uid or CotProtoMessage().uid + self.self_sa_delta = self_sa_delta + self.last_sa_write = time.time() - self.self_sa_delta + + if mission_config is not None: + self.mission_config = mission_config + self.mission_cot_count = defaultdict(int) + self.mission_locations = dict() + self.mission_cot_delta = self.mission_config.get("mission_write_interval") + self.last_mission_write = time.time() + self.mission_cot_delta + else: + self.mission_config = dict() + + self.data_sync_sess = MissionApiPyTAKHelper(self.address[0], self.address[1], + certfile=self.certfile, + password=self.password, + uid=self.uid, + mission_config=mission_config) + + self.ssl_context = ssl.SSLContext(ssl.PROTOCOL_TLSv1_2) + + with p12_to_pem(self.certfile, self.password) as cert: + self.ssl_context.load_cert_chain(cert) + + self.location = (random.uniform(-90, 90), random.uniform(-180, 180)) + self.data_dict = data_dict if data_dict is not None else {} + self.data_dict[self.uid] = {'write': 0, 'read': 0, 'connected': False} + + self.read_socket: azmq.Socket = None + self.read_pool: Pool = None + + + async def read_handler(self, websocket): + async for message in websocket: + if self.mission_config.get("react_to_change_message", False): + await self.read_socket.send(message) + # connection_data = self.data_dict[self.uid] + # connection_data['read'] += 1 + # self.data_dict[self.uid] = connection_data + + async def send_self_sa(self, websocket): + self_sa_message = CotProtoMessage(uid=self.uid, lat=str(self.location[0]), lon=str(self.location[1])) + self_sa_message.add_callsign_detail(group_name="Cyan", platform="PyTAKWebsocketProto") + await websocket.send(self_sa_message.serialize()) + self.last_sa_write = time.time() + + async def send_mission_cot(self, websocket): + for mission in self.data_sync_sess.missions_subscribed: + mission_loc = self.mission_locations.get(mission) + if mission_loc is None: + mission_loc = (str(random.uniform(-90, 90)), str(random.uniform(-180, 180))) + self.mission_locations[mission] = mission_loc + else: + lat = float(mission_loc[0]) + random.choice([-1, 0, 1]) + if lat < -90: + lat = -90 + elif lat > 90: + lat = 90 + lon = float(mission_loc[1]) + random.choice([-1, 0, 1]) + if lon < -180: + lon = 180 + elif lon > 180: + lon = -180 + mission_loc = (str(lat), str(lon)) + self.mission_locations[mission] = mission_loc + + send_only_new_tracks = self.mission_config.get("send_only_new_tracks", False) + mission_cot_uid = self.uid + "_mission_" + mission + if send_only_new_tracks: + mission_cot_uid += "_" + str(self.mission_cot_count[mission]) + mission_cot_proto = CotProtoMessage(uid=mission_cot_uid, lat=mission_loc[0], + lon=mission_loc[1]) + if self.mission_cot_count[mission] == 0 or send_only_new_tracks: + dest_attr = {"mission": mission} + mission_cot_proto.add_sub_detail("marti", "dest", dest_attr) + await websocket.send(mission_cot_proto.serialize()) + self.mission_cot_count[mission] += 1 + self.last_mission_write = time.time() + + async def time_to_write(self): + while True: + now = time.time() + if now - self.last_sa_write > self.self_sa_delta: + return "self_sa" + if self.mission_config.get("send_mission_cot", False) and ( + now - self.last_mission_write > self.mission_cot_delta): + return "mission_cot" + if self.data_sync_sess.ready_to_request(): + return "data_sync" + await asyncio.sleep(0.5) + + async def write_handler(self, websocket): + while True: + write_action = await self.time_to_write() + if write_action == "self_sa": + await self.send_self_sa(websocket) + elif write_action == "mission_cot": + await self.send_mission_cot(websocket) + # elif write_action == "data_sync": + # self.data_sync_sess.make_requests() + await asyncio.sleep(0.1) + connection_data = self.data_dict[self.uid] + connection_data['write'] += 1 + self.data_dict[self.uid] = connection_data + + async def connect_websocket(self): + times_connected = 0 + while True: + try: + self.data_sync_sess.get("https://{host}:{port}/".format(host=self.address[0], port=self.address[1])) + session_cookie = "JSESSIONID=" + dict(self.data_sync_sess.session.cookies).get("JSESSIONID") + async with websockets.connect(self.uri, ssl=self.ssl_context, + extra_headers={"cookie": session_cookie}) as ws: + connection_data = self.data_dict[self.uid] + connection_data['connected'] = True + self.data_dict[self.uid] = connection_data + times_connected += 1 + + if self.mission_config.get('react_to_change_message', False): + context = azmq.Context() + self.read_socket = context.socket(zmq.PUSH) + port = self.read_socket.bind_to_random_port('tcp://*') + data_sync_port = self.data_sync_sess.port + POOL_SIZE = 3 + self.read_pool = Pool(POOL_SIZE) + for i in range(POOL_SIZE): + self.read_pool.apply_async(read_thread_zmq, args=(port, data_sync_port)) + await asyncio.sleep(1) + + # get list of recent client connect / disconnect events + self.data_sync_sess.get_client_endpoints() + + read_task = asyncio.ensure_future(self.read_handler(ws)) + write_task = asyncio.ensure_future(self.write_handler(ws)) + done, pending = await asyncio.wait([read_task, write_task], return_when=asyncio.FIRST_COMPLETED) + for task in pending: + print(self.uid, "finishing?", task) + task.cancel() + for task in done: + print(self.uid, "done?", task) + + except ConnectionRefusedError as e: + print(self.uid, "connection refused", e) + except websockets.ConnectionClosed as e: + print(self.uid, "connection closed?", e) + except ConnectTimeout as e: + print(self.uid, "ConnectTimeout", e) + except ConnectionError as e: + print(self.uid, "ConnectionError: are you sure that", e.request.url, "is the correct address?") + except Exception as e: + print(self.uid, type(e), e) + connection_data = self.data_dict[self.uid] + connection_data['connected'] = False + self.data_dict[self.uid] = connection_data + print(self.uid, "retry #", times_connected) + await asyncio.sleep(0.5) + + +def read_thread_zmq(port: int, data_sync_port): + context = zmq.Context() + socket = context.socket(zmq.PULL) + data_sync_sock = context.socket(zmq.PUSH) + addr = 'tcp://localhost:' + str(port) + data_sync_addr = 'tcp://localhost:' + str(data_sync_port) + socket.connect(addr) + data_sync_sock.connect(data_sync_addr) + + try: + while True: + data = socket.recv() + proto_message = CotProtoMessage(msg=data) + if proto_message.is_sa(): + continue + + change_type, change_data = proto_message.mission_change() + if change_type: + send_data = {'req_type': change_type, 'req_data': change_data} + data_sync_sock.send_json(send_data, flags=zmq.NOBLOCK) + + fileshare_data = proto_message.fileshare() + if fileshare_data: + send_data = {'req_type': 'download_file', 'req_data': fileshare_data} + data_sync_sock.send_json(send_data, flags=zmq.NOBLOCK) + + except Exception as e: + print("read_thread error: " + str(type(e)) + " -> " + str(e.args)) + raise e + + +class PyTAKWebsocketProcess(multiprocessing.Process): + def __init__(self, address=None, websocket_path="takproto/1", uid=None, cert=None, password=None, + self_sa_delta=5.0, mission_config=None, data_dict=None): + multiprocessing.Process.__init__(self) + self.address = address + self.websocket_path = websocket_path + self.uid = uid + self.cert = cert + self.password = password + self.self_sa_delta = self_sa_delta + self.mission_config = mission_config + self.data_dict = data_dict + self.agent = None + + def run(self): + try: + self.agent = PyTAKWebsocket(address=self.address, + websocket_path=self.websocket_path, + uid=self.uid, + certfile=self.cert, + password=self.password, + self_sa_delta=self.self_sa_delta, + mission_config=self.mission_config, + data_dict=self.data_dict) + asyncio.get_event_loop().run_until_complete(self.agent.connect_websocket()) + + except KeyboardInterrupt: + pass + finally: + return + + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/requirements.txt b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/requirements.txt new file mode 100755 index 00000000..fe707f43 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/requirements.txt @@ -0,0 +1,10 @@ +lxml==4.9.2 +protobuf==3.11.3 +pyOpenSSL==23.1.1 +PyYAML==5.1 +requests==2.26 +urllib3==1.26 +websockets +pyzmq==25.0.2 +boto3==1.18 + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/runload.sh b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/runload.sh new file mode 100755 index 00000000..a60b1e99 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/runload.sh @@ -0,0 +1,3 @@ +#!/bin/bash +python3.6 tak_tester.py --test-pytak --config config_elu_load_testing.yml + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/stats.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/stats.py new file mode 100755 index 00000000..994ff3b3 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/stats.py @@ -0,0 +1,64 @@ + +# global stats_cot +# stats_cot = { +# 'messages_sent': 0, +# 'messages_received': 0, +# 'connect_count': 0, +# 'disconnect_count': 0, +# 'ping_count': 0, +# 'pong_count': 0, +# } + +# global stats_cot_proto +# stats_cot_proto = { +# 'messages_sent': 0, +# 'messages_received': 0, +# 'connect_count': 0, +# 'disconnect_count': 0, +# 'ping_count': 0, +# 'pong_count': 0, +# } +# def reset_stats(stats): +# stats['messages_sent'] = 0 +# stats['messages_received'] = 0 +# stats['connect_count'] = 0 +# stats['disconnect_count'] = 0 +# stats['ping_count'] = 0 +# stats['pong_count'] = 0 + +import threading +from multiprocessing import Process, Value, Array + +# indices in metric arrays +MESSAGES_SENT_INDEX = 0 +MESSAGES_RECEIVED_INDEX = 1 +CONNECT_EVENT_COUNT_INDEX = 2 +DISCONNECT_EVENT_COUNT_INDEX = 3 +MESSAGES_PING_COUNT_INDEX = 4 +MESSAGES_PONG_COUNT_INDEX = 5 +TIME_BETWEEN_PING_PONG = 6 # in ms + +class Stats: + + def __init__(self): + self.data = {} # key is uid, value is a dictionary + self.threadLock = threading.Lock() + + def init_uid(self, uid): + try: + self.threadLock.acquire() + self.data[uid] = Array('i', range(7)) + for i in range(len(self.data[uid])): + self.data[uid][i] = 0 + return self.data[uid] + finally: + self.threadLock.release() + + def get_uid(self, uid): + try: + self.threadLock.acquire() + return self.data.get(uid) + finally: + self.threadLock.release() + + diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/tak_tester.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/tak_tester.py new file mode 100755 index 00000000..369769ee --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/tak_tester.py @@ -0,0 +1,661 @@ +#!/usr/local/bin/python + +import argparse +import logging +import socket +import time +import sys +from multiprocessing import Lock, Manager, Pool +from pprint import pformat + +import yaml +from requests import Session + +from create_cot import CotMessage, id_gen +from mission_api import MissionApiSetupProcess +from pyTak import TLSClientProcess +if sys.version_info >= (3, 6): + from pyTakWebsocket import PyTAKWebsocketProcess + from pyTakStreamingProto import PyTAKStreamingProtoProcess + from pyTakStreamingCot import PyTAKStreamingCotProcess +from utils import IgnoreHostNameAdapter, p12_to_pem, print_data_dict +from stats import Stats +from cloud_watch import cloud_watch_process, print_info_without_sending_to_cloud_watch_process +from multiprocessing import Process, Value, Array + + +global logger + +def test_mission_api(host, port, cert, password, verbose=True): + mission_api = "https://{host}:{port}/Marti/api/".format(host=host, port=port) + enterprise_sync = "https://{host}:{port}/Marti/sync/".format(host=host, port=port) + + + with p12_to_pem(cert, password) as cert_file: + sess = Session() + sess.mount("https://", IgnoreHostNameAdapter()) + sess.cert = cert_file + sess.verify = cert_file + + logger.info("subcribing to a mission?...") + files = {'file': open('Mission-API.pdf', 'rb')} + response = sess.post(enterprise_sync + "upload", params={"name": "test_file.pdf", "creatorUid": "test"}, files=files, headers={"Content-Type": "application/pdf"}) + + + logger.info(pformat(response)) + logger.info(pformat(response.request.url)) + logger.info(pformat(response.request.headers)) + logger.info(pformat(response.json())) + + return True + + +def test_tls(host, port, cert, password, + clients=1, + self_sa_delta=5.0, + offset=1.0, + sequential_uids=True, + mission_config=None, + mission_port=None, + mission_api_config=None): + + if sequential_uids: + uids = ["PyTAK-"+str(i) for i in range(clients)] + else: + uids = [None for i in range(clients)] + + procs = list() + + if mission_config: + if not mission_port: + print("Need to specify https port if you want to use mission api") + exit(1) + print("Initializing the server with files and missions") + lock = Lock() + initializer = MissionApiSetupProcess(address=(host, mission_port), + cert=cert, password=password, + config_dict=mission_config, + lock=lock) + + procs.append(initializer) + initializer.start() + + lock.acquire() + print("Initializing complete") + lock.release() + + + for i in range(clients): + try: + p = TLSClientProcess(address=(host, port), + cert=cert, password=password, + uid=uids[i], + self_sa_delta=self_sa_delta, + mission_config=mission_api_config) + procs.append(p) + p.start() + time.sleep(offset) + except KeyboardInterrupt: + print("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + print(str(p.uid), "is dead") + exit(0) + + while True: + try: + pass + except KeyboardInterrupt: + print("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + print(str(p.uid), "is dead") + exit(0) + +def test_websocket_proto(host, port, websocket_path, cert, password, + clients=1, + self_sa_delta=5.0, + offset=1.0, + sequential_uids=True, + mission_config=None, + mission_port=None, + mission_api_config=None): + if sequential_uids: + uids = ["PyTAK-%04d" % i for i in range(clients)] + else: + uids = [None for i in range(clients)] + + procs = list() + + if mission_config: + if not mission_port: + logger.error("Need to specify https port if you want to use mission api") + exit(1) + logger.info("Initializing the server with files and missions") + lock = Lock() + initializer = MissionApiSetupProcess(address=(host, mission_port), + cert=cert, password=password, + config_dict=mission_config, + lock=lock) + + procs.append(initializer) + initializer.start() + + lock.acquire() + logger.info("Initializing complete") + lock.release() + + data_dict = Manager().dict() + for i in range(clients): + try: + p = PyTAKWebsocketProcess(address=(host, port), + websocket_path=websocket_path, + cert=cert, password=password, + uid=uids[i], + self_sa_delta=self_sa_delta, + mission_config=mission_api_config, + data_dict=data_dict) + procs.append(p) + p.start() + time.sleep(offset) + logger.info(print_data_dict(data_dict)) + except KeyboardInterrupt: + logger.info("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + exit(0) + logger.info("Done with setting up " + str(clients) + " clients") + + while True: + try: + logger.info(print_data_dict(data_dict)) + time.sleep(1) + except KeyboardInterrupt: + logger.info("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + exit(0) + +def test_streaming_proto(host, port, cert, password, + clients=1, + self_sa_delta=5.0, + offset=1.0, + sequential_uids=True, + mission_config=None, + mission_port=None, + mission_api_config=None, + ping=False, + ping_interval=1000, + send_metrics=False, + send_metrics_interval=60, + cloudwatch_namespace="pyTAK-test"): + if sequential_uids: + uids = ["PyTAK-%04d" % i for i in range(clients)] + else: + uids = [None for i in range(clients)] + + procs = list() + + if mission_config: + if not mission_port: + logger.error("Need to specify https port if you want to use mission api") + exit(1) + logger.info("Initializing the server with files and missions") + lock = Lock() + initializer = MissionApiSetupProcess(address=(host, mission_port), + cert=cert, password=password, + config_dict=mission_config, + lock=lock) + + procs.append(initializer) + initializer.start() + + lock.acquire() + logger.info("Initializing complete") + lock.release() + data_dict = Manager().dict() + + if mission_api_config is not None: + if mission_port is None: + logger.error("Need to specify https port if you want to use the mission port") + exit(1) + mission_api_config['address'] = (host, mission_port) + for i in range(clients): + try: + + #stats_uid = "proto_client_" + str(i) + uid = ("proto_client_" + str(i)) if uids[i] is None else uids[i] + arr = None + + global stats + stats.init_uid(uid) + arr = stats.get_uid(uid) + + p = PyTAKStreamingProtoProcess(address=(host, port), + cert=cert, password=password, + uid=uids[i], + self_sa_delta=self_sa_delta, + mission_config=mission_api_config, + data_dict=data_dict, + ping=ping, + ping_interval=ping_interval, + arr = arr) + procs.append(p) + p.start() + + if send_metrics: + p_cloud_watch = Process(target=cloud_watch_process, args=(uid, arr,cloudwatch_namespace, send_metrics_interval)) + procs.append(p_cloud_watch) + p_cloud_watch.start() + else: + p_info_without_sending_to_cloud_watch_process= Process(target=print_info_without_sending_to_cloud_watch_process, args=(uid, arr, send_metrics_interval)) + procs.append(p_info_without_sending_to_cloud_watch_process) + p_info_without_sending_to_cloud_watch_process.start() + + time.sleep(offset) + logger.info(print_data_dict(data_dict)) + + except KeyboardInterrupt: + logger.info("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + exit(0) + + while True: + try: + logger.info(print_data_dict(data_dict)) + time.sleep(1) + except KeyboardInterrupt: + logger.info("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + exit(0) + + +def test_streaming_cot(host, port, cert, password, + clients=1, + self_sa_delta=5.0, + offset=1.0, + sequential_uids=True, + mission_config=None, + mission_port=None, + mission_api_config=None, + debug=False, + ping=False, + ping_interval=1000, + send_metrics=False, + send_metrics_interval=60, + cloudwatch_namespace="pyTAK-test" + ): + + print("Testing streaming cot.") + logger.info("testing streaming cot.") + + if sequential_uids: + uids = ["PyTAK-%04d" % i for i in range(clients)] + else: + uids = [None for i in range(clients)] + + procs = list() + + if mission_config: + if not mission_port: + logger.error("Need to specify https port if you want to use mission api") + exit(1) + logger.info("Initializing the server with files and missions") + lock = Lock() + initializer = MissionApiSetupProcess(address=(host, mission_port), + cert=cert, password=password, + config_dict=mission_config, + lock=lock) + + procs.append(initializer) + initializer.start() + + lock.acquire() + logger.info("Initializing complete") + lock.release() + + if mission_api_config is not None: + if mission_port is None: + logger.error("Need to specify https port if you want to use the mission port") + exit(1) + mission_api_config['address'] = (host, mission_port) + + data_dict = Manager().dict() + for i in range(clients): + try: + # stats_uid = "cot_client_" + str(i) + uid = ("cot_client_" + str(i)) if uids[i] is None else uids[i] + arr = None + + global stats + stats.init_uid(uid) + arr = stats.get_uid(uid) + + logger.info("Starting pytak streaming cot process for client " + str(i)); + + p = PyTAKStreamingCotProcess(address=(host, port), + cert=cert, password=password, + uid=uids[i], + self_sa_delta=self_sa_delta, + mission_config=mission_api_config, + data_dict=data_dict, + debug = debug, + ping=ping, + ping_interval=ping_interval, + arr = arr + ) + procs.append(p) + p.start() + + if send_metrics: + p_cloud_watch = Process(target=cloud_watch_process, args=(uid, arr,cloudwatch_namespace, send_metrics_interval)) + procs.append(p_cloud_watch) + p_cloud_watch.start() + else: + p_info_without_sending_to_cloud_watch_process= Process(target=print_info_without_sending_to_cloud_watch_process, args=(uid, arr, send_metrics_interval)) + procs.append(p_info_without_sending_to_cloud_watch_process) + p_info_without_sending_to_cloud_watch_process.start() + + time.sleep(offset) + logger.info(print_data_dict(data_dict)) + except KeyboardInterrupt: + logger.info("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + exit(0) + + while True: + try: + logger.info(print_data_dict(data_dict)) + time.sleep(1) + except KeyboardInterrupt: + logger.info("trying to kill {} processes".format(len(procs))) + for p in procs: + p.join() + exit(0) + +def test_udp_wrapper(args_and_kwargs): + + args, kwargs = args_and_kwargs + return test_udp(*args, **kwargs) + +def test_udp(host, port, interval=0.25, track_uid=None): + + sock = socket.socket( socket.AF_INET, socket.SOCK_DGRAM ) + + sock.settimeout(1.0) + + if not track_uid: + track_uid = id_gen() + + lat = 40 + lon = -65 + + logger.debug("------------- Testing UDP ---------------------") + logger.debug(track_uid + " is going to be trying to send things on udp port") + while True: + try: + m = CotMessage(uid=track_uid, lat=str(lat), lon=str(lon)) + sock.sendto(m.to_string(), (host, port)) + + + time.sleep(interval) + except KeyboardInterrupt: + return + + +if __name__ == "__main__": + + parser = argparse.ArgumentParser() + + + + parser.add_argument("--https", help="specify https port for REST calls") + parser.add_argument("--test-websocket-proto", help="test the websocket protocol buffer client type", action="store_true") + + + parser.add_argument("--test-pytak", help="test using the pytak client (you can also use the PyTAK program alone)", action="store_true") + parser.add_argument("--test-pytak-proto", help="test the streaming connection with protocol buffers instead of cot xml", action="store_true") + parser.add_argument("--tls", help="specify tls port", type=int) + + parser.add_argument("--udp", help="specify udp port", type=int) + parser.add_argument("--test-udp", help="perform the udp test", action="store_true") + + + parser.add_argument("-v", "--verbose", action="store_true") + parser.add_argument("-c", "--cert", + help="path to p12 client certificate for TAK server") + parser.add_argument("-p", "--password", + default="atakatak", + help="password for p12 certificate") + + parser.add_argument("--host", + help="address of the TAK server (default 128.89.77.154)") + + parser.add_argument("--port", + default=8089, + help="tls port on the TAK server (default 8089)", + type=int) + + parser.add_argument("-i", "--interval", + help="how many seconds between self-sa messages (default 5 seconds)", + type=int) + + parser.add_argument("--offset", + help="how many seconds between starting each new client (default 1 second)", + default=1.0, + type=float) + + parser.add_argument("--config", help="use a config file to specify host:port and other settings") + + parser.add_argument("--clients", + help="how many clients should we create", + type=int) + + parser.add_argument("--sequential-uids", + action="store_true", + help="instead of random uids for each client, name them seqeuntially, ie PyTAK-0000, PyTAK-0001") + + parser.add_argument("--skip-mission-api", + action="store_true", + help="dont test mission api capabilities") + + parser.add_argument("--skip-mission-init", + action="store_true", + help="dont create and upload files and missions to the server at the beginning of the test") + + parser.add_argument("--aws", help="running this test on AWS, so replace host ip with the server ip", action="store_true") + + parser.add_argument("--ping", help="sending ping messages to server", type=bool) + + parser.add_argument("--ping-interval", help="ping interval in MILLISECOND", type=int) + + parser.add_argument("--send-metrics", help="sending client metrics to server", type=bool) + + parser.add_argument("--send-metrics-interval", help="sending metrics interval (in seconds)", type=int) + + parser.add_argument("--cloudwatch-namespace", help="CloudWatch namespace to send metrics to") + + args = parser.parse_args() + + if args.verbose: + level = logging.DEBUG + else: + level = logging.INFO + logging.basicConfig(level=level, + format='%(asctime)s - %(name)s - %(levelname)s:\t %(message)s', + stream=sys.stdout) + formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s:\t %(message)s') + handler = logging.FileHandler(filename="loadTest.log") + handler.setFormatter(formatter) + handler.setLevel(level) + logger = logging.getLogger("tak_tester") + logger.addHandler(handler) + + config_file = args.config + if config_file: + with open(config_file) as stream: + try: + if sys.version_info.major < 3: + config = yaml.load(stream) + elif sys.version_info.major >= 3: + config = yaml.load(stream, Loader=yaml.FullLoader) + except yaml.YAMLError as e: + logger.error(e) + exit(1) + + conn_config = config['connection'] + host = args.host or conn_config.get('host') + if host is None: + print("Missing host address in config (host:
    in connection section)") + exit(1) + tls = args.tls or conn_config.get('tls') + udp = args.udp or conn_config.get('udp') + https = args.https or conn_config.get('https') + + websocket_path = config['PyTAK'].get('websocket_path', 'takproto/1') + + auth_conf = config.get('authentication', {}) + cert = args.cert or auth_conf.get('cert') + password = args.password or auth_conf.get('password') + if cert is None or password is None: + print("Missing cert or password for TAK server") + + + pytak_conf = config.get('PyTAK') + if pytak_conf: + self_sa_delta = args.interval or pytak_conf.get('self_sa_delta', 5.0) + + offset = args.offset or pytak_conf.get('offset', 1.0) + pytak_clients = args.clients or pytak_conf.get('clients', 1) + + mission_api_config = config['PyTAK'].get('missions', None) + if mission_api_config is None and not args.skip_mission_api: + logger.warning("No mission api config set for the load test client") + + ping = args.ping or pytak_conf.get('ping', False) + ping_interval = args.ping_interval or pytak_conf.get('ping_interval', 1000) + send_metrics = args.send_metrics or pytak_conf.get('send_metrics', False) + send_metrics_interval = args.send_metrics_interval or pytak_conf.get('send_metrics_interval', 60) + cloudwatch_namespace = args.cloudwatch_namespace or pytak_conf.get('cloudwatch_namespace', 'pyTAK-test') + + udp_conf = config.get("UDPTest") + if udp_conf: + udp_interval = args.interval or udp_conf.get("interval", 0.1) + udp_clients = args.clients or udp_conf.get("clients", 1) + + + logger.debug("The current test config '{}' is:".format(config_file)) + logger.debug("\n"+pformat(config)) + + + else: # no config file + cert = args.cert + if cert is None: + print("-c/--cert CERT required") + exit(1) + password = args.password or "atakatak" + + host = args.host + + https = args.https + tls = args.tls + udp = args.udp + + + interval = args.interval + self_sa_delta = udp_interval = interval or 5 + + offset = args.offset + + pytak_clients = udp_clients = args.clients or 1 + + ping = args.ping or False + ping_interval = args.ping_interval or 1000 + send_metrics = args.send_metrics or False + send_metrics_interval = args.send_metrics_interval or 60 + cloudwatch_namespace = args.cloudwatch_namespace + + config = {} + + if args.aws: + print ("Open TAKServerPrivateIP.txt") + with open("TAKServerPrivateIP.txt") as f: + host = f.readline().strip() + + if args.skip_mission_api: + config["Missions"] = None + mission_api_config = None + + if args.skip_mission_init: + config["Missions"] = None + + global stats + stats = Stats() + + if args.test_pytak: + logger.info("") + logger.info("---------- testing streaming with cot xml ----------") + logger.info("testing host: {}:{}".format(host, tls)) + test_streaming_cot(host, tls, cert, password, + clients=pytak_clients, + self_sa_delta=self_sa_delta, + offset=offset, + sequential_uids=args.sequential_uids, + mission_config=config.get("Missions"), + mission_port=https, + mission_api_config=mission_api_config, + debug = args.verbose, + ping=ping, + ping_interval=ping_interval, + send_metrics=send_metrics, + send_metrics_interval=send_metrics_interval, + cloudwatch_namespace=cloudwatch_namespace + ) + + if args.test_pytak_proto: + logger.info("") + logger.info("---------- testing streaming with protocol buffer ----------") + logger.info("testing host: {}:{}".format(host, tls)) + test_streaming_proto(host, tls, cert, password, + clients=pytak_clients, + self_sa_delta=self_sa_delta, + offset=offset, + sequential_uids=args.sequential_uids, + mission_config=config.get("Missions"), + mission_port=https, + mission_api_config=mission_api_config, + ping=ping, + ping_interval=ping_interval, + send_metrics=send_metrics, + send_metrics_interval=send_metrics_interval, + cloudwatch_namespace=cloudwatch_namespace + ) + + if args.test_websocket_proto: + logger.info("") + logger.info("---------- testing websockets with protocol buffers -----------") + logger.info("testing host: {}:{}/{}".format(host, https, websocket_path)) + test_websocket_proto(host, https, websocket_path, cert, password, + clients=pytak_clients, + self_sa_delta=self_sa_delta, + offset=offset, + sequential_uids=args.sequential_uids, + mission_config=config.get("Missions"), + mission_port=https, + mission_api_config=mission_api_config) + + + if args.test_udp: + logger.info("") + logger.info("----------- testing the udp transport -------------------") + logger.info("testing host: {}:{}".format(host, udp)) + args_and_kwargs = lambda x: ((host, udp), {"track_uid": str(x), "interval": udp_interval}) + worker_data = [args_and_kwargs(i) for i in range(udp_clients)] + p = Pool(udp_clients) + try: + res = p.map(test_udp_wrapper, worker_data) + except KeyboardInterrupt: + exit() diff --git a/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/utils.py b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/utils.py new file mode 100755 index 00000000..b183b12f --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/base_takserver_config/pytak_config_files/load_test/utils.py @@ -0,0 +1,130 @@ +import contextlib +import tempfile + +import math +from OpenSSL import crypto +from lxml import etree +from requests import Session +from requests.adapters import HTTPAdapter + + +###### This allows us to verify the host without needing to match the hostname ############### +#### use these methods to make requests ##################### +class IgnoreHostNameAdapter(HTTPAdapter): + def init_poolmanager(self, *args, **kwargs): + kwargs['assert_hostname'] = False + return super(IgnoreHostNameAdapter, self).init_poolmanager(*args, **kwargs) + +def request(*args, **kwargs): + with Session() as sess: + adapter = IgnoreHostNameAdapter() + sess.mount("https://", adapter) + return sess.request(*args, **kwargs) + +def get(*args, **kwargs): + return request('get', *args, **kwargs) + +def post(*args, **kwargs): + return request('post', *args, **kwargs) + +def put(*args, **kwargs): + return request('put', *args, **kwargs) + +def delete(*args, **kwargs): + return request('delete', *args, **kwargs) +######### End HTTPS Request helpers ######################################################## + + +##### This allows us to use PKCS#12 cert files ##################### +@contextlib.contextmanager +def p12_to_pem(p12_path, p12_password): + """ Decrypts the .p12 file to be used with requests. """ + with tempfile.NamedTemporaryFile(suffix='.pem') as t_pem: + f_pem = open(t_pem.name, 'wb') + p12_data = open(p12_path, 'rb').read() + p12 = crypto.load_pkcs12(p12_data, p12_password) + f_pem.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, p12.get_privatekey())) + f_pem.write(crypto.dump_certificate(crypto.FILETYPE_PEM, p12.get_certificate())) + ca = p12.get_ca_certificates() + if ca is not None: + for cert in ca: + f_pem.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert)) + f_pem.close() + yield t_pem.name + +########## End PKCS#12 helpers #################################### + + +def is_sa_message(message): + root = etree.fromstring(message) + details = root.find("detail") + for c in details.getchildren(): + if c.tag == "contact": + return True + return False + + +def mission_change(message): + root = etree.fromstring(message) + details = root.find("detail") + for c in details.getchildren(): + if c.tag == "mission": + change_type = c.attrib.get("type") + mission_name = c.attrib.get("name") + file_hashes = list() + for mission_changes in c.getchildren(): + for mission_change in mission_changes.getchildren(): + for detail in mission_change.getchildren(): + if detail.tag == "contentResource" and detail.find("filename") is not None: + file_hashes.append(detail.find("hash").text) + if file_hashes: + return "download_mission_files", file_hashes + + if change_type in {"CHANGE", "CREATE", "INVITE", "DELETE"}: + return change_type, mission_name + + return False, False + + +prev_data = {'read': 0, 'write': 0, 'bytes': 0} +def print_data_dict(connection_data): + total_clients = len(connection_data) + connected_clients = 0 + num_writes = 0 + num_reads = 0 + #num_bytes = 0 + for client_data in connection_data.values(): + if client_data['connected']: + connected_clients += 1 + num_reads += client_data['read'] + num_writes += client_data['write'] + #num_bytes += client_data['bytes'] + # try: + # diff_reads = math.ceil((num_reads - prev_data['read']) / connected_clients) + # except ZeroDivisionError: + # diff_reads = num_reads - prev_data['read'] + # prev_data['read'] = num_reads + # + # diff_bytes = num_bytes - prev_data['bytes'] + # if diff_bytes > 1000000000: # GB + # diff_bytes = "{:.3f} GB".format((diff_bytes / 1000000000)) + # elif diff_bytes > 1000000: + # diff_bytes = "{:.3f} MB".format((diff_bytes / 1000000)) + # else: + # diff_bytes = "{:.3f} KB".format((diff_bytes / 1000)) + + # prev_data['bytes'] = num_bytes + + diff_reads = num_reads - prev_data['read'] + prev_data['read'] = num_reads + + diff_writes = num_writes - prev_data['write'] + prev_data['write'] = num_writes + + # ; num_reads/client: {reads:>10}; num_bytes: {bytes:>10}" \ + return "clients connected: {conn:>3} / {tot:>3}; num_writes: {writes:>10}; num_reads: {reads:>10} " \ + .format(conn=connected_clients, + tot=total_clients, + writes=diff_writes, + reads=diff_reads) #, + #bytes=diff_bytes) \ No newline at end of file diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/ReadMe.txt b/src/testing/fedhub_automation/takserver_config_files/certs/ReadMe.txt new file mode 100755 index 00000000..d03458c0 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/ReadMe.txt @@ -0,0 +1,65 @@ +## Changes from previous scripts: +## - massive cleanup, actually easy to set certificate metadata now +## - add in V3 attributes to limit usage of certificates appropriately +## (e.g., a client cert cannot be used as a server cert) +## - support for multi-tier CA configurations + + +To use these scripts: +## Step 1: Make the root CA +First edit cert-metadata.sh to have appropriate locality information for the +CA you want to make. + +If desired, you can also edit config.cfg and set different "basicConstraints" +in the "v3_ca" section. Speficially, you can set the pathlen attribute if you +want to limit the depth of the verification chain (only applies if doing multi- +tier CA configuration). + +Finally, run + ./makeRootCa.sh + +You should now have a 'files' subdirectory with a pem and key file + +## Step 1.a: Decide if you have an OCSP server +If you have an OCSP server that the client and server will be able to reach to +check revocation status of certificates, you can edit the 'client' and +'server' sections of config.cfg to add the authorityInfoAccess attribute. + +## Step 2: make server cert(s) +Edit cert-metadata.sh if you want to add more detail to the locality for a +given certificate. Decide what you want the CommonName for your certificate +to be. If possible, use the DNS name of the machine running the server. +In this case, we'll call it foo.bar.com; run the './makeCert.sh' script: + ./makeCert.sh server "foo.bar.com" +The script will add the appropriate v3 attributes for server name or IP address +so be aware that you may run into trouble if you try to take the same server +cert and move it to another machine. + +## Step 3: make client cert(s) +Edit cert-metadata.sh if you want to add more detail to the locality for a +given certificate. Decide what you want the CommonName for your certificate +to be (in our example, we'll use 'foo@bar.com'), then run the ./makeCert.sh +script with the client parameter: + ./makeCert.sh client "foo@bar.com" + + +## Optional: multi-tier CA: +The ./makeCert.sh script can also be used with the 'ca' parameter to make new +CAs that are signed by the root-CA. For example: + ./makeCert.sh ca intermediate-CA + +The generated truststore-intermediate-CA.[p12|jks] will have all the CAs in the +validation chain, so that is what you should use for your server and clients. + +## Note: steps below can be done automatically by the script when making a CA +To make new server or client certs that are signed my the new intermediate-CA, +go into the 'files' directory, and copy the various files over the base 'ca' +files: + cp intermediate-CA.pem ca.pem + cp intermediate-CA-trusted.pem ca-trusted.pem + cp intermediate-CA.key ca-do-not-share.key + +The Root-CA's version of those files are already saved under a different name +(e.g., root-ca.pem). Those files are used by the 'makeCert.sh' script. So in +this way you can create arbitrarily deep chains of CAs. + diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/cert-metadata.sh b/src/testing/fedhub_automation/takserver_config_files/certs/cert-metadata.sh new file mode 100755 index 00000000..39b33d6e --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/cert-metadata.sh @@ -0,0 +1,43 @@ +# Common configuration for all certificates +# Edit these fields to be appropriate for your organization +# If they are left blank, they will not be included. Do not leave COUNTRY +# blank (you may set it to "XX" if you want to be obtuse). +# +# Values for each may be optionally set as environment variables. +# Replace variables such as ${STATE} and ${CITY} as needed. +# + +COUNTRY=US +STATE=CA +CITY=Signal_Hill +ORGANIZATION=ISS +ORGANIZATIONAL_UNIT=BBN + +CAPASS=atakatak +PASS=atakatak + +## subdirectory to put all the actual certs and keys in +DIR=files + +##### don't edit below this line ##### + +if [[ -z ${STATE} || -z ${CITY} || -z ${ORGANIZATIONAL_UNIT} ]]; then + echo "Please set the following variables before running this script: STATE, CITY, ORGANIZATIONAL_UNIT. \n + The following environment variables can also be set to further secure and customize your certificates: ORGANIZATION, ORGANIZATIONAL_UNIT, CAPASS, and PASS." + exit -1 +fi + +SUBJBASE="/C=${COUNTRY}/" +if [ -n "$STATE" ]; then + SUBJBASE+="ST=${STATE}/" +fi +if [ -n "$CITY" ]; then + SUBJBASE+="L=${CITY}/" +fi +if [ -n "$ORGANIZATION" ]; then + SUBJBASE+="O=${ORGANIZATION}/" +fi +if [ -n "$ORGANIZATIONAL_UNIT" ]; then + SUBJBASE+="OU=${ORGANIZATIONAL_UNIT}/" +fi + diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/config.cfg b/src/testing/fedhub_automation/takserver_config_files/certs/config.cfg new file mode 100755 index 00000000..e45dee54 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/config.cfg @@ -0,0 +1,43 @@ + +default_crl_days= 730 # how long before next CRL + +[ ca ] + default_ca = CA_default # The default ca section + +[ CA_default ] + dir = . # Where everything is kept + certs = $dir # Where the issued certs are kept + crl_dir = $dir/crl # Where the issued crl are kept + database = $dir/crl_index.txt # database index file. + default_md = default # use public key default MD + +[ req ] + default_bits = 2048 + default_keyfile = ca.pem + distinguished_name = req_distinguished_name + x509_extensions = v3_ca + +[ req_distinguished_name ] + countryName_min = 2 + countryName_max = 2 + commonName_max = 64 + +[ v3_ca ] +#basicConstraints=critical,CA:TRUE, pathlen:2 +basicConstraints=critical,CA:TRUE +keyUsage=critical, cRLSign, keyCertSign +#nameConstraints=critical,permitted;DNS:.bbn.com # this allows you to restrict a CA to only issue server certs for a particular domain + +[ client ] +basicConstraints=critical,CA:FALSE +keyUsage=critical, digitalSignature, keyEncipherment +extendedKeyUsage = critical, clientAuth +#extendedKeyUsage = critical, clientAuth, challengePassword +#authorityInfoAccess = OCSP;URI: http://localhost:4444 + +[ server ] +basicConstraints=critical,CA:FALSE +keyUsage=critical, digitalSignature, keyEncipherment +extendedKeyUsage = critical, clientAuth, serverAuth +#authorityInfoAccess = OCSP;URI: http://localhost:4444 + diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCerts.sh b/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCerts.sh new file mode 100755 index 00000000..31778b95 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCerts.sh @@ -0,0 +1,13 @@ +#!/bin/bash +set -u +if [ -z ${CA_NAME+x} ]; +then + echo "Please set the following variables before running this script: CA_NAME. \n" + exit -1 +else + ./cert-metadata.sh + ./makeRootCa.sh --ca-name $CA_NAME + ./makeCert.sh server takserver + ./makeCert.sh client user + ./makeCert.sh client admin +fi \ No newline at end of file diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCertsConfigMap.sh b/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCertsConfigMap.sh new file mode 100755 index 00000000..f14197f1 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCertsConfigMap.sh @@ -0,0 +1,4 @@ +#!/bin/bash +set -u +./generateClusterCerts.sh +kubectl create configmap cert-migration --from-file="./files" --dry-run=client -o yaml >cert-migration.yaml \ No newline at end of file diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCertsIfNoneExist.sh b/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCertsIfNoneExist.sh new file mode 100755 index 00000000..db794294 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/generateClusterCertsIfNoneExist.sh @@ -0,0 +1,12 @@ +#!/bin/bash +CERT_DIR="/tak/certs/files" +if [ -d $CERT_DIR ] && [ -n "$(ls -A $CERT_DIR)" ] +then + echo "Existing certificates found at $CERT_DIR. New certificates will not be generated." + exit 1 +else + echo "Generating TakServer certificates..." + ./generateClusterCerts.sh +fi + +sleep infinity \ No newline at end of file diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/makeCert.sh b/src/testing/fedhub_automation/takserver_config_files/certs/makeCert.sh new file mode 100755 index 00000000..95f235d9 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/makeCert.sh @@ -0,0 +1,145 @@ +#!/bin/bash +# EDIT cert-metadata.sh before running this script! +# Optionally, you may also edit config.cfg, although unless you know what +# you are doing, you probably shouldn't. + +. cert-metadata.sh + +mkdir -p "$DIR" +cd "$DIR" + +usage() { + echo "Usage: ./makeCert.sh [server|client|ca|dbclient] " + echo " If you do not provide a common name on the command line, you will be prompted for one" + exit -1 +} + +if [ ! -e ca.pem ]; then + echo "ca.pem does not exist! Please make a CA before trying to make server certficiates" + exit -1 +fi + +if [ "$1" ]; then + if [ "$1" == "server" ]; then + EXT=server + elif [ "$1" == "client" ]; then + EXT=client + elif [ "$1" == "ca" ]; then + EXT=v3_ca + elif [ "$1" == "dbclient" ]; then + EXT=client + else + usage + fi +else + usage +fi + + +if [ "$2" ]; then + SNAME=$2 +else + if [ "$1" == "dbclient" ]; then + echo "Use default name martiuser for database client certificate" + SNAME=martiuser + else + echo "Please give the common name for your certificate (no spaces). It should be unique. If you don't enter anything, or try something under 5 characters, I will make one for you" + read SNAME + canamelen=${#SNAME} + if [[ "$canamelen" -lt 5 ]]; then + SNAME=`date +%N` + fi + fi +fi + +if [ "$1" == "server" ]; then + if [[ $SNAME =~ ^[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}$ ]]; then + ALTNAMEFIELD="IP.1" + else + ALTNAMEFIELD="DNS.1" + fi + + cp ../config.cfg config-$SNAME.cfg + echo " +subjectAltName = @alt_names + +[alt_names] +$ALTNAMEFIELD = $SNAME +" >> config-$SNAME.cfg + CONFIG=config-$SNAME.cfg +else + CONFIG=../config.cfg +fi + +CRYPTO_SETTINGS="" + +openssl list -providers 2>&1 | grep "\(invalid command\|unknown option\)" >/dev/null +if [ $? -ne 0 ] ; then + echo "Using legacy provider" + CRYPTO_SETTINGS="-legacy" +fi + +for var in "$@" +do + echo "$var" + if [ "$var" == "-fips" ] || [ "$var" == "--fips" ];then + CRYPTO_SETTINGS='-macalg SHA256 -aes256 -descert -keypbe AES-256-CBC -certpbe AES-256-CBC' + fi +done + +SUBJ=$SUBJBASE"CN=$SNAME" +echo "Making a $1 cert for " $SUBJ +if [[ "$1" == "ca" ]]; then + # Have to use the password {CAPASS} instead of {PASS} since the original CA can be replaced by this new CA at the end + openssl req -new -newkey rsa:2048 -sha256 -keyout "${SNAME}".key -passout pass:${CAPASS} -out "${SNAME}".csr -subj "$SUBJ" +else + openssl req -new -newkey rsa:2048 -sha256 -keyout "${SNAME}".key -passout pass:${PASS} -out "${SNAME}".csr -subj "$SUBJ" +fi +openssl x509 -sha256 -req -days 730 -in "${SNAME}".csr -CA ca.pem -CAkey ca-do-not-share.key -out "${SNAME}".pem -set_serial ${RANDOM} -passin pass:${CAPASS} -extensions $EXT -extfile $CONFIG + +if [[ "$1" == "ca" ]]; then + openssl x509 -in "${SNAME}".pem -addtrust clientAuth -addtrust serverAuth -setalias "${SNAME}" -out "${SNAME}"-trusted.pem +fi + +# Convert the database client private key to PKCS#8 format to use in TAK Server configuration file +if [[ "$1" == "dbclient" ]]; then + openssl pkcs8 -topk8 -outform DER -in "${SNAME}".key -passin pass:$PASS -out "${SNAME}".key.pk8 -nocrypt +fi + +# now add the chain +cat ca.pem >> "${SNAME}".pem +cat ca-trusted.pem >> "${SNAME}"-trusted.pem + +# now make pkcs12 and jks keystore files +if [[ "$1" == "server" || "$1" == "client" || "$1" == "dbclient" ]]; then + openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}".pem -inkey "${SNAME}".key -out "${SNAME}".p12 -name "${SNAME}" -CAfile ca.pem -passin pass:${PASS} -passout pass:${PASS} + keytool -importkeystore -deststorepass "${PASS}" -destkeypass "${PASS}" -destkeystore "${SNAME}".jks -srckeystore "${SNAME}".p12 -srcstoretype PKCS12 -srcstorepass "${PASS}" -alias "${SNAME}" +else # a CA + + openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}"-trusted.pem -out truststore-"${SNAME}".p12 -nokeys -passout pass:${CAPASS} + keytool -import -trustcacerts -file "${SNAME}".pem -keystore truststore-"${SNAME}".jks -storepass "${CAPASS}" -noprompt + + # include a CA signing keystore; NOT FOR DISTRIBUTION TO CLIENTS + openssl pkcs12 ${CRYPTO_SETTINGS} -export -in "${SNAME}".pem -inkey "${SNAME}".key -out "${SNAME}"-signing.p12 -name "${SNAME}" -passin pass:${CAPASS} -passout pass:${CAPASS} + keytool -importkeystore -deststorepass "${CAPASS}" -destkeypass "${CAPASS}" -destkeystore "${SNAME}"-signing.jks -srckeystore "${SNAME}"-signing.p12 -srcstoretype PKCS12 -srcstorepass "${CAPASS}" -alias "${SNAME}" + + ## create empty crl + openssl ca -config ../config.cfg -gencrl -keyfile "${SNAME}".key -key ${CAPASS} -cert "${SNAME}".pem -out "${SNAME}".crl + + echo "Do you want me to move files around so that future server and client certificates are signed by this new CA? [y/n]" + read MVREQ + if [[ "$MVREQ" == "y" || "$MVREQ" == "Y" ]]; then + cp $SNAME.pem ca.pem + cp $SNAME.key ca-do-not-share.key + cp $SNAME-trusted.pem ca-trusted.pem + else + echo "Ok, not overwriting existing keys. To manually change the CA later, execute these commands from the 'files' directory:" + echo " cp $SNAME.pem ca.pem" + echo " cp $SNAME.key ca-do-not-share.key" + echo " cp $SNAME-trusted.pem ca-trusted.pem" + fi + +fi + +chmod og-rwx "${SNAME}".key + diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/makeRootCa.sh b/src/testing/fedhub_automation/takserver_config_files/certs/makeRootCa.sh new file mode 100755 index 00000000..299c1acf --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/makeRootCa.sh @@ -0,0 +1,75 @@ +#!/bin/bash +# EDIT cert-metadata.sh before running this script! +# Optionally, you may also edit config.cfg, although unless you know what +# you are doing, you probably shouldn't. + +. cert-metadata.sh + +mkdir -p "$DIR" +cd "$DIR" + +if [ -e ca.pem ]; then + echo "ca.pem file already exists! Please delete it before trying again" + exit -1 +fi + +CA_NAME=$1 +for (( i=1; i <= "$#"; i++ )); do + if [ "${!i}" == "--ca-name" ];then + NEXT_VAL=$(($i + 1)) + CA_NAME=${!NEXT_VAL} + fi +done + +if [ -z "${CA_NAME}" ]; then + echo "Please give a name for your CA (no spaces). It should be unique. If you don't enter anything, or try something under 5 characters, I will make one for you" + read CA_NAME +fi + +canamelen=${#CA_NAME} +if [[ "$canamelen" -lt 5 ]]; then + CA_NAME=`date +%N` +fi + +CRYPTO_SETTINGS="" + +openssl list -providers 2>&1 | grep "\(invalid command\|unknown option\)" >/dev/null +if [ $? -ne 0 ] ; then + echo "Using legacy provider" + CRYPTO_SETTINGS="-legacy" +fi + +for var in "$@" +do + if [ "$var" == "-fips" ] || [ "$var" == "--fips" ];then + CRYPTO_SETTINGS='-macalg SHA256 -aes256 -descert -keypbe AES-256-CBC -certpbe AES-256-CBC' + fi +done + +SUBJ=$SUBJBASE"CN=$CA_NAME" +echo "Making a CA for " $SUBJ +openssl req -new -sha256 -x509 -days 3652 -extensions v3_ca -keyout ca-do-not-share.key -out ca.pem -passout pass:${CAPASS} -config ../config.cfg -subj "$SUBJ" +openssl x509 -in ca.pem -addtrust clientAuth -addtrust serverAuth -setalias "${CA_NAME}" -out ca-trusted.pem + +openssl pkcs12 ${CRYPTO_SETTINGS} -export -in ca-trusted.pem -out truststore-root.p12 -nokeys -caname "${CA_NAME}" -passout pass:${CAPASS} +keytool -import -trustcacerts -file ca.pem -keystore truststore-root.jks -alias "${CA_NAME}" -storepass "${CAPASS}" -noprompt +cp truststore-root.jks fed-truststore.jks + +## make copies for safety +cp ca.pem root-ca.pem +cp ca-trusted.pem root-ca-trusted.pem +cp ca-do-not-share.key root-ca-do-not-share.key + +## create empty crl +KEYPASS="-key $CAPASS" + +touch crl_index.txt +touch crl_index.txt.attr +if ! $(grep -q unique_subject crl_index.txt.attr); then + echo "unique_subject = no" >> crl_index.txt.attr +fi + +openssl ca -config ../config.cfg -gencrl -keyfile ca-do-not-share.key $KEYPASS -cert ca.pem -out ca.crl + + + diff --git a/src/testing/fedhub_automation/takserver_config_files/certs/revokeCert.sh b/src/testing/fedhub_automation/takserver_config_files/certs/revokeCert.sh new file mode 100755 index 00000000..2c54608a --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/certs/revokeCert.sh @@ -0,0 +1,46 @@ +#!/bin/bash +### Script to revoke a certificate and update the CRL. + +. cert-metadata.sh + +if [ ! "$1" ]; then + echo "Provide the filename of the certificate to revoke (.pem)." + exit 1 +fi + +if [ ! "$2" ]; then + echo "Provide the filename of the CA key (.key)." + exit 1 +fi + +if [ ! "$3" ]; then + echo "Provide the filename of the CA certificate (.pem)." + exit 1 +fi + +mkdir -p "$DIR" +cd "$DIR" + +touch crl_index.txt +touch crl_index.txt.attr +if ! $(grep -q unique_subject crl_index.txt.attr); then + echo "unique_subject = no" >> crl_index.txt.attr +fi + +## if you have a custom password for your CA key, edit this, or comment it +## out to have the openssl commands below prompt you for the password: +KEYPASS="-key $PASS" +CONFIG=../config.cfg + +# make a copy of the pem that we can modify, remove all blank lines to account for bug in Client Certificates +# interface that was saving out pem files with blank line +cp "$1.pem" "$1-revoke.pem" +sed -i '/^$/d' "$1-revoke.pem" + +openssl ca -config $CONFIG -revoke "$1-revoke.pem" -keyfile "$2".key $KEYPASS -cert "$3".pem +openssl ca -config $CONFIG -gencrl -keyfile "$2".key $KEYPASS -cert "$3".pem -out "$3".crl + +rm -f "$1-revoke.pem" + +## the command below will print the CRL in a readable form +# openssl crl -text -in *.crl diff --git a/src/testing/fedhub_automation/takserver_config_files/generate_fedhub_policy.py b/src/testing/fedhub_automation/takserver_config_files/generate_fedhub_policy.py new file mode 100755 index 00000000..4f5bb5f7 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/generate_fedhub_policy.py @@ -0,0 +1,236 @@ +import json +import os +import subprocess +import sys +import uuid + +def extract_ca_info(pem_file_path): + try: + # Run the OpenSSL command to get the fingerprint + fingerprint_output = subprocess.check_output(["openssl", "x509", "-in", pem_file_path, "-noout", "-fingerprint", "-sha256"]).decode("utf-8") + + # Extract the fingerprint from the output + fingerprint = fingerprint_output.split("sha256 Fingerprint=")[1].strip() + + # Remove colons and spaces, and convert to lowercase + cleaned_fingerprint = fingerprint.replace(":", "").replace(" ", "").lower() + + # Run the OpenSSL command to get the subject + subject_output = subprocess.check_output(["openssl", "x509", "-noout", "-subject", "-in", pem_file_path]).decode("utf-8") + + # Extract the subject from the output + subject = subject_output.split("subject=")[1].strip() + + # Split the subject into individual fields + fields = subject.split(", ") + + # Reorder and remove spaces from the fields + formatted_fields = [f.replace(" ", "") for f in fields[::-1]] + + # Join the fields back together with commas + formatted_subject = ",".join(formatted_fields) + + # Combine formatted_subject and cleaned_fingerprint with a hyphen + result = f"{formatted_subject}-{cleaned_fingerprint}" + + return result + except subprocess.CalledProcessError as e: + print(f"Error processing {pem_file_path}: {e}") + return None + +def read_pem_file(pem_file_path): + try: + with open(pem_file_path, 'r') as file: + lines = file.readlines() + content = ''.join(line.strip() for line in lines[1:-1]) # Exclude first and last line + return content + except Exception as e: + print(f"Error reading {pem_file_path}: {e}") + return None + +def generate_ui_policy(ca_contents, connection_index): + ui_policy = { + "name": "Testing", + "federate_edges": [], + "groups": [], + "additionalData": { + "uiData": { + "name": "Testing", + "version": "", + "type": "", + "description": "Testing", + "thumbnail": None, + "cells": [], + "diagramType": "Federation" + } + } + } + + z = 0 + index = 0 + + ca_group_cell_uuids = [] + + # Add groups and cells + for ca_name,pem_contents in ca_contents: + group = { + "uid": ca_name, + "interconnected": False + } + ui_policy["groups"].append(group) + + node_name = "BBN" + str(index) + + ca_group_cell_uuids.append(str(uuid.uuid4())) + + cell = { + "graphType": "GroupCell", + "id": ca_group_cell_uuids[index], + "attrs": { + ".body": {"fill": "white", "opacity": "0.35"}, + ".inner": {"visibility": "hidden"}, + "path": {"ref": ".outer"}, + "image": {"ref": ".outer", "ref-dy": "", "ref-y": 5, "xlink:href": "data:image/png;base64," + pem_contents}, + "text": {"ref-y": 0.5}, + ".content": {"text": node_name}, + ".fobj": {"width": 200, "height": 150}, + "div": {"style": {"width": 200, "height": 150}}, + ".fobj div": {"style": {"verticalAlign": "middle", "paddingTop": 0}}, + ".outer": {"stroke-width": 1, "stroke-dasharray": "none"}, + ".sub-process": {"visibility": "hidden", "data-sub-process": ""} + }, + "type": "bpmn.Activity", + "roger_federation": { + "name": ca_name, + "id": None, + "description": node_name, + "attributes": [], + "interconnected": False, + "groupFilters": [], + "type": "Group", + "stringId": node_name + }, + "size": {"width": 200, "height": 150}, + "activeConnections": [], + "icon": "circle", + "angle": 0, + "z": str(z), + "position": {"x": 1064, "y": 1346}, + "activityType": "task", + "embeds": "", + "content": node_name + } + ui_policy["additionalData"]["uiData"]["cells"].append(cell) + + z += 1 + index += 1 + + # Add edges between each pair of federates + + connection_index_int = int(connection_index) + + for i in range(len(ca_contents)): + + num_connections = 0 + + for j in range(i + 1, i + 1 + len(ca_contents)): + + if j >= len(ca_contents): + j = j - len(ca_contents) + + if i == j: + continue + + # take connection_index into account as well + if num_connections >= connection_index_int: + continue + + edge1 = { + "source": ca_contents[i][0], + "destination": ca_contents[j][0], + "groupsFilterType": "ALL" + } + edge2 = { + "source": ca_contents[j][0], + "destination": ca_contents[i][0], + "groupsFilterType": "ALL" + } + ui_policy["federate_edges"].extend([edge1, edge2]) + + name = "BBN" + str(i) + " -> BBN" + str(j) + + cell = { + "graphType": "EdgeCell", + "id": str(uuid.uuid4()), + "attrs": {}, + "type": "bpmn.Flow", + "roger_federation": { + "name": name, + "allowedGroups": [], + "disallowedGroups": [], + "groupsFilterType": "allGroups", + "edgeFilters": [], + "type": "Federate Policy" + }, + "router": { + "name": "metro" + }, + "connector": { + "name": "rounded" + }, + "z": str(z), + "source": { + "id": ca_group_cell_uuids[i] + }, + "embeds": "", + "flowType": "normal", + "target": { + "id": ca_group_cell_uuids[j] + }, + "labels": [ + { + "position": 0.5, + "attrs": { + "text": { + "text": name + } + } + } + ] + } + ui_policy["additionalData"]["uiData"]["cells"].append(cell) + + z += 1 + + num_connections += 1 + + return json.dumps(ui_policy, indent=4) + +def main(directory, connection_index): + ca_contents = [] + + for subdir in os.listdir(directory): + if subdir.startswith("takserver_config_"): + + pem_file_path = os.path.join(directory, subdir, "files", "ca.pem") + if os.path.exists(pem_file_path): + + result = extract_ca_info(pem_file_path) + pem_contents = read_pem_file(pem_file_path) + + if result and pem_contents: + ca_contents.append(tuple((result, pem_contents))) + + if ca_contents: + json_policy = generate_ui_policy(ca_contents, connection_index) + print(json_policy) + else: + print("No valid CA information found.") + +if __name__ == "__main__": + if len(sys.argv) != 3: + print("Usage: python script.py ") + sys.exit(1) + directory_path = sys.argv[1] + connection_index = sys.argv[2] + main(directory_path, connection_index) diff --git a/src/testing/fedhub_automation/takserver_config_files/load_testing_cfgs/config_elu_load_testing.yml.jn2 b/src/testing/fedhub_automation/takserver_config_files/load_testing_cfgs/config_elu_load_testing.yml.jn2 new file mode 100755 index 00000000..de1ebdc2 --- /dev/null +++ b/src/testing/fedhub_automation/takserver_config_files/load_testing_cfgs/config_elu_load_testing.yml.jn2 @@ -0,0 +1,40 @@ +connection: + host: "{{ takserver_host }}" + tls: 8089 + https: 8443 + udp: 8087 +authentication: + cert: "./certs/user.p12" + password: "atakatak" # if no password, use empty string +PyTAK: + self_sa_delta: {{ pytak_self_sa_delta }} + offset: 0 # time between starting clients + clients: {{ pytak_clients }} + websocket_path: "takproto/1" + missions: + random: False + subscribe: + - mission_1 + send_mission_cot: True + send_only_new_tracks: False + mission_write_interval: 100000 # seconds + react_to_change_message: True + download_mission_content: 1.0 # percent of clients that download file (from 0 -> 1) + download_existing_content: False + send_mission_cot_probability: 0.1 + uploads: + probability: 0.15 + size: 10000000 # 10mb + interval: 1200 # in seconds +UDPTest: + interval: 100 # milliseconds + clients: 100 +Missions: + creatorUid: PyTAK-0 + group: __ANON__ + tool: public + size_files: + - 1mb.txt: 1000000 + - 5mb.txt: 5000000 + missions: + - mission_1: diff --git a/src/testing/fedhub_automation/variables.yml b/src/testing/fedhub_automation/variables.yml new file mode 100755 index 00000000..49ec6c48 --- /dev/null +++ b/src/testing/fedhub_automation/variables.yml @@ -0,0 +1,18 @@ +aws_secret_key: # SECRET KEY HERE +aws_access_key: # ACCESS KEY HERE +aws_session_token: # SESSION TOKEN HERE +key_name: tak-bbn-admin +fedhub_version: takserver-fed-hub-5.2-DEV53.noarch.rpm +takserver_version: takserver-docker-5.2-DEV-84 +takserver_num_instances: 4 +pytak_num_instances: 1 # number of pytak instances to provision per takserver instance +clients: 1 # number of clients for each pytak client to simulate +self_sa_delta: 1 # frequency in seconds for position reporting by pytak simulated clients +fedhub_instance_type: c5.4xlarge +takserver_instance_type: c5.xlarge +pytak_instance_type: t2.medium +fedhub_ami_id: ami-027bcdee875d9ac1a +takserver_ami_id: ami-00fd0e043ae988157 +pytak_ami_id: ami-0f8604d85567e0aba +connection_index: 1 +instance_name_prefix: automation_test \ No newline at end of file diff --git a/src/testing/scripts/prep-artifact-comparison.sh b/src/testing/scripts/prep-artifact-comparison.sh index 7d8fb887..108de074 100644 --- a/src/testing/scripts/prep-artifact-comparison.sh +++ b/src/testing/scripts/prep-artifact-comparison.sh @@ -20,13 +20,15 @@ set -e -if [[ ! -f 'artifacts-master.zip' ]];then +if [[ ! -f 'artifacts-master.zip' ]] && [[ ! -d 'UNSIGNED-master' ]];then echo Please copy or symlink the reference artifacts.zip produced from the signing of a recent master CI build to artifacts-master.zip! + echo Optionally move a locally built 'UNSIGNED' directory to UNSIGNED-master. exit 1 fi -if [[ ! -f 'artifacts-modified.zip' ]];then +if [[ ! -f 'artifacts-modified.zip' ]] && [[ ! -d 'UNSIGNED-modified' ]];then echo Please copy or symlink the artifacts.zip produced from the signing of a modified branch freom the CI to artifacts-modified.zip! + echo Optionally move a locally built 'UNSIGNED' directory to UNSIGNED-modified. exit 1 fi @@ -48,11 +50,18 @@ fi extractArtifacts() { identifier=${1} + targetDir="${PWD}/artifacts-${identifier}" mkdir "${targetDir}" - unzip artifacts-${identifier}.zip -d artifacts-${identifier}-tmp - pushd "artifacts-${identifier}-tmp/SIGNED/$(ls artifacts-${identifier}-tmp/SIGNED)" + if [[ -f "artifacts-${identifier}.zip" ]];then + unzip artifacts-${identifier}.zip -d artifacts-${identifier}-tmp + pushd "artifacts-${identifier}-tmp/UNSIGNED/$(ls artifacts-${identifier}-tmp/UNSIGNED)" + + elif [[ -d "UNSIGNED-${identifier}" ]];then + version=$(ls "UNSIGNED-${identifier}/") + pushd "UNSIGNED-${identifier}/${version}" + fi for filename in ./*;do if [[ "${filename}" == *.deb ]];then @@ -108,8 +117,10 @@ extractArtifacts() { fi done popd - rm -r artifacts-${identifier}-tmp + if [[ -d "artifacts-${identifier}-tmp" ]];then + rm -r artifacts-${identifier}-tmp + fi } -extractArtifacts master +#extractArtifacts master extractArtifacts modified diff --git a/src/testing/tak-db-profiler/README.md b/src/testing/tak-db-profiler/README.md new file mode 100644 index 00000000..a7e772fc --- /dev/null +++ b/src/testing/tak-db-profiler/README.md @@ -0,0 +1,40 @@ +## Setup + +## Build + +Exporter +``` +./gradlew shadowjar buildExporter +``` + +Importer +``` +./gradlew shadowjar buildImporter +``` + +## Run + +### With default config options +``` +java -jar build/libs/tak-db-profiler-exporter-*.jar +``` +``` +java -jar build/libs/tak-db-profiler-importer-*.jar +``` + + +### With custom config options +``` +java -jar build/libs/tak-db-profiler-exporter-*.jar -host -port -password -username -configDir +``` +``` +java -jar build/libs/tak-db-profiler-importer-*.jar -host -port -password -username -configDir +``` + +Example +``` +java -jar build/libs/tak-db-profiler-exporter-*.jar -host localhost -port 5432 -password pass4marti -username martiuser -configDir /opt/tak/db-utils/db-profile +``` +``` +java -jar build/libs/tak-db-profiler-importer-*.jar -host localhost -port 5432 -password pass4marti -username martiuser -configDir /opt/tak/db-utils/db-profile +``` \ No newline at end of file diff --git a/src/testing/tak-db-profiler/build.gradle b/src/testing/tak-db-profiler/build.gradle new file mode 100644 index 00000000..e27060f3 --- /dev/null +++ b/src/testing/tak-db-profiler/build.gradle @@ -0,0 +1,105 @@ +apply plugin: 'java-library' +apply plugin: 'maven-publish' +apply plugin: 'idea' +apply plugin: 'eclipse' +apply plugin: 'application' + +apply plugin: 'com.github.johnrengelman.shadow' + +import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar + +buildscript { + repositories { + mavenCentral() + maven { + url 'https://plugins.gradle.org/m2' + } + } + dependencies { + classpath 'gradle.plugin.com.github.johnrengelman:shadow:' + gradle_shadow_version + } +} + +//apply plugin: 'com.github.johnrengelman.shadow' + +repositories { + mavenCentral() + mavenLocal() +} +dependencies { + implementation 'commons-cli:commons-cli:1.7.0' + + implementation 'org.apache.commons:commons-lang3:' + commons_lang_version + implementation group: 'org.postgresql', name: 'postgresql', version: postgres_version + implementation group: 'com.zaxxer', name: 'HikariCP', version: hikaricp_version + implementation group: 'xerces', name: 'xercesImpl', version: xerces_version + + implementation group: 'jakarta.persistence', name: 'jakarta.persistence-api', version: jakarta_persistence_api_version + implementation group: 'jakarta.xml.bind', name: 'jakarta.xml.bind-api', version: jakarta_xml_bind_api_version + api group: 'com.zaxxer', name: 'HikariCP', version: hikaricp_version + + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-annotations', version: jackson_version + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-core', version: jackson_version + implementation group: 'com.fasterxml.jackson.core', name: 'jackson-databind', version: jackson_version + implementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-yaml', version: jackson_version + implementation 'org.apache.commons:commons-lang3:' + commons_lang_version + implementation group: 'ch.qos.logback', name: 'logback-classic', version: logback_version + implementation group: 'org.apache.logging.log4j', name: 'log4j-api', version: log4j_api_version + implementation group: 'org.apache.logging.log4j', name: 'log4j-to-slf4j', version: log4j_api_version + implementation group: 'org.slf4j', name: 'slf4j-api', version: slf4j_version + + api group: 'com.google.guava', name: 'guava', version: guava_version + implementation group: 'ch.qos.logback.contrib', name: 'logback-json-classic', version: logback_jackson_version + + // required to fix version conflict for h2 between ignite and spring boot + implementation group: 'com.h2database', name: 'h2', version: h2_version + + implementation group: 'org.apache.ignite', name: 'ignite-core', version: ignite_version + implementation ("org.apache.ignite:ignite-spring:$ignite_version") { + exclude group: 'org.springframework', module: 'spring-context' + } + + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-actuator', version: spring_boot_version + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-web', version: spring_boot_version + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-cache', version: spring_boot_version + + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-jpa', version: spring_boot_version + implementation group: 'org.springframework.boot', name: 'spring-boot-loader', version: spring_boot_version + implementation group: 'org.springframework.boot', name: 'spring-boot-starter-mail', version: spring_boot_version + + implementation group: 'org.hibernate.orm', name: 'hibernate-core', version: hibernate_version + implementation group: 'org.hibernate.orm', name: 'hibernate-entitymanager', version: hibernate_entitymanager_version + + implementation group: 'org.postgresql', name: 'postgresql', version: postgres_version + + implementation group: 'jakarta.xml.bind', name: 'jakarta.xml.bind-api', version: jakarta_xml_bind_api_version + + implementation group: 'joda-time', name: 'joda-time', version: joda_version + + +} + +def jarBaseName = '' + +task buildImporter() {} +task buildExporter() {} + +shadowJar { + setZip64(true) + if (project.gradle.startParameter.taskNames.contains('buildImporter')) { + mainClassName = 'tak.db.profiler.TakDBProfilerImporter' + jarBaseName = 'tak-db-profiler-importer' + } + + if (project.gradle.startParameter.taskNames.contains('buildExporter')) { + mainClassName = 'tak.db.profiler.TakDBProfilerExporter' + jarBaseName = 'tak-db-profiler-exporter' + } + baseName = jarBaseName + classifier = 'uber' + version = '1.0' +} + +clean { + delete 'bin' +} diff --git a/src/testing/tak-db-profiler/gradle.properties b/src/testing/tak-db-profiler/gradle.properties new file mode 100644 index 00000000..5f72191a --- /dev/null +++ b/src/testing/tak-db-profiler/gradle.properties @@ -0,0 +1,73 @@ +commons_codec_version = 1.15 +commons_lang_version = 3.12.0 + + +slf4j_version = 2.0.7 +logback_version = 1.4.12 +log4j_api_version = 2.19.0 +logback_jackson_version = 0.1.5 + +postgres_version = 42.7.2 + + +guava_version = 30.1-jre + +commons_collections_version = 4.2 + +hibernate_validator_version = 8.0.1.Final + +# Current 5.6.8.Final released 04/2022 +#hibernate_version = 5.6.8.Final +# NEW_VERSION +hibernate_version = 6.1.7.Final + +# NEW_VERSION +hibernate_entitymanager_version = 6.0.0.Alpha7 + +# hikaricp_version=3.3.1 +# NEW_VERSION +hikaricp_version=5.0.1 + + +jakarta_persistence_api_version = 3.1.0 + + +jakarta_xml_bind_api_version = 4.0.1 + +jakarta_xml_ws_api_version = 4.0.1 +jakarta_xml_soap_api_version = 3.0.1 + +#javax_annotation_api_version = 1.3.2 +#javax_activation_version = 1.1.1 +jakarta_activation_api_version = 2.1.2 +#jboss_logging_version = 3.3.2.Final +#javax_transaction_version = 1.0.0.Final +jcommander_version = 1.72 + +# As of 06/2022 1.1.1 released 03/2012 is the latest with no known vulnerabilities +json_simple_version = 1.1.1 + +#jsr311_version = 1.1.1 +libidn_version = 1.15 +prettytime_version = 4.0.2.Final + +# As of 06/2022 2.7.1 released 07/2013 is the latest with no known vunlerabilities +simplexml_version = 2.7.1 + + +jackson_version = 2.14.3 + + +# As of 06/2022 2.12.2 released 01/2022 is the latest with no known vunlerabilities +xerces_version = 2.12.2 + +xpp3_version = 1.1.4c +hamcrest_version = 1.3 +junit_version = 4.12 +flyway_version = 9.8.3 +joda_version = 2.9.1 + + +jakarta_servlet_api_version = '6.0.0' + +gradle_shadow_version = 7.1.2 diff --git a/src/testing/tak-db-profiler/gradle/wrapper/gradle-wrapper.jar b/src/testing/tak-db-profiler/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000..249e5832 Binary files /dev/null and b/src/testing/tak-db-profiler/gradle/wrapper/gradle-wrapper.jar differ diff --git a/src/testing/tak-db-profiler/gradle/wrapper/gradle-wrapper.properties b/src/testing/tak-db-profiler/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ae04661e --- /dev/null +++ b/src/testing/tak-db-profiler/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/src/testing/tak-db-profiler/gradlew b/src/testing/tak-db-profiler/gradlew new file mode 100755 index 00000000..a69d9cb6 --- /dev/null +++ b/src/testing/tak-db-profiler/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/src/testing/tak-db-profiler/gradlew.bat b/src/testing/tak-db-profiler/gradlew.bat new file mode 100644 index 00000000..53a6b238 --- /dev/null +++ b/src/testing/tak-db-profiler/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DBConnection.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DBConnection.java new file mode 100644 index 00000000..21a33803 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DBConnection.java @@ -0,0 +1,29 @@ +package tak.db.profiler; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class DBConnection { + private static final Logger logger = LoggerFactory.getLogger(DBConnection.class); + private Connection connection; + + public DBConnection(TakDBProfilerParams params) { + String jdbcUrl = "jdbc:postgresql://" + params.getHost() + ":" + params.getPort() + "/" + params.getDatabase(); + try { + connection = DriverManager.getConnection(jdbcUrl, params.getUsername(), params.getPassword()); + logger.info("Connected to database: " + params); + } catch (SQLException e) { + logger.error("Error connecting to Postgres on " + jdbcUrl + " with username: " + params.getUsername() + + " and password " + params.getPassword(), e); + System.exit(1); + } + } + + public Connection getConnection() throws IllegalStateException { + return connection; + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DatabaseExportProcedure.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DatabaseExportProcedure.java new file mode 100644 index 00000000..4931aca2 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DatabaseExportProcedure.java @@ -0,0 +1,85 @@ +package tak.db.profiler; + +import java.io.File; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; + +import jakarta.annotation.PostConstruct; +import tak.db.profiler.table.CotRouterTable; +import tak.db.profiler.table.GroupsTable; +import tak.db.profiler.table.MissionResourceTable; +import tak.db.profiler.table.MissionTable; +import tak.db.profiler.table.MissionUidTable; +import tak.db.profiler.table.ResourceTable; +import tak.db.profiler.table.DatabaseStatistics; + +public class DatabaseExportProcedure { + private static final Logger logger = LoggerFactory.getLogger(DatabaseExportProcedure.class); + private TakDBProfilerParams profilerParams; + + @Autowired + DataSource dataSource; + + @Autowired + DatabaseStatistics databaseStatistics; + + @Autowired + @Qualifier("groupsTableDb") + GroupsTable groupsTableDb; + + @Autowired + @Qualifier("cotRouterTableDb") + CotRouterTable cotRouterTableDb; + + @Autowired + @Qualifier("resourceTableDb") + ResourceTable resourceTableDb; + + @Autowired + @Qualifier("missionTableDb") + MissionTable missionTableDb; + + @Autowired + @Qualifier("missionResourceTableDb") + MissionResourceTable missionResourceTableDb; + + @Autowired + @Qualifier("missionUidTableDb") + MissionUidTable missionUidTableDb; + + public DatabaseExportProcedure(TakDBProfilerParams profilerParams) { + this.profilerParams = profilerParams; + } + + @PostConstruct + public void init() { + String outputDir = profilerParams.getConfigDir(); + + saveToFile(groupsTableDb, outputDir + "/" + TakDBProfilerConstants.GROUPS_FILE_NAME); + saveToFile(missionTableDb, outputDir + "/" + TakDBProfilerConstants.MISSIONS_FILE_NAME); + saveToFile(missionUidTableDb, outputDir + "/" + TakDBProfilerConstants.MISSION_UIDS_FILE_NAME); + saveToFile(missionResourceTableDb, outputDir + "/" + TakDBProfilerConstants.MISSION_RESOURCES_FILE_NAME); + saveToFile(resourceTableDb, outputDir + "/" + TakDBProfilerConstants.RESOURCES_FILE_NAME); + saveToFile(cotRouterTableDb, outputDir + "/" + TakDBProfilerConstants.COT_ROUTER_FILE_NAME); + saveToFile(databaseStatistics, outputDir + "/" + TakDBProfilerConstants.DATABASE_STATISTICS_FILE_NAME); + } + + public static void saveToFile(Object o, String path) { + try { + ObjectMapper om = new ObjectMapper(new YAMLFactory()); + om.writeValue(new File(path), o); + logger.info("Successfully wrote configuration " + path); + } catch (Exception e) { + logger.error("Error writing " + path, e); + } + + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DatabaseImportProcedure.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DatabaseImportProcedure.java new file mode 100644 index 00000000..8ff6c07a --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/DatabaseImportProcedure.java @@ -0,0 +1,395 @@ +package tak.db.profiler; + +import java.io.BufferedOutputStream; +import java.io.ByteArrayOutputStream; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashSet; +import java.util.Random; +import java.util.Set; +import java.util.TimeZone; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicLong; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; +import org.joda.time.DateTime; +import javax.sql.DataSource; + +import org.joda.time.format.DateTimeFormat; +import org.joda.time.format.DateTimeFormatter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.ResultSetExtractor; + +import jakarta.annotation.PostConstruct; +import jakarta.xml.bind.DatatypeConverter; +import tak.db.profiler.table.CotRouterTable; +import tak.db.profiler.table.CotRouterTable.Cot; +import tak.db.profiler.table.GroupsTable; +import tak.db.profiler.table.MissionResourceTable; +import tak.db.profiler.table.MissionTable; +import tak.db.profiler.table.MissionTable.Mission; +import tak.db.profiler.table.MissionUidTable; +import tak.db.profiler.table.ResourceTable; +import tak.db.profiler.table.ResourceTable.Resource; +import tak.db.profiler.table.DatabaseStatistics; + +public class DatabaseImportProcedure { + private static final Logger logger = LoggerFactory.getLogger(DatabaseImportProcedure.class); + public static final int GROUPS_BIT_VECTOR_LEN = 32768; + private static final Calendar utcCalendar = new GregorianCalendar(TimeZone.getTimeZone("GMT")); + + private static Random random = new Random(); + + @Autowired + DataSource dataSource; + + @Autowired + @Qualifier("databaseStatisticsDB") + DatabaseStatistics databaseStatisticsDB; + + @Autowired + @Qualifier("databaseStatisticsFile") + DatabaseStatistics databaseStatisticsFile; + + @Autowired + @Qualifier("groupsTableFile") + GroupsTable groupsTableFile; + + @Autowired + @Qualifier("cotRouterTableFile") + CotRouterTable cotRouterTableFile; + + @Autowired + @Qualifier("resourceTableFile") + ResourceTable resourceTableFile; + + @Autowired + @Qualifier("missionTableFile") + MissionTable missionTableFile; + + @Autowired + @Qualifier("missionResourceTableFile") + MissionResourceTable missionResourceTableFile; + + @Autowired + @Qualifier("missionUidTableFile") + MissionUidTable missionUidTableFile; + + @PostConstruct + public void init() { + checkIfDBIsEmpty(); + + insertGroups(); + + insertCot(); + insertResources(); + + insertMissions(); + + insertMissionUids(); + insertMissionResouces(); + } + + private void insertMissionResouces() { + missionResourceTableFile.getMissionResources().forEach(res -> { + try (Connection conn = dataSource.getConnection()) { + PreparedStatement missionResourceInsert = conn.prepareStatement("INSERT INTO mission_resource" + + " (mission_id, resource_id, resource_hash) VALUES " + + "(?,(select id from resource where id = 1),(select hash from resource where id = 1) ) "); + + missionResourceInsert.setInt(1, res.getMissionId()); + + missionResourceInsert.execute(); + } catch (SQLException e) { + logger.info("exception executing mission resource insert ", e); + } + }); + } + + private void insertMissionUids() { + missionUidTableFile.getMissionCots().forEach(cot -> { + + try (Connection conn = dataSource.getConnection()) { + ResultSet rs = conn.prepareStatement("SELECT COUNT(*) FROM cot_router").executeQuery(); + long cotRouterCount = 0; + while (rs.next()) { + cotRouterCount = rs.getLong(1); + } + + Random rand = new Random(); + Set cotIds = new HashSet<>(); + + while(cotIds.size() != cot.getCount()) { + long randomNum = rand.longs(1, 101l, 101l + cotRouterCount).findFirst().getAsLong(); + cotIds.add(randomNum); + } + + for (long id: cotIds) { + PreparedStatement missionUidInsert = conn.prepareStatement("INSERT INTO mission_uid" + + " (mission_id, uid) VALUES " + + "(?, (select uid from cot_router where id = ?) ) "); + + missionUidInsert.setInt(1, cot.getMissionId()); + missionUidInsert.setLong(2, id); + + missionUidInsert.execute(); + } + } catch (SQLException e) { + logger.info("exception executing mission uid insert ", e); + } + }); + } + + private void insertMissions() { + missionTableFile.getMissions().forEach(mission -> { + try (Connection conn = dataSource.getConnection()) { + PreparedStatement missionInsert = conn.prepareStatement("INSERT INTO mission" + + " (id, name, create_time, tool, default_role_id, guid, groups) VALUES " + + "(?,?,?,?,?,?,(?)::bit(" + GROUPS_BIT_VECTOR_LEN + ") ) "); + + setMissionQueryParams(missionInsert, mission); + + missionInsert.execute(); + conn.prepareStatement("select nextval('mission_id_seq');").execute(); + } catch (SQLException e) { + logger.info("exception executing mission insert ", e); + } + }); + } + + private void insertResources() { + resourceTableFile.getResources().forEach(res -> { + try (Connection conn = dataSource.getConnection()) { + PreparedStatement resourceInsert = conn.prepareStatement("INSERT INTO resource" + + " (hash, filename, name, mimetype, keywords, expiration, groups, data) VALUES " + + "(?,?,?,?,?,?,(?)::bit(" + GROUPS_BIT_VECTOR_LEN + "),? ) "); + + setResourceQueryParams(conn, resourceInsert, res); + + resourceInsert.execute(); + + } catch (SQLException e) { + logger.info("exception executing resource insert ", e); + } + }); + } + + private void insertCot() { + AtomicLong cotUidCounter = new AtomicLong(1); + cotRouterTableFile.getCotMessages().forEach(cot -> { + try (Connection conn = dataSource.getConnection()) { + for (int i = 0; i < cot.getCount(); i++) { + PreparedStatement cotRouterInsert = conn.prepareStatement("INSERT INTO cot_router" + + " (uid, event_pt, cot_type, " + + "start, time, stale, detail, " + + "access, qos, opex, " + + "how, point_hae, point_ce, point_le, groups, " + + "id, servertime, caveat, releaseableto) VALUES " + + "(?,ST_GeometryFromText(?, 4326),?,?,?,?,?,?,?,?,?,?,?,?,(?)::bit(" + GROUPS_BIT_VECTOR_LEN + "), nextval('cot_router_seq'),?,?,?) "); + + setCotQueryParams(cotRouterInsert, cot, cotUidCounter.getAndIncrement()); + + cotRouterInsert.execute(); + } + } catch (SQLException e) { + logger.info("exception executing CoT insert ", e); + } + }); + } + + private void insertGroups() { + groupsTableFile.getGroups().forEach(group -> { + // try to create group + try { + JdbcTemplate template = new JdbcTemplate(dataSource); + Integer bitpos = null; + + // allocate a bit position + bitpos = template.query("update group_bitpos_sequence set bitpos = bitpos + 1 returning bitpos + 1", + new ResultSetExtractor() { + @Override + public Integer extractData(ResultSet rs) throws SQLException, DataAccessException { + // group exists, return it + if (rs.next()) { + return rs.getInt(1); + } else { + throw new IllegalStateException("unable to increment bit position"); + } + } + }); + + template.update("insert into groups (name, bitpos, create_ts, type) values (?, ?, now(), ?)", + new Object[] { group, bitpos, 1 }); + } catch (Exception e) { + logger.info("exception saving group " + group + " " + e.getMessage(), e); + } + }); + } + + private void checkIfDBIsEmpty() { + databaseStatisticsDB.getDatabaseStatistics().forEach(table -> { + if (table.getTableName().equals("cot_router") && table.getRowCount() > 0) { + logger.info("*** Database is not empty, aborting! ***"); + Runtime.getRuntime().halt(0); + } + if (table.getTableName().equals("groups") && table.getRowCount() > 0) { + logger.info("*** Database is not empty, aborting! ***"); + Runtime.getRuntime().halt(0); + } + }); + } + + private void setMissionQueryParams(PreparedStatement ps, Mission mission) throws SQLException { + DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZoneUTC(); + String serverTime = formatter.print(new Date().getTime()); + + ps.setInt(1, mission.getId()); + ps.setString(2, mission.getName()); + ps.setTimestamp(3, new Timestamp(DatatypeConverter.parseDateTime(serverTime).getTimeInMillis()), utcCalendar); + ps.setString(4, "public"); + ps.setInt(5, 2); + ps.setObject(6, UUID.randomUUID()); + + char[] groupsBitVector = new char[32768]; + Arrays.fill(groupsBitVector, '0'); + mission.getGroups().forEach(g -> { + int group = Integer.valueOf(g); + groupsBitVector[group] = '1'; + }); + + String groups = String.valueOf(groupsBitVector); + ps.setString(7, new StringBuilder(groups).reverse().toString()); + } + + private void setCotQueryParams(PreparedStatement ps, Cot cot, long uid) throws SQLException { + DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'").withZoneUTC(); + String serverTime = formatter.print(new Date().getTime()); + + ps.setString(1, String.valueOf(uid)); + int lat = random.nextInt(90 + 1 - -90) + -90; + int lon = random.nextInt(180 + 1 - -180) + -180; + ps.setString(2, "POINT(" + lat + " " + lon + ")"); + ps.setString(3, cot.getCotType()); + + //start + ps.setTimestamp(4, new Timestamp(DatatypeConverter.parseDateTime(serverTime).getTimeInMillis()), utcCalendar); + //time + ps.setTimestamp(5, new Timestamp(DatatypeConverter.parseDateTime(serverTime).getTimeInMillis()), utcCalendar); + //stale + ps.setTimestamp(6, new Timestamp(DatatypeConverter.parseDateTime(serverTime).getTimeInMillis()), utcCalendar); + + ps.setString(7, ""); + ps.setString(8, ""); + ps.setString(9, ""); + ps.setString(10, ""); + ps.setString(11, "m-g"); + ps.setDouble(12, 9999999.0); + ps.setDouble(13, 9999999.0); + ps.setDouble(14, 9999999.0); + + char[] groupsBitVector = new char[32768]; + Arrays.fill(groupsBitVector, '0'); + cot.getGroups().forEach(g -> { + int group = Integer.valueOf(g); + groupsBitVector[group] = '1'; + }); + + String groups = String.valueOf(groupsBitVector); + ps.setString(15, new StringBuilder(groups).reverse().toString()); + + ps.setTimestamp(16, new Timestamp(DatatypeConverter.parseDateTime(serverTime).getTimeInMillis()), utcCalendar); + + ps.setString(17, ""); + ps.setString(18, ""); + } + + private void setResourceQueryParams(Connection conn, PreparedStatement ps, Resource res) throws SQLException { + // generate fake data matching close to the original size + String content = generateRandomString(res.getSize()); + byte[] contentB = content.getBytes(); + + MessageDigest msgDigest = null; + StringBuffer hash = new StringBuffer(); + try { + msgDigest = MessageDigest.getInstance("SHA-256"); + msgDigest.update(contentB); + byte[] mdbytes = msgDigest.digest(); + for (int i = 0; i < mdbytes.length; i++) { + hash.append(Integer.toString((mdbytes[i] & 0xff) + 0x100, 16).substring(1)); + } + } catch (NoSuchAlgorithmException e) {} + + String filename = generateRandomString(16); + String mimetype; + byte[] data = new byte[0]; + // handle resource vs mission package + if (res.getKeywords().contains("missionpackage")) { + mimetype = "application/zip"; + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + BufferedOutputStream bos = new BufferedOutputStream(baos); + ZipOutputStream zos = new ZipOutputStream(bos); + + zos.putNextEntry(new ZipEntry(filename + ".txt")); + zos.write(contentB); + zos.closeEntry(); + + zos.close(); + + data = baos.toByteArray(); + } catch (Exception e) { + logger.error("exception creating mission package", e); + } + filename = filename + ".zip"; + } else { + filename = filename + ".txt"; + mimetype = "text/plain"; + data = contentB; + } + + ps.setString(1, hash.toString()); + ps.setString(2, filename); + ps.setString(3, filename); + ps.setString(4, mimetype); + + java.sql.Array keywords = conn.createArrayOf("VARCHAR", res.getKeywords().toArray()); + ps.setArray(5, keywords); + ps.setLong(6, res.getExpiration()); + + char[] groupsBitVector = new char[32768]; + Arrays.fill(groupsBitVector, '0'); + res.getGroups().forEach(g -> { + int group = Integer.valueOf(g); + groupsBitVector[group] = '1'; + }); + + String groups = String.valueOf(groupsBitVector); + ps.setString(7, new StringBuilder(groups).reverse().toString()); + ps.setBytes(8, data); + } + + private String generateRandomString(long sizeB) { + String candidateChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"; + StringBuilder sb = new StringBuilder(); + Random random = new Random(); + for (int i = 0; i < sizeB; i++) { + sb.append(candidateChars.charAt(random.nextInt(candidateChars.length()))); + } + + return sb.toString(); + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerConstants.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerConstants.java new file mode 100644 index 00000000..fcf098b7 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerConstants.java @@ -0,0 +1,11 @@ +package tak.db.profiler; + +public class TakDBProfilerConstants { + public static final String GROUPS_FILE_NAME = "groups.yml"; + public static final String MISSIONS_FILE_NAME = "mission.yml"; + public static final String MISSION_UIDS_FILE_NAME = "mission_uid.yml"; + public static final String MISSION_RESOURCES_FILE_NAME = "mission_resource.yml"; + public static final String RESOURCES_FILE_NAME = "resource.yml"; + public static final String COT_ROUTER_FILE_NAME = "cot_router.yml"; + public static final String DATABASE_STATISTICS_FILE_NAME = "table_statistics.yml"; +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerExporter.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerExporter.java new file mode 100644 index 00000000..9595dffa --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerExporter.java @@ -0,0 +1,203 @@ +package tak.db.profiler; + +import java.io.File; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import tak.db.profiler.table.CotRouterTable; +import tak.db.profiler.table.DatabaseStatistics; +import tak.db.profiler.table.GroupsTable; +import tak.db.profiler.table.MissionResourceTable; +import tak.db.profiler.table.MissionTable; +import tak.db.profiler.table.MissionUidTable; +import tak.db.profiler.table.ResourceTable; + +public class TakDBProfilerExporter { + private static final Logger logger = LoggerFactory.getLogger(TakDBProfilerExporter.class); + private static final TakDBProfilerParams profilerParams = new TakDBProfilerParams(); + + public static void main(String[] args) throws Exception { + logger.info("EXPORTER"); + Options options = new Options(); + options.addOption("username", true, "Database username"); + options.addOption("password", true, "Database password"); + options.addOption("host", true, "Database host"); + options.addOption("port", true, "Database port"); + options.addOption("configDir", true, "Config directory to use"); + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine cmd = parser.parse(options, args); + + logger.info("-USERNAME"); + if (cmd.hasOption("username")) { + profilerParams.setUsername(cmd.getOptionValue("username")); + logger.info("\tUsername provided: " + profilerParams.getUsername()); + } else { + logger.info("\tNo username provided. Using default: " + profilerParams.getUsername()); + } + + logger.info("-PASSWORD"); + if (cmd.hasOption("password")) { + profilerParams.setPassword(cmd.getOptionValue("password")); + logger.info("\tPassword provided: " + profilerParams.getPassword()); + } else { + logger.info("\tNo password provided. Using default: " + profilerParams.getPassword()); + } + + logger.info("-HOST"); + if (cmd.hasOption("host")) { + profilerParams.setHost(cmd.getOptionValue("host")); + logger.info("\tHost provided: " + profilerParams.getHost()); + } else { + logger.info("\tNo host provided. Using default: " + profilerParams.getHost()); + } + + logger.info("-PORT"); + if (cmd.hasOption("port")) { + profilerParams.setPort(Integer.parseInt(cmd.getOptionValue("port"))); + logger.info("\tPort provided: " + profilerParams.getPort()); + } else { + logger.info("\tNo port provided. Using default: " + profilerParams.getPort()); + } + + logger.info("-CONFIG DIR"); + if (cmd.hasOption("configDir")) { + profilerParams.setConfigDir(cmd.getOptionValue("configDir")); + logger.info("\tConfig directory provided: " + profilerParams.getConfigDir()); + new File(profilerParams.getConfigDir()).mkdirs(); + } else { + logger.info("\tNo config directory provided. Using default: " + profilerParams.getConfigDir()); + new File(profilerParams.getConfigDir()).mkdirs(); + } + + } catch (ParseException e) { + System.err.println("Error parsing params: " + e.getMessage()); + System.exit(1); + } + + SpringApplication application = new SpringApplication(TakDBProfilerExporter.class); + application.setWebApplicationType(WebApplicationType.NONE); + ApplicationContext context = application.run(args); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public DatabaseExportProcedure databaseExportProcedure() { + return new DatabaseExportProcedure(profilerParams); + } + + @Bean + public DatabaseStatistics databaseStatistics(DataSource dataSource) { + DatabaseStatistics databaseStatistics = new DatabaseStatistics(); + databaseStatistics.initializeTable(dataSource); + return databaseStatistics; + } + + @Bean + @Qualifier("groupsTableDb") + public GroupsTable groupsTableDb(DataSource dataSource) { + GroupsTable groupsTable = new GroupsTable(); + groupsTable.initializeTable(dataSource); + return groupsTable; + } + + @Bean + @Qualifier("cotRouterTableDb") + public CotRouterTable cotRouterTableDb(DataSource dataSource, @Qualifier("groupsTableDb") GroupsTable groupsTableDb) { + CotRouterTable cotRouterTable = new CotRouterTable(); + cotRouterTable.initializeTable(dataSource, groupsTableDb); + return cotRouterTable; + } + + @Bean + @Qualifier("resourceTableDb") + public ResourceTable resourceTableDb(DataSource dataSource, @Qualifier("groupsTableDb") GroupsTable groupsTableDb) { + ResourceTable resourceTable = new ResourceTable(); + resourceTable.initializeTable(dataSource, groupsTableDb); + return resourceTable; + } + + @Bean + @Qualifier("missionTableDb") + public MissionTable missionTableDb(DataSource dataSource, @Qualifier("groupsTableDb") GroupsTable groupsTableDb) { + MissionTable missionTable = new MissionTable(); + missionTable.initializeTable(dataSource, groupsTableDb); + return missionTable; + } + + @Bean + @Qualifier("missionResourceTableDb") + public MissionResourceTable missionResourceTableDb(DataSource dataSource) { + MissionResourceTable missionResourceTable = new MissionResourceTable(); + missionResourceTable.initializeTable(dataSource); + return missionResourceTable; + } + + @Bean + @Qualifier("missionUidTableDb") + public MissionUidTable missionUidTableDb(DataSource dataSource) { + MissionUidTable missionUidTable = new MissionUidTable(); + missionUidTable.initializeTable(dataSource); + return missionUidTable; + } + + @Bean + public HikariDataSource dataSource() { + + String jdbcUrl = "jdbc:postgresql://" + profilerParams.getHost() + ":" + profilerParams.getPort() + "/" + + profilerParams.getDatabase(); + + Properties props = new Properties(); + props.setProperty("user", profilerParams.getUsername()); + props.setProperty("password", profilerParams.getPassword()); + + HikariConfig hikariConfig = new HikariConfig(); + + hikariConfig.setUsername(profilerParams.getUsername()); + hikariConfig.setPassword(profilerParams.getPassword()); + hikariConfig.setJdbcUrl(jdbcUrl); + hikariConfig.setMaxLifetime(600000); + hikariConfig.setIdleTimeout(10000); + hikariConfig.setMaximumPoolSize(50); + hikariConfig.setConnectionTimeout(60000); + hikariConfig.setAllowPoolSuspension(true); + hikariConfig.setInitializationFailTimeout(-1); + hikariConfig.setMinimumIdle(1); + + return new HikariDataSource(hikariConfig); + } + + public static void saveToFile(Object o, String path) { + try { + ObjectMapper om = new ObjectMapper(new YAMLFactory()); + om.writeValue(new File(path), o); + logger.info("Successfully wrote configuration " + path); + } catch (Exception e) { + logger.error("Error writing " + path, e); + } + + } + +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerImporter.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerImporter.java new file mode 100644 index 00000000..833c829c --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerImporter.java @@ -0,0 +1,210 @@ +package tak.db.profiler; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.util.Properties; + +import javax.sql.DataSource; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.CommandLineParser; +import org.apache.commons.cli.DefaultParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.WebApplicationType; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; + +import tak.db.profiler.table.CotRouterTable; +import tak.db.profiler.table.GroupsTable; +import tak.db.profiler.table.MissionResourceTable; +import tak.db.profiler.table.MissionTable; +import tak.db.profiler.table.MissionUidTable; +import tak.db.profiler.table.ResourceTable; +import tak.db.profiler.table.DatabaseStatistics; + +public class TakDBProfilerImporter { + private static final Logger logger = LoggerFactory.getLogger(TakDBProfilerImporter.class); + private static final TakDBProfilerParams profilerParams = new TakDBProfilerParams(); + + public static void main(String[] args) throws Exception { + logger.info("IMPORTER"); + + Options options = new Options(); + options.addOption("username", true, "Database username"); + options.addOption("password", true, "Database password"); + options.addOption("host", true, "Database host"); + options.addOption("port", true, "Database port"); + options.addOption("configDir", true, "Config directory to use"); + + CommandLineParser parser = new DefaultParser(); + try { + CommandLine cmd = parser.parse(options, args); + + logger.info("-USERNAME"); + if (cmd.hasOption("username")) { + profilerParams.setUsername(cmd.getOptionValue("username")); + logger.info("\tUsername provided: " + profilerParams.getUsername()); + } else { + logger.info("\tNo username provided. Using default: " + profilerParams.getUsername()); + } + + logger.info("-PASSWORD"); + if (cmd.hasOption("password")) { + profilerParams.setPassword(cmd.getOptionValue("password")); + logger.info("\tPassword provided: " + profilerParams.getPassword()); + } else { + logger.info("\tNo password provided. Using default: " + profilerParams.getPassword()); + } + + logger.info("-HOST"); + if (cmd.hasOption("host")) { + profilerParams.setHost(cmd.getOptionValue("host")); + logger.info("\tHost provided: " + profilerParams.getHost()); + } else { + logger.info("\tNo host provided. Using default: " + profilerParams.getHost()); + } + + logger.info("-PORT"); + if (cmd.hasOption("port")) { + profilerParams.setPort(Integer.parseInt(cmd.getOptionValue("port"))); + logger.info("\tPort provided: " + profilerParams.getPort()); + } else { + logger.info("\tNo port provided. Using default: " + profilerParams.getPort()); + } + + logger.info("-CONFIG DIR"); + if (cmd.hasOption("configDir")) { + profilerParams.setConfigDir(cmd.getOptionValue("configDir")); + logger.info("\tConfig directory provided: " + profilerParams.getConfigDir()); + new File(profilerParams.getConfigDir()).mkdirs(); + } else { + logger.info("\tNo config directory provided. Using default: " + profilerParams.getConfigDir()); + new File(profilerParams.getConfigDir()).mkdirs(); + } + + } catch (ParseException e) { + System.err.println("Error parsing params: " + e.getMessage()); + System.exit(1); + } + + SpringApplication application = new SpringApplication(TakDBProfilerImporter.class); + application.setWebApplicationType(WebApplicationType.NONE); + ApplicationContext context = application.run(args); + } + + @Bean + @Order(Ordered.LOWEST_PRECEDENCE) + public DatabaseImportProcedure databaseImportProcedure() { + return new DatabaseImportProcedure(); + } + + @Bean + @Qualifier("databaseStatisticsDB") + public DatabaseStatistics databaseStatisticsDB(DataSource dataSource) { + DatabaseStatistics databaseStatistics = new DatabaseStatistics(); + databaseStatistics.initializeTable(dataSource); + return databaseStatistics; + } + + @Bean + @Qualifier("databaseStatisticsFile") + public DatabaseStatistics databaseStatisticsFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.DATABASE_STATISTICS_FILE_NAME, DatabaseStatistics.class); + } + + @Bean + @Qualifier("groupsTableFile") + public GroupsTable groupsTableFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.GROUPS_FILE_NAME, GroupsTable.class); + } + + @Bean + @Qualifier("cotRouterTableFile") + public CotRouterTable cotRouterTableFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.COT_ROUTER_FILE_NAME, CotRouterTable.class); + } + + @Bean + @Qualifier("resourceTableFile") + public ResourceTable resourceTableFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.RESOURCES_FILE_NAME, ResourceTable.class); + } + + @Bean + @Qualifier("missionTableFile") + public MissionTable missionTableFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.MISSIONS_FILE_NAME, MissionTable.class); + } + + @Bean + @Qualifier("missionResourceTableFile") + public MissionResourceTable missionResourceTableFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.MISSION_RESOURCES_FILE_NAME, MissionResourceTable.class); + } + + @Bean + @Qualifier("missionUidTableFile") + public MissionUidTable missionUidTableFile(DataSource dataSource) { + return loadFile(profilerParams.getConfigDir() + "/" + TakDBProfilerConstants.MISSION_UIDS_FILE_NAME, MissionUidTable.class); + } + + @Bean + public HikariDataSource dataSource() { + + String jdbcUrl = "jdbc:postgresql://" + profilerParams.getHost() + ":" + profilerParams.getPort() + "/" + + profilerParams.getDatabase(); + + Properties props = new Properties(); + props.setProperty("user", profilerParams.getUsername()); + props.setProperty("password", profilerParams.getPassword()); + + HikariConfig hikariConfig = new HikariConfig(); + + hikariConfig.setUsername(profilerParams.getUsername()); + hikariConfig.setPassword(profilerParams.getPassword()); + hikariConfig.setJdbcUrl(jdbcUrl); + hikariConfig.setMaxLifetime(600000); + hikariConfig.setIdleTimeout(10000); + hikariConfig.setMaximumPoolSize(50); + hikariConfig.setConnectionTimeout(30000); + hikariConfig.setAllowPoolSuspension(true); + hikariConfig.setInitializationFailTimeout(-1); + hikariConfig.setMinimumIdle(1); + + return new HikariDataSource(hikariConfig); + } + + public static T loadFile(String file, Class clazz) { + try { + return new ObjectMapper(new YAMLFactory()).readValue(new FileInputStream(file), clazz); + } catch (IOException e) { + logger.error("Error loading " + clazz, e); + return null; + } + } + + public static void saveToFile(Object o, String path) { + try { + ObjectMapper om = new ObjectMapper(new YAMLFactory()); + om.writeValue(new File(path), o); + logger.info("Successfully wrote configuration " + path); + } catch (Exception e) { + logger.error("Error writing " + path, e); + } + + } + +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerParams.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerParams.java new file mode 100644 index 00000000..ed04b007 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/TakDBProfilerParams.java @@ -0,0 +1,53 @@ +package tak.db.profiler; + +public class TakDBProfilerParams { + + private String username = "martiuser"; + private String password = "pass4marti"; + private String database = "cot"; + private String host = "127.0.0.1"; + private int port = 5432; + private String configDir = "/opt/tak/db-utils/db-profiler"; + + public String getUsername() { + return username; + } + public void setUsername(String username) { + this.username = username; + } + public String getPassword() { + return password; + } + public void setPassword(String password) { + this.password = password; + } + public String getDatabase() { + return database; + } + public void setDatabase(String database) { + this.database = database; + } + public String getHost() { + return host; + } + public void setHost(String host) { + this.host = host; + } + public int getPort() { + return port; + } + public void setPort(int port) { + this.port = port; + } + public String getConfigDir() { + return configDir; + } + public void setConfigDir(String configDir) { + this.configDir = configDir; + } + @Override + public String toString() { + return "TakDBProfilerParams [username=" + username + ", password=" + password + ", database=" + database + + ", host=" + host + ", port=" + port + ", configDir=" + configDir + "]"; + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/CotRouterTable.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/CotRouterTable.java new file mode 100644 index 00000000..031ad9a2 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/CotRouterTable.java @@ -0,0 +1,138 @@ +package tak.db.profiler.table; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class CotRouterTable { + private static final Logger logger = LoggerFactory.getLogger(CotRouterTable.class); + + private Collection cotMessages = new ArrayList<>(); + + @JsonIgnore + private final Map cotMap = new HashMap<>(); + + private long increment = 1000; + + public void initializeTable(DataSource dataSource, GroupsTable groupsTable) { + try { + doPagedQuery(dataSource, groupsTable, 100); + } catch (SQLException e) { + logger.error("Error populating cot router table", e); + } + cotMessages = cotMap.values(); + } + + private void doPagedQuery(DataSource dataSource, GroupsTable groupsTable, long currIdStart) throws SQLException { + PreparedStatement ps = dataSource.getConnection().prepareStatement("select cot_type, groups from cot_router where id > ? limit ?;"); + ps.setLong(1, currIdStart); + ps.setLong(2, increment); + + ResultSet rs = ps.executeQuery(); + int rowcount = 0; + while(rs.next()) { + rowcount++; + + String cotType = rs.getString("cot_type"); + String groups = rs.getString("groups"); + groups = new StringBuilder(groups).reverse().toString(); + + Set foundGroups = new HashSet(); + for (int i = 0; i < groups.toCharArray().length; i++) { + // skip null groups + if (groups.charAt(i) == '0') + continue; + + String groupName = groupsTable.getGroupsMap().get(i); + if (groupName != null) + foundGroups.add(groupName); + } + + Cot cot = new Cot(); + cot.setCotType(cotType); + cot.setGroups(foundGroups); + + Cot foundCot = cotMap.get(cot); + + if (foundCot == null) { + cot.incrementCount(); + cotMap.put(cot, cot); + } else { + foundCot.incrementCount(); + } + } + rs.close(); + + if (rowcount == increment) { + doPagedQuery(dataSource, groupsTable, currIdStart + increment); + } + } + + public Collection getCotMessages() { + return cotMessages; + } + + @Override + public String toString() { + return "CotRouterTable [cotMessages=" + cotMessages + "]"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Cot { + private long count = 0; + private String cotType = ""; + private Set groups = new HashSet<>(); + public String getCotType() { + return cotType; + } + public void setCotType(String cotType) { + this.cotType = cotType; + } + public Set getGroups() { + return groups; + } + public void setGroups(Set groups) { + this.groups = groups; + } + public void incrementCount() { + count++; + } + public long getCount() { + return count; + } + public void setCount(long count) { + this.count = count; + } + @Override + public int hashCode() { + return Objects.hash(cotType, groups); + } + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + Cot other = (Cot) obj; + return Objects.equals(cotType, other.cotType) && Objects.equals(groups, other.groups); + } + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/DatabaseStatistics.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/DatabaseStatistics.java new file mode 100644 index 00000000..ade9c20c --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/DatabaseStatistics.java @@ -0,0 +1,110 @@ +package tak.db.profiler.table; + +import java.sql.Connection; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class DatabaseStatistics { + private static final Logger logger = LoggerFactory.getLogger(DatabaseStatistics.class); + + List databaseStatistics = new ArrayList<>(); + + public void initializeTable(DataSource dataSource) { + Connection conn = null; + try { + conn = dataSource.getConnection(); + ResultSet rs = conn.getMetaData().getTables(dataSource.getConnection().getCatalog(), "", null, new String[]{"TABLE", "SEQUENCE"}); + while (rs.next()) { + String tableName = rs.getString(3); + + boolean isSeq = tableName.endsWith("_seq"); + + String query = ""; + if (isSeq) { + query = "SELECT last_value FROM " + tableName; + } else { + query = "SELECT COUNT(*) FROM " + tableName; + } + + ResultSet rs2 = conn.prepareStatement(query).executeQuery(); + while (rs2.next()) { + long res = rs2.getLong(1); + + TableStatistic tableStatistic = new TableStatistic(); + tableStatistic.setTableName(tableName); + + if (isSeq) { + tableStatistic.setLastValue(res); + } else { + tableStatistic.setRowCount(res); + } + + databaseStatistics.add(tableStatistic); + } + } + } catch (SQLException e) { + logger.error("Error populating table statistics", e); + } finally { + try { + conn.close(); + } catch (SQLException e) { + logger.error("error closing connection", e); + } + } + } + + public List getDatabaseStatistics() { + return databaseStatistics; + } + + public void setDatabaseStatistics(List databaseStatistics) { + this.databaseStatistics = databaseStatistics; + } + + @Override + public String toString() { + return "DatabaseStatistics [databaseStatistics=" + databaseStatistics + "]"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class TableStatistic { + private String tableName; + private long rowCount; + private long lastValue; + + public String getTableName() { + return tableName; + } + public long getRowCount() { + return rowCount; + } + public void setTableName(String tableName) { + this.tableName = tableName; + } + public void setRowCount(long rowCount) { + this.rowCount = rowCount; + } + public long getLastValue() { + return lastValue; + } + public void setLastValue(long lastValue) { + this.lastValue = lastValue; + } + @Override + public String toString() { + return "TableStatistic [tableName=" + tableName + ", rowCount=" + rowCount + ", lastValue=" + lastValue + + "]"; + } + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/GroupsTable.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/GroupsTable.java new file mode 100644 index 00000000..f45bd1dd --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/GroupsTable.java @@ -0,0 +1,69 @@ +package tak.db.profiler.table; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class GroupsTable { + private static final Logger logger = LoggerFactory.getLogger(GroupsTable.class); + + @JsonIgnore + private final Map groupsMap = new HashMap<>(); + + @JsonIgnore + private final Set groupSet = new HashSet<>(); + + private Collection groups = new ArrayList<>(); + + public void initializeTable(DataSource dataSource) { + try { + ResultSet rs = dataSource.getConnection().prepareStatement("select bitpos from groups;").executeQuery(); + + while(rs.next()) { + int bitpos = rs.getInt("bitpos"); + String group = String.valueOf(bitpos); + groupsMap.put(bitpos, group); + groupSet.add(group); + } + + groups = groupsMap.values(); + } catch (SQLException e) { + logger.error("Error populating groups table", e); + } + } + + public Collection getGroups() { + return groups; + } + + public void setGroups(Collection groups) { + this.groups = groups; + } + + public Map getGroupsMap() { + return groupsMap; + } + + public Set getGroupSet() { + return groupSet; + } + + @Override + public String toString() { + return "GroupsTable [groupsMap=" + groupsMap + ", groups=" + groups + "]"; + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionResourceTable.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionResourceTable.java new file mode 100644 index 00000000..b75c816e --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionResourceTable.java @@ -0,0 +1,93 @@ +package tak.db.profiler.table; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class MissionResourceTable { + private static final Logger logger = LoggerFactory.getLogger(MissionResourceTable.class); + + List missionResources = new ArrayList<>(); + + private long increment = 1000; + + public void initializeTable(DataSource dataSource) { + try { + doPagedQuery(dataSource, 0); + } catch (SQLException e) { + logger.error("Error populating mission resources table", e); + } + } + + private void doPagedQuery(DataSource dataSource, long currIdStart) throws SQLException { + PreparedStatement ps = dataSource.getConnection().prepareStatement("select mission_id,resource_id from mission_resource order by mission_id offset ? limit ?;"); + ps.setLong(1, currIdStart); + ps.setLong(2, increment); + + ResultSet rs = ps.executeQuery(); + int rowcount = 0; + while(rs.next()) { + rowcount++; + + int missionId = rs.getInt("mission_id"); + String resource_id = rs.getString("resource_id"); + + MissionResource missionResource = new MissionResource(); + missionResource.setMissionId(missionId); + missionResource.setResourceId(resource_id); + + missionResources.add(missionResource); + } + + rs.close(); + + if (rowcount == increment) { + doPagedQuery(dataSource, currIdStart + increment); + } + } + + public List getMissionResources() { + return missionResources; + } + + public void setMissionResources(List missionResources) { + this.missionResources = missionResources; + } + + @Override + public String toString() { + return "MissionResourceTable [missionResources=" + missionResources + "]"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class MissionResource { + private int missionId; + private String resourceId; + public int getMissionId() { + return missionId; + } + public void setMissionId(int missionId) { + this.missionId = missionId; + } + public String getResourceId() { + return resourceId; + } + public void setResourceId(String resourceId) { + this.resourceId = resourceId; + } + @Override + public String toString() { + return "MissionResource [missionId=" + missionId + ", resourceId=" + resourceId + "]"; + } + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionTable.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionTable.java new file mode 100644 index 00000000..feae4603 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionTable.java @@ -0,0 +1,146 @@ +package tak.db.profiler.table; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import tak.db.profiler.TakDBProfilerConstants; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class MissionTable { + private static final Logger logger = LoggerFactory.getLogger(MissionTable.class); + + private static final Set MISSION_SKIP_LIST = new HashSet<>(); + + private List missions = new ArrayList<>(); + private final Map redactedMissionNames = new HashMap<>(); + private int missionCount = 0; + + private long increment = 1000; + + public void initializeTable(DataSource dataSource, GroupsTable groupsTable) { + initMissionSkipList(); + + try { + doPagedQuery(dataSource, groupsTable, 2); + } catch (SQLException e) { + logger.error("Error populating groups table", e); + } + } + + private void doPagedQuery(DataSource dataSource, GroupsTable groupsTable, long currIdStart) throws SQLException { + PreparedStatement ps = dataSource.getConnection().prepareStatement("select id, name, groups from mission where id > ? limit ?;"); + ps.setLong(1, currIdStart); + ps.setLong(2, increment); + + ResultSet rs = ps.executeQuery(); + int rowcount = 0; + while(rs.next()) { + rowcount++; + + int id = rs.getInt("id"); + String name = rs.getString("name"); + + // get the groups and reverse the chars so that we can iterate in ascending order + String groups = rs.getString("groups"); + groups = new StringBuilder(groups).reverse().toString(); + + // skip missions we explicitly don't want to profile + if (MISSION_SKIP_LIST.contains(name)) + continue; + + // create redacted mission names. use a hashmap so that mission's with the same name + // receive the same redacted name + if (!redactedMissionNames.containsKey(name)) { + redactedMissionNames.put(name, "mission_" + missionCount++); + } + + List foundGroups = new ArrayList(); + for (int i = 0; i < groups.toCharArray().length; i++) { + // skip null groups + if (groups.charAt(i) == '0') + continue; + + String groupName = groupsTable.getGroupsMap().get(i); + if (groupName != null) + foundGroups.add(groupName); + } + + Mission mission = new Mission(); + mission.setId(id); + mission.setName(redactedMissionNames.get(name)); + mission.setGroups(foundGroups); + + missions.add(mission); + } + + rs.close(); + + if (rowcount == increment) { + doPagedQuery(dataSource, groupsTable, currIdStart + increment); + } + } + + public List getMissions() { + return missions; + } + + public void setMissions(List missions) { + this.missions = missions; + } + + private void initMissionSkipList() { + MISSION_SKIP_LIST.add("exchecktemplates"); + MISSION_SKIP_LIST.add("citrap"); + } + + @Override + public String toString() { + return "MissionTable [missions=" + missions + ", redactedMissionNames=" + redactedMissionNames + + ", missionCount=" + missionCount + "]"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Mission { + private String name; + private List groups; + private int id; + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public List getGroups() { + return groups; + } + public void setGroups(List groups) { + this.groups = groups; + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + + @Override + public String toString() { + return "Mission [name=" + name + ", groups=" + groups + ", id=" + id + "]"; + } + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionUidTable.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionUidTable.java new file mode 100644 index 00000000..0182823e --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/MissionUidTable.java @@ -0,0 +1,111 @@ +package tak.db.profiler.table; + +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import tak.db.profiler.TakDBProfilerConstants; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class MissionUidTable { + private static final Logger logger = LoggerFactory.getLogger(MissionUidTable.class); + + @JsonIgnore + private Map missionCotMap = new HashMap<>(); + + private Collection missionCots = new ArrayList<>(); + + private long increment = 10000; + + public void initializeTable(DataSource dataSource) { + try { + doPagedQuery(dataSource, 0); + missionCots = missionCotMap.values(); + + } catch (SQLException e) { + logger.error("Error populating mission uid table", e); + } + } + + private void doPagedQuery(DataSource dataSource, long currIdStart) throws SQLException { + PreparedStatement ps = dataSource.getConnection().prepareStatement("select mission_id from mission_uid order by mission_id offset ? limit ?;"); + ps.setLong(1, currIdStart); + ps.setLong(2, increment); + + ResultSet rs = ps.executeQuery(); + int rowcount = 0; + while(rs.next()) { + rowcount++; + + int missionId = rs.getInt("mission_id"); + + MissionCot missionCot = new MissionCot(); + missionCot.setMissionId(missionId); + + MissionCot foundMissionCot = missionCotMap.get(missionCot); + + if (foundMissionCot == null) { + missionCotMap.put(missionCot, missionCot); + missionCot.incrementCount(); + } else { + foundMissionCot.incrementCount(); + } + } + + rs.close(); + + if (rowcount == increment) { + doPagedQuery(dataSource, currIdStart + increment); + } + } + + public Collection getMissionCots() { + return missionCots; + } + + public void setMissionCots(Collection missionCots) { + this.missionCots = missionCots; + } + + @Override + public String toString() { + return "MissionUidTable [missionCots=" + missionCots + "]"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class MissionCot { + int missionId; + int count = 0; + public int getMissionId() { + return missionId; + } + public void setMissionId(int missionId) { + this.missionId = missionId; + } + public void incrementCount() { + this.count++; + } + public int getCount() { + return count; + } + public void setCount(int count) { + this.count = count; + } + @Override + public String toString() { + return "MissionCot [missionId=" + missionId + ", count=" + count + "]"; + } + } +} diff --git a/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/ResourceTable.java b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/ResourceTable.java new file mode 100644 index 00000000..2845b246 --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/java/tak/db/profiler/table/ResourceTable.java @@ -0,0 +1,162 @@ +package tak.db.profiler.table; + +import java.sql.Array; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; + +import tak.db.profiler.TakDBProfilerConstants; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class ResourceTable { + private static final Logger logger = LoggerFactory.getLogger(ResourceTable.class); + + private List resources = new ArrayList<>(); + + private long increment = 1000; + + public void initializeTable(DataSource dataSource, GroupsTable groupsTable) { + try { + doPagedQuery(dataSource, groupsTable, 0); + + } catch (SQLException e) { + logger.error("Error populating resource table", e); + } + } + + private void doPagedQuery(DataSource dataSource, GroupsTable groupsTable, long currIdStart) throws SQLException { + PreparedStatement ps = dataSource.getConnection().prepareStatement("select mimetype, id, groups, keywords, expiration, octet_length(data) as file_size from resource where id > ? limit ?;"); + ps.setLong(1, currIdStart); + ps.setLong(2, increment); + + ResultSet rs = ps.executeQuery(); + int rowcount = 0; + + while(rs.next()) { + rowcount++; + + String id = rs.getString("id"); + + Resource resource = new Resource(); + resource.setId(id); + resource.setMimetype(rs.getString("mimetype")); + resource.setExpiration(rs.getLong("expiration")); + resource.setSize(rs.getLong("file_size")); + + // get the groups and reverse the chars so that we can iterate in ascending order + String groups = rs.getString("groups"); + groups = new StringBuilder(groups).reverse().toString(); + + List foundGroups = new ArrayList(); + for (int i = 0; i < groups.toCharArray().length; i++) { + // skip null groups + if (groups.charAt(i) == '0') + continue; + + String groupName = groupsTable.getGroupsMap().get(i); + if (groupName != null) + foundGroups.add(groupName); + } + resource.setGroups(foundGroups); + + Array keywordsObj = rs.getArray("keywords"); + + // handle case for null keywords + String[] keywords; + if (keywordsObj == null) { + keywords = new String[0]; + } else { + keywords = (String[]) keywordsObj.getArray(); + } + + // only keyword we care about is mission package + List foundKeywords = new ArrayList(); + for (int i = 0; i < keywords.length; i++) { + String keyword = keywords[i]; + if ("missionpackage".equals(keyword)) + foundKeywords.add(keyword); + } + resource.setKeywords(foundKeywords); + + resources.add(resource); + } + rs.close(); + + if (rowcount == increment) { + doPagedQuery(dataSource, groupsTable, currIdStart + increment); + } + } + + public List getResources() { + return resources; + } + + public void setResources(List resources) { + this.resources = resources; + } + + @Override + public String toString() { + return "ResourceTable [resources=" + resources + "]"; + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class Resource { + private String id; + private String mimetype; + private long expiration; + private long size; + private List groups; + private List keywords; + public String getId() { + return id; + } + public void setId(String id) { + this.id = id; + } + public String getMimetype() { + return mimetype; + } + public void setMimetype(String mimetype) { + this.mimetype = mimetype; + } + public long getExpiration() { + return expiration; + } + public void setExpiration(long expiration) { + this.expiration = expiration; + } + public long getSize() { + return size; + } + public void setSize(long size) { + this.size = size; + } + public List getGroups() { + return groups; + } + public void setGroups(List groups) { + this.groups = groups; + } + public List getKeywords() { + return keywords; + } + public void setKeywords(List keywords) { + this.keywords = keywords; + } + @Override + public String toString() { + return "Resource [id=" + id + ", mimetype=" + mimetype + ", expiration=" + expiration + ", size=" + size + + ", groups=" + groups + ", keywords=" + keywords + "]"; + } + } +} diff --git a/src/testing/tak-db-profiler/src/main/resources/logback.xml b/src/testing/tak-db-profiler/src/main/resources/logback.xml new file mode 100644 index 00000000..20652a4e --- /dev/null +++ b/src/testing/tak-db-profiler/src/main/resources/logback.xml @@ -0,0 +1,26 @@ + + + + + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + + + + + + + + + + +