blob: d00b63f6109505db8e437bb7a04b26866fb4d2f2 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF 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
*
* http://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 org.apache.camel.component.milo.server;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.KeyPair;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Supplier;
import org.apache.camel.Endpoint;
import org.apache.camel.component.milo.KeyStoreLoader;
import org.apache.camel.component.milo.server.internal.CamelNamespace;
import org.apache.camel.spi.annotations.Component;
import org.apache.camel.support.DefaultComponent;
import org.eclipse.milo.opcua.sdk.server.OpcUaServer;
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig;
import org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfigBuilder;
import org.eclipse.milo.opcua.sdk.server.identity.AnonymousIdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.IdentityValidator;
import org.eclipse.milo.opcua.sdk.server.identity.UsernameIdentityValidator;
import org.eclipse.milo.opcua.stack.core.StatusCodes;
import org.eclipse.milo.opcua.stack.core.UaException;
import org.eclipse.milo.opcua.stack.core.application.CertificateManager;
import org.eclipse.milo.opcua.stack.core.application.CertificateValidator;
import org.eclipse.milo.opcua.stack.core.application.DefaultCertificateManager;
import org.eclipse.milo.opcua.stack.core.application.DefaultCertificateValidator;
import org.eclipse.milo.opcua.stack.core.security.SecurityPolicy;
import org.eclipse.milo.opcua.stack.core.types.builtin.LocalizedText;
import org.eclipse.milo.opcua.stack.core.types.enumerated.UserTokenType;
import org.eclipse.milo.opcua.stack.core.types.structured.BuildInfo;
import org.eclipse.milo.opcua.stack.core.types.structured.UserTokenPolicy;
import static java.util.Collections.singletonList;
import static org.eclipse.milo.opcua.sdk.server.api.config.OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS;
/**
* OPC UA Server based component
*/
@Component("milo-server")
public class MiloServerComponent extends DefaultComponent {
public static final String DEFAULT_NAMESPACE_URI = "urn:org:apache:camel";
private static final String URL_CHARSET = "UTF-8";
private static final OpcUaServerConfig DEFAULT_SERVER_CONFIG;
static {
final OpcUaServerConfigBuilder cfg = OpcUaServerConfig.builder();
cfg.setCertificateManager(new DefaultCertificateManager());
cfg.setCertificateValidator(DenyAllCertificateValidator.INSTANCE);
cfg.setSecurityPolicies(EnumSet.allOf(SecurityPolicy.class));
cfg.setApplicationName(LocalizedText.english("Apache Camel Milo Server"));
cfg.setApplicationUri("urn:org:apache:camel:milo:server");
cfg.setProductUri("urn:org:apache:camel:milo");
if (Boolean.getBoolean("org.apache.camel.milo.server.default.enableAnonymous")) {
cfg.setUserTokenPolicies(singletonList(USER_TOKEN_POLICY_ANONYMOUS));
cfg.setIdentityValidator(AnonymousIdentityValidator.INSTANCE);
}
DEFAULT_SERVER_CONFIG = cfg.build();
}
private static final class DenyAllCertificateValidator implements CertificateValidator {
public static final CertificateValidator INSTANCE = new DenyAllCertificateValidator();
private DenyAllCertificateValidator() {
}
@Override
public void validate(final X509Certificate certificate) throws UaException {
throw new UaException(StatusCodes.Bad_CertificateUseNotAllowed);
}
@Override
public void verifyTrustChain(List<X509Certificate> certificateChain) throws UaException {
throw new UaException(StatusCodes.Bad_CertificateUseNotAllowed);
}
}
private String namespaceUri = DEFAULT_NAMESPACE_URI;
private final OpcUaServerConfigBuilder serverConfig;
private OpcUaServer server;
private CamelNamespace namespace;
private final Map<String, MiloServerEndpoint> endpoints = new HashMap<>();
private Boolean enableAnonymousAuthentication;
private Map<String, String> userMap;
private String usernameSecurityPolicyUri = OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME.getSecurityPolicyUri();
private List<String> bindAddresses;
private Supplier<CertificateValidator> certificateValidator;
private final List<Runnable> runOnStop = new LinkedList<>();
public MiloServerComponent() {
this(DEFAULT_SERVER_CONFIG);
}
public MiloServerComponent(final OpcUaServerConfig serverConfig) {
this.serverConfig = OpcUaServerConfig.copy(serverConfig != null ? serverConfig : DEFAULT_SERVER_CONFIG);
}
@Override
protected void doStart() throws Exception {
this.server = new OpcUaServer(buildServerConfig());
this.namespace = this.server.getNamespaceManager().registerAndAdd(this.namespaceUri, index -> new CamelNamespace(index, this.namespaceUri, this.server));
super.doStart();
this.server.startup();
}
/**
* Build the final server configuration, apply all complex configuration
*
* @return the new server configuration, never returns {@code null}
*/
private OpcUaServerConfig buildServerConfig() {
if (this.userMap != null || this.enableAnonymousAuthentication != null) {
// set identity validator
final Map<String, String> userMap = this.userMap != null ? new HashMap<>(this.userMap) : Collections.emptyMap();
final boolean allowAnonymous = Boolean.TRUE.equals(this.enableAnonymousAuthentication);
final IdentityValidator identityValidator = new UsernameIdentityValidator(allowAnonymous, challenge -> {
final String pwd = userMap.get(challenge.getUsername());
if (pwd == null) {
return false;
}
return pwd.equals(challenge.getPassword());
});
this.serverConfig.setIdentityValidator(identityValidator);
// add token policies
final List<UserTokenPolicy> tokenPolicies = new LinkedList<>();
if (allowAnonymous) {
tokenPolicies.add(OpcUaServerConfig.USER_TOKEN_POLICY_ANONYMOUS);
}
if (userMap != null) {
tokenPolicies.add(getUsernamePolicy());
}
this.serverConfig.setUserTokenPolicies(tokenPolicies);
}
if (this.bindAddresses != null) {
this.serverConfig.setBindAddresses(new ArrayList<>(this.bindAddresses));
}
if (this.certificateValidator != null) {
final CertificateValidator validator = this.certificateValidator.get();
log.debug("Using validator: {}", validator);
if (validator instanceof Closeable) {
runOnStop(() -> {
try {
log.debug("Closing: {}", validator);
((Closeable)validator).close();
} catch (final IOException e) {
log.warn("Failed to close", e);
}
});
}
this.serverConfig.setCertificateValidator(validator);
}
// build final configuration
return this.serverConfig.build();
}
/**
* Get the user token policy for using with username authentication
*
* @return the user token policy to use for username authentication
*/
private UserTokenPolicy getUsernamePolicy() {
if (this.usernameSecurityPolicyUri == null || this.usernameSecurityPolicyUri.isEmpty()) {
return OpcUaServerConfig.USER_TOKEN_POLICY_USERNAME;
}
return new UserTokenPolicy("username", UserTokenType.UserName, null, null, this.usernameSecurityPolicyUri);
}
private void runOnStop(final Runnable runnable) {
this.runOnStop.add(runnable);
}
@Override
protected void doStop() throws Exception {
this.server.shutdown();
super.doStop();
this.runOnStop.forEach(runnable -> {
try {
runnable.run();
} catch (final Exception e) {
log.warn("Failed to run on stop", e);
}
});
this.runOnStop.clear();
}
@Override
protected Endpoint createEndpoint(final String uri, final String remaining, final Map<String, Object> parameters) throws Exception {
synchronized (this) {
if (remaining == null || remaining.isEmpty()) {
return null;
}
MiloServerEndpoint endpoint = this.endpoints.get(remaining);
if (endpoint == null) {
endpoint = new MiloServerEndpoint(uri, remaining, this.namespace, this);
setProperties(endpoint, parameters);
this.endpoints.put(remaining, endpoint);
}
return endpoint;
}
}
/**
* The URI of the namespace, defaults to <code>urn:org:apache:camel</code>
*/
public void setNamespaceUri(final String namespaceUri) {
this.namespaceUri = namespaceUri;
}
/**
* The application name
*/
public void setApplicationName(final String applicationName) {
Objects.requireNonNull(applicationName);
this.serverConfig.setApplicationName(LocalizedText.english(applicationName));
}
/**
* The application URI
*/
public void setApplicationUri(final String applicationUri) {
Objects.requireNonNull(applicationUri);
this.serverConfig.setApplicationUri(applicationUri);
}
/**
* The product URI
*/
public void setProductUri(final String productUri) {
Objects.requireNonNull(productUri);
this.serverConfig.setProductUri(productUri);
}
/**
* The TCP port the server binds to
*/
public void setBindPort(final int port) {
this.serverConfig.setBindPort(port);
}
/**
* Set whether strict endpoint URLs are enforced
*/
public void setStrictEndpointUrlsEnabled(final boolean strictEndpointUrlsEnforced) {
this.serverConfig.setStrictEndpointUrlsEnabled(strictEndpointUrlsEnforced);
}
/**
* Server name
*/
public void setServerName(final String serverName) {
this.serverConfig.setServerName(serverName);
}
/**
* Server hostname
*/
public void setHostname(final String hostname) {
this.serverConfig.setServerName(hostname);
}
/**
* Security policies
*/
public void setSecurityPolicies(final Set<SecurityPolicy> securityPolicies) {
if (securityPolicies == null || securityPolicies.isEmpty()) {
this.serverConfig.setSecurityPolicies(EnumSet.noneOf(SecurityPolicy.class));
} else {
this.serverConfig.setSecurityPolicies(EnumSet.copyOf(securityPolicies));
}
}
/**
* Security policies by URI or name
*/
public void setSecurityPoliciesById(final Collection<String> securityPolicies) {
final EnumSet<SecurityPolicy> policies = EnumSet.noneOf(SecurityPolicy.class);
if (securityPolicies != null) {
for (final String policyName : securityPolicies) {
final SecurityPolicy policy = SecurityPolicy.fromUriSafe(policyName).orElseGet(() -> SecurityPolicy.valueOf(policyName));
policies.add(policy);
}
}
this.serverConfig.setSecurityPolicies(policies);
}
/**
* Security policies by URI or name
*/
public void setSecurityPoliciesById(final String... ids) {
if (ids != null) {
setSecurityPoliciesById(Arrays.asList(ids));
} else {
setSecurityPoliciesById((Collection<String>)null);
}
}
/**
* Set user password combinations in the form of "user1:pwd1,user2:pwd2"
* <p>
* Usernames and passwords will be URL decoded
* </p>
*/
public void setUserAuthenticationCredentials(final String userAuthenticationCredentials) {
if (userAuthenticationCredentials != null) {
this.userMap = new HashMap<>();
for (final String creds : userAuthenticationCredentials.split(",")) {
final String[] toks = creds.split(":", 2);
if (toks.length == 2) {
try {
this.userMap.put(URLDecoder.decode(toks[0], URL_CHARSET), URLDecoder.decode(toks[1], URL_CHARSET));
} catch (final UnsupportedEncodingException e) {
log.warn("Failed to decode user map entry", e);
}
}
}
} else {
this.userMap = null;
}
}
/**
* Enable anonymous authentication, disabled by default
*/
public void setEnableAnonymousAuthentication(final boolean enableAnonymousAuthentication) {
this.enableAnonymousAuthentication = enableAnonymousAuthentication;
}
/**
* Set the {@link UserTokenPolicy} used when
*/
public void setUsernameSecurityPolicyUri(final SecurityPolicy usernameSecurityPolicy) {
this.usernameSecurityPolicyUri = usernameSecurityPolicy.getSecurityPolicyUri();
}
/**
* Set the {@link UserTokenPolicy} used when
*/
public void setUsernameSecurityPolicyUri(String usernameSecurityPolicyUri) {
this.usernameSecurityPolicyUri = usernameSecurityPolicyUri;
}
/**
* Set the addresses of the local addresses the server should bind to
*/
public void setBindAddresses(final String bindAddresses) {
if (bindAddresses != null) {
this.bindAddresses = Arrays.asList(bindAddresses.split(","));
} else {
this.bindAddresses = null;
}
}
/**
* Server build info
*/
public void setBuildInfo(final BuildInfo buildInfo) {
this.serverConfig.setBuildInfo(buildInfo);
}
/**
* Server certificate
*/
public void setServerCertificate(final KeyStoreLoader.Result result) {
/*
* We are not implicitly deactivating the server certificate manager. If
* the key could not be found by the KeyStoreLoader, it will return
* "null" from the load() method. So if someone calls
* setServerCertificate ( loader.load () ); he may, by accident, disable
* the server certificate. If disabling the server certificate is
* desired, do it explicitly.
*/
Objects.requireNonNull(result, "Setting a null is not supported. call setCertificateManager(null) instead.)");
setServerCertificate(result.getKeyPair(), result.getCertificate());
}
/**
* Server certificate
*/
public void setServerCertificate(final KeyPair keyPair, final X509Certificate certificate) {
setCertificateManager(new DefaultCertificateManager(keyPair, certificate));
}
/**
* Server certificate manager
*/
public void setCertificateManager(final CertificateManager certificateManager) {
if (certificateManager != null) {
this.serverConfig.setCertificateManager(certificateManager);
} else {
this.serverConfig.setCertificateManager(new DefaultCertificateManager());
}
}
/**
* Validator for client certificates
*/
public void setCertificateValidator(final Supplier<CertificateValidator> certificateValidator) {
this.certificateValidator = certificateValidator;
}
/**
* Validator for client certificates using default file based approach
*/
public void setDefaultCertificateValidator(final File certificatesBaseDir) {
this.certificateValidator = () -> new DefaultCertificateValidator(certificatesBaseDir);
}
}