mirror of
https://github.com/gyurix1968/guacamole-client.git
synced 2025-09-06 13:17:41 +00:00
Merge 0.9.14-incubating changes back to master.
This commit is contained in:
@@ -251,6 +251,24 @@ public class ConfigurationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the boolean value for whether the connection should
|
||||||
|
* follow referrals or not. By default, it will not.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The boolean value of whether to follow referrals
|
||||||
|
* as configured in guacamole.properties.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If guacamole.properties cannot be parsed.
|
||||||
|
*/
|
||||||
|
public boolean getFollowReferrals() throws GuacamoleException {
|
||||||
|
return environment.getProperty(
|
||||||
|
LDAPGuacamoleProperties.LDAP_FOLLOW_REFERRALS,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a set of LDAPSearchConstraints to apply globally
|
* Returns a set of LDAPSearchConstraints to apply globally
|
||||||
* to all LDAP searches.
|
* to all LDAP searches.
|
||||||
@@ -272,6 +290,23 @@ public class ConfigurationService {
|
|||||||
return constraints;
|
return constraints;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum number of referral hops to follow.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The maximum number of referral hops to follow
|
||||||
|
* as configured in guacamole.properties.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If guacamole.properties cannot be parsed.
|
||||||
|
*/
|
||||||
|
public int getMaxReferralHops() throws GuacamoleException {
|
||||||
|
return environment.getProperty(
|
||||||
|
LDAPGuacamoleProperties.LDAP_MAX_REFERRAL_HOPS,
|
||||||
|
5
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the search filter that should be used when querying the
|
* Returns the search filter that should be used when querying the
|
||||||
* LDAP server for Guacamole users. If no filter is specified,
|
* LDAP server for Guacamole users. If no filter is specified,
|
||||||
@@ -292,4 +327,21 @@ public class ConfigurationService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the maximum number of seconds to wait for LDAP operations.
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* The maximum number of seconds to wait for LDAP operations
|
||||||
|
* as configured in guacamole.properties.
|
||||||
|
*
|
||||||
|
* @throws GuacamoleException
|
||||||
|
* If guacamole.properties cannot be parsed.
|
||||||
|
*/
|
||||||
|
public int getOperationTimeout() throws GuacamoleException {
|
||||||
|
return environment.getProperty(
|
||||||
|
LDAPGuacamoleProperties.LDAP_OPERATION_TIMEOUT,
|
||||||
|
30
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -21,12 +21,14 @@ package org.apache.guacamole.auth.ldap;
|
|||||||
|
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.novell.ldap.LDAPConnection;
|
import com.novell.ldap.LDAPConnection;
|
||||||
|
import com.novell.ldap.LDAPConstraints;
|
||||||
import com.novell.ldap.LDAPException;
|
import com.novell.ldap.LDAPException;
|
||||||
import com.novell.ldap.LDAPJSSESecureSocketFactory;
|
import com.novell.ldap.LDAPJSSESecureSocketFactory;
|
||||||
import com.novell.ldap.LDAPJSSEStartTLSFactory;
|
import com.novell.ldap.LDAPJSSEStartTLSFactory;
|
||||||
import java.io.UnsupportedEncodingException;
|
import java.io.UnsupportedEncodingException;
|
||||||
import org.apache.guacamole.GuacamoleException;
|
import org.apache.guacamole.GuacamoleException;
|
||||||
import org.apache.guacamole.GuacamoleUnsupportedException;
|
import org.apache.guacamole.GuacamoleUnsupportedException;
|
||||||
|
import org.apache.guacamole.auth.ldap.ReferralAuthHandler;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
@@ -111,6 +113,27 @@ public class LDAPConnectionService {
|
|||||||
// Obtain appropriately-configured LDAPConnection instance
|
// Obtain appropriately-configured LDAPConnection instance
|
||||||
LDAPConnection ldapConnection = createLDAPConnection();
|
LDAPConnection ldapConnection = createLDAPConnection();
|
||||||
|
|
||||||
|
// Configure LDAP connection constraints
|
||||||
|
LDAPConstraints ldapConstraints = ldapConnection.getConstraints();
|
||||||
|
if (ldapConstraints == null)
|
||||||
|
ldapConstraints = new LDAPConstraints();
|
||||||
|
|
||||||
|
// Set whether or not we follow referrals
|
||||||
|
ldapConstraints.setReferralFollowing(confService.getFollowReferrals());
|
||||||
|
|
||||||
|
// Set referral authentication to use the provided credentials.
|
||||||
|
if (userDN != null && !userDN.isEmpty())
|
||||||
|
ldapConstraints.setReferralHandler(new ReferralAuthHandler(userDN, password));
|
||||||
|
|
||||||
|
// Set the maximum number of referrals we follow
|
||||||
|
ldapConstraints.setHopLimit(confService.getMaxReferralHops());
|
||||||
|
|
||||||
|
// Set timelimit to wait for LDAP operations, converting to ms
|
||||||
|
ldapConstraints.setTimeLimit(confService.getOperationTimeout() * 1000);
|
||||||
|
|
||||||
|
// Apply the constraints to the connection
|
||||||
|
ldapConnection.setConstraints(ldapConstraints);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// Connect to LDAP server
|
// Connect to LDAP server
|
||||||
|
@@ -19,6 +19,7 @@
|
|||||||
|
|
||||||
package org.apache.guacamole.auth.ldap;
|
package org.apache.guacamole.auth.ldap;
|
||||||
|
|
||||||
|
import org.apache.guacamole.properties.BooleanGuacamoleProperty;
|
||||||
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
import org.apache.guacamole.properties.IntegerGuacamoleProperty;
|
||||||
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
import org.apache.guacamole.properties.StringGuacamoleProperty;
|
||||||
|
|
||||||
@@ -174,4 +175,34 @@ public class LDAPGuacamoleProperties {
|
|||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not we should follow referrals.
|
||||||
|
*/
|
||||||
|
public static final BooleanGuacamoleProperty LDAP_FOLLOW_REFERRALS = new BooleanGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ldap-follow-referrals"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maximum number of referral hops to follow.
|
||||||
|
*/
|
||||||
|
public static final IntegerGuacamoleProperty LDAP_MAX_REFERRAL_HOPS = new IntegerGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ldap-max-referral-hops"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of seconds to wait for LDAP operations to complete.
|
||||||
|
*/
|
||||||
|
public static final IntegerGuacamoleProperty LDAP_OPERATION_TIMEOUT = new IntegerGuacamoleProperty() {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getName() { return "ldap-operation-timeout"; }
|
||||||
|
|
||||||
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
* 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.guacamole.auth.ldap;
|
||||||
|
|
||||||
|
import com.google.inject.Inject;
|
||||||
|
import com.novell.ldap.LDAPAuthHandler;
|
||||||
|
import com.novell.ldap.LDAPAuthProvider;
|
||||||
|
import com.novell.ldap.LDAPConnection;
|
||||||
|
import java.io.UnsupportedEncodingException;
|
||||||
|
import org.apache.guacamole.GuacamoleException;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class that implements the necessary authentication handling
|
||||||
|
* for following referrals in LDAP connections.
|
||||||
|
*/
|
||||||
|
public class ReferralAuthHandler implements LDAPAuthHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger for this class.
|
||||||
|
*/
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(ReferralAuthHandler.class);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The LDAPAuthProvider object that will be set and returned to the referral handler.
|
||||||
|
*/
|
||||||
|
private final LDAPAuthProvider ldapAuth;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a ReferralAuthHandler object to handle authentication when
|
||||||
|
* following referrals in a LDAP connection, using the provided dn and
|
||||||
|
* password.
|
||||||
|
*/
|
||||||
|
public ReferralAuthHandler(String dn, String password) {
|
||||||
|
byte[] passwordBytes;
|
||||||
|
try {
|
||||||
|
|
||||||
|
// Convert password into corresponding byte array
|
||||||
|
if (password != null)
|
||||||
|
passwordBytes = password.getBytes("UTF-8");
|
||||||
|
else
|
||||||
|
passwordBytes = null;
|
||||||
|
|
||||||
|
}
|
||||||
|
catch (UnsupportedEncodingException e) {
|
||||||
|
logger.error("Unexpected lack of support for UTF-8: {}", e.getMessage());
|
||||||
|
logger.debug("Support for UTF-8 (as required by Java spec) not found.", e);
|
||||||
|
throw new UnsupportedOperationException("Unexpected lack of UTF-8 support.", e);
|
||||||
|
}
|
||||||
|
ldapAuth = new LDAPAuthProvider(dn, passwordBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public LDAPAuthProvider getAuthProvider(String host, int port) {
|
||||||
|
return ldapAuth;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -24,6 +24,7 @@ import com.novell.ldap.LDAPAttribute;
|
|||||||
import com.novell.ldap.LDAPConnection;
|
import com.novell.ldap.LDAPConnection;
|
||||||
import com.novell.ldap.LDAPEntry;
|
import com.novell.ldap.LDAPEntry;
|
||||||
import com.novell.ldap.LDAPException;
|
import com.novell.ldap.LDAPException;
|
||||||
|
import com.novell.ldap.LDAPReferralException;
|
||||||
import com.novell.ldap.LDAPSearchResults;
|
import com.novell.ldap.LDAPSearchResults;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Enumeration;
|
import java.util.Enumeration;
|
||||||
@@ -129,62 +130,79 @@ public class ConnectionService {
|
|||||||
Map<String, Connection> connections = new HashMap<String, Connection>();
|
Map<String, Connection> connections = new HashMap<String, Connection>();
|
||||||
while (results.hasMore()) {
|
while (results.hasMore()) {
|
||||||
|
|
||||||
LDAPEntry entry = results.next();
|
try {
|
||||||
|
|
||||||
// Get common name (CN)
|
LDAPEntry entry = results.next();
|
||||||
LDAPAttribute cn = entry.getAttribute("cn");
|
|
||||||
if (cn == null) {
|
|
||||||
logger.warn("guacConfigGroup is missing a cn.");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get associated protocol
|
// Get common name (CN)
|
||||||
LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
|
LDAPAttribute cn = entry.getAttribute("cn");
|
||||||
if (protocol == null) {
|
if (cn == null) {
|
||||||
logger.warn("guacConfigGroup \"{}\" is missing the "
|
logger.warn("guacConfigGroup is missing a cn.");
|
||||||
+ "required \"guacConfigProtocol\" attribute.",
|
continue;
|
||||||
cn.getStringValue());
|
}
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set protocol
|
// Get associated protocol
|
||||||
GuacamoleConfiguration config = new GuacamoleConfiguration();
|
LDAPAttribute protocol = entry.getAttribute("guacConfigProtocol");
|
||||||
config.setProtocol(protocol.getStringValue());
|
if (protocol == null) {
|
||||||
|
logger.warn("guacConfigGroup \"{}\" is missing the "
|
||||||
|
+ "required \"guacConfigProtocol\" attribute.",
|
||||||
|
cn.getStringValue());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
// Get parameters, if any
|
// Set protocol
|
||||||
LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
|
GuacamoleConfiguration config = new GuacamoleConfiguration();
|
||||||
if (parameterAttribute != null) {
|
config.setProtocol(protocol.getStringValue());
|
||||||
|
|
||||||
// For each parameter
|
// Get parameters, if any
|
||||||
Enumeration<?> parameters = parameterAttribute.getStringValues();
|
LDAPAttribute parameterAttribute = entry.getAttribute("guacConfigParameter");
|
||||||
while (parameters.hasMoreElements()) {
|
if (parameterAttribute != null) {
|
||||||
|
|
||||||
String parameter = (String) parameters.nextElement();
|
// For each parameter
|
||||||
|
Enumeration<?> parameters = parameterAttribute.getStringValues();
|
||||||
|
while (parameters.hasMoreElements()) {
|
||||||
|
|
||||||
// Parse parameter
|
String parameter = (String) parameters.nextElement();
|
||||||
int equals = parameter.indexOf('=');
|
|
||||||
if (equals != -1) {
|
|
||||||
|
|
||||||
// Parse name
|
// Parse parameter
|
||||||
String name = parameter.substring(0, equals);
|
int equals = parameter.indexOf('=');
|
||||||
String value = parameter.substring(equals+1);
|
if (equals != -1) {
|
||||||
|
|
||||||
config.setParameter(name, value);
|
// Parse name
|
||||||
|
String name = parameter.substring(0, equals);
|
||||||
|
String value = parameter.substring(equals+1);
|
||||||
|
|
||||||
|
config.setParameter(name, value);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter the configuration, substituting all defined tokens
|
||||||
|
tokenFilter.filterValues(config.getParameters());
|
||||||
|
|
||||||
|
// Store connection using cn for both identifier and name
|
||||||
|
String name = cn.getStringValue();
|
||||||
|
Connection connection = new SimpleConnection(name, name, config);
|
||||||
|
connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP);
|
||||||
|
connections.put(name, connection);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter the configuration, substituting all defined tokens
|
// Deal with issues following LDAP referrals
|
||||||
tokenFilter.filterValues(config.getParameters());
|
catch (LDAPReferralException e) {
|
||||||
|
if (confService.getFollowReferrals()) {
|
||||||
// Store connection using cn for both identifier and name
|
logger.error("Could not follow referral: {}", e.getFailedReferral());
|
||||||
String name = cn.getStringValue();
|
logger.debug("Error encountered trying to follow referral.", e);
|
||||||
Connection connection = new SimpleConnection(name, name, config);
|
throw new GuacamoleServerException("Could not follow LDAP referral.", e);
|
||||||
connection.setParentIdentifier(LDAPAuthenticationProvider.ROOT_CONNECTION_GROUP);
|
}
|
||||||
connections.put(name, connection);
|
else {
|
||||||
|
logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
|
||||||
|
logger.debug("Got a referral, but configured to not follow them.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,8 +269,22 @@ public class ConnectionService {
|
|||||||
// The guacConfig group uses the seeAlso attribute to refer
|
// The guacConfig group uses the seeAlso attribute to refer
|
||||||
// to these other groups
|
// to these other groups
|
||||||
while (userRoleGroupResults.hasMore()) {
|
while (userRoleGroupResults.hasMore()) {
|
||||||
LDAPEntry entry = userRoleGroupResults.next();
|
try {
|
||||||
connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")");
|
LDAPEntry entry = userRoleGroupResults.next();
|
||||||
|
connectionSearchFilter.append("(seeAlso=").append(escapingService.escapeLDAPSearchFilter(entry.getDN())).append(")");
|
||||||
|
}
|
||||||
|
|
||||||
|
catch (LDAPReferralException e) {
|
||||||
|
if (confService.getFollowReferrals()) {
|
||||||
|
logger.error("Could not follow referral: {}", e.getFailedReferral());
|
||||||
|
logger.debug("Error encountered trying to follow referral.", e);
|
||||||
|
throw new GuacamoleServerException("Could not follow LDAP referral.", e);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
|
||||||
|
logger.debug("Got a referral, but configured to not follow them.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -24,6 +24,7 @@ import com.novell.ldap.LDAPAttribute;
|
|||||||
import com.novell.ldap.LDAPConnection;
|
import com.novell.ldap.LDAPConnection;
|
||||||
import com.novell.ldap.LDAPEntry;
|
import com.novell.ldap.LDAPEntry;
|
||||||
import com.novell.ldap.LDAPException;
|
import com.novell.ldap.LDAPException;
|
||||||
|
import com.novell.ldap.LDAPReferralException;
|
||||||
import com.novell.ldap.LDAPSearchResults;
|
import com.novell.ldap.LDAPSearchResults;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
@@ -107,19 +108,36 @@ public class UserService {
|
|||||||
// Read all visible users
|
// Read all visible users
|
||||||
while (results.hasMore()) {
|
while (results.hasMore()) {
|
||||||
|
|
||||||
LDAPEntry entry = results.next();
|
try {
|
||||||
|
|
||||||
|
LDAPEntry entry = results.next();
|
||||||
|
|
||||||
|
// Get username from record
|
||||||
|
LDAPAttribute username = entry.getAttribute(usernameAttribute);
|
||||||
|
if (username == null) {
|
||||||
|
logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store user using their username as the identifier
|
||||||
|
String identifier = username.getStringValue();
|
||||||
|
if (users.put(identifier, new SimpleUser(identifier)) != null)
|
||||||
|
logger.warn("Possibly ambiguous user account: \"{}\".", identifier);
|
||||||
|
|
||||||
// Get username from record
|
|
||||||
LDAPAttribute username = entry.getAttribute(usernameAttribute);
|
|
||||||
if (username == null) {
|
|
||||||
logger.warn("Queried user is missing the username attribute \"{}\".", usernameAttribute);
|
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store user using their username as the identifier
|
// Deal with errors trying to follow referrals
|
||||||
String identifier = username.getStringValue();
|
catch (LDAPReferralException e) {
|
||||||
if (users.put(identifier, new SimpleUser(identifier)) != null)
|
if (confService.getFollowReferrals()) {
|
||||||
logger.warn("Possibly ambiguous user account: \"{}\".", identifier);
|
logger.error("Could not follow referral: {}", e.getFailedReferral());
|
||||||
|
logger.debug("Error encountered trying to follow referral.", e);
|
||||||
|
throw new GuacamoleServerException("Could not follow LDAP referral.", e);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.warn("Given a referral, but referrals are disabled. Error was: {}", e.getMessage());
|
||||||
|
logger.debug("Got a referral, but configured to not follow them.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,8 +285,23 @@ public class UserService {
|
|||||||
|
|
||||||
// Add all DNs for found users
|
// Add all DNs for found users
|
||||||
while (results.hasMore()) {
|
while (results.hasMore()) {
|
||||||
LDAPEntry entry = results.next();
|
try {
|
||||||
userDNs.add(entry.getDN());
|
LDAPEntry entry = results.next();
|
||||||
|
userDNs.add(entry.getDN());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal with errors following referrals
|
||||||
|
catch (LDAPReferralException e) {
|
||||||
|
if (confService.getFollowReferrals()) {
|
||||||
|
logger.error("Error trying to follow a referral: {}", e.getFailedReferral());
|
||||||
|
logger.debug("Encountered an error trying to follow a referral.", e);
|
||||||
|
throw new GuacamoleServerException("Failed while trying to follow referrals.", e);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
logger.warn("Given a referral, not following it. Error was: {}", e.getMessage());
|
||||||
|
logger.debug("Given a referral, but configured to not follow them.", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return all discovered DNs (if any)
|
// Return all discovered DNs (if any)
|
||||||
|
Reference in New Issue
Block a user