From 2cff5b3b021f87289e4880f9bbec81320f5643de Mon Sep 17 00:00:00 2001 From: Shane Freeder Date: Thu, 9 Dec 2021 18:46:11 +0000 Subject: [PATCH] Backport log4j 2.15.0 bugfix --- proxy/build.gradle | 2 + .../log4j/core/appender/mom/JmsAppender.java | 310 +++++++++++++++++ .../logging/log4j/core/net/JndiManager.java | 317 ++++++++++++++++++ .../logging/log4j/core/util/NetUtils.java | 232 +++++++++++++ 4 files changed, 861 insertions(+) create mode 100644 proxy/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java create mode 100644 proxy/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java create mode 100644 proxy/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java diff --git a/proxy/build.gradle b/proxy/build.gradle index fd38a2004..38d577d34 100644 --- a/proxy/build.gradle +++ b/proxy/build.gradle @@ -37,6 +37,8 @@ shadowJar { tasks.withType(Checkstyle) { exclude('**/com/velocitypowered/proxy/protocol/packet/*.java') + // TODO: Remove during l4j2 2.15 update + exclude('**/org/apache/logging/log4j/core/**/*.java') } dependencies { diff --git a/proxy/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java b/proxy/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java new file mode 100644 index 000000000..d3f0d49b8 --- /dev/null +++ b/proxy/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java @@ -0,0 +1,310 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * 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.logging.log4j.core.appender.mom; + +import java.io.Serializable; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import org.apache.logging.log4j.core.Appender; +import org.apache.logging.log4j.core.Filter; +import org.apache.logging.log4j.core.Layout; +import org.apache.logging.log4j.core.LogEvent; +import org.apache.logging.log4j.core.appender.AbstractAppender; +import org.apache.logging.log4j.core.appender.AbstractManager; +import org.apache.logging.log4j.core.appender.mom.JmsManager.JmsManagerConfiguration; +import org.apache.logging.log4j.core.config.Node; +import org.apache.logging.log4j.core.config.Property; +import org.apache.logging.log4j.core.config.plugins.Plugin; +import org.apache.logging.log4j.core.config.plugins.PluginAliases; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute; +import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory; +import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required; +import org.apache.logging.log4j.core.net.JndiManager; + +/** + * Generic JMS Appender plugin for both queues and topics. This Appender replaces the previous split ones. However, + * configurations set up for the 2.0 version of the JMS appenders will still work. + */ +@Plugin(name = "JMS", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true) +@PluginAliases({ "JMSQueue", "JMSTopic" }) +public class JmsAppender extends AbstractAppender { + + public static class Builder> extends AbstractAppender.Builder + implements org.apache.logging.log4j.core.util.Builder { + + public static final int DEFAULT_RECONNECT_INTERVAL_MILLIS = 5000; + + @PluginBuilderAttribute + private String factoryName; + + @PluginBuilderAttribute + private String providerUrl; + + @PluginBuilderAttribute + private String urlPkgPrefixes; + + @PluginBuilderAttribute + private String securityPrincipalName; + + @PluginBuilderAttribute(sensitive = true) + private String securityCredentials; + + @PluginBuilderAttribute + @Required(message = "A javax.jms.ConnectionFactory JNDI name must be specified") + private String factoryBindingName; + + @PluginBuilderAttribute + @PluginAliases({ "queueBindingName", "topicBindingName" }) + @Required(message = "A javax.jms.Destination JNDI name must be specified") + private String destinationBindingName; + + @PluginBuilderAttribute + private String userName; + + @PluginBuilderAttribute(sensitive = true) + private char[] password; + + @PluginBuilderAttribute + private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL_MILLIS; + + @PluginBuilderAttribute + private boolean immediateFail; + + @PluginBuilderAttribute + private String allowedLdapClasses; + + @PluginBuilderAttribute + private String allowedLdapHosts; + + @PluginBuilderAttribute + private String allowedJndiProtocols; + + // Programmatic access only for now. + private JmsManager jmsManager; + + private Builder() { + } + + @SuppressWarnings("resource") // actualJmsManager and jndiManager are managed by the JmsAppender + @Override + public JmsAppender build() { + JmsManager actualJmsManager = jmsManager; + JmsManagerConfiguration configuration = null; + if (actualJmsManager == null) { + Properties additionalProperties = null; + if (allowedLdapClasses != null || allowedLdapHosts != null) { + additionalProperties = new Properties(); + if (allowedLdapHosts != null) { + additionalProperties.put(JndiManager.ALLOWED_HOSTS, allowedLdapHosts); + } + if (allowedLdapClasses != null) { + additionalProperties.put(JndiManager.ALLOWED_CLASSES, allowedLdapClasses); + } + if (allowedJndiProtocols != null) { + additionalProperties.put(JndiManager.ALLOWED_PROTOCOLS, allowedJndiProtocols); + } + } + final Properties jndiProperties = JndiManager.createProperties(factoryName, providerUrl, urlPkgPrefixes, + securityPrincipalName, securityCredentials, additionalProperties); + configuration = new JmsManagerConfiguration(jndiProperties, factoryBindingName, destinationBindingName, + userName, password, false, reconnectIntervalMillis); + actualJmsManager = AbstractManager.getManager(getName(), JmsManager.FACTORY, configuration); + } + if (actualJmsManager == null) { + // JmsManagerFactory has already logged an ERROR. + return null; + } + final Layout layout = getLayout(); + if (layout == null) { + LOGGER.error("No layout provided for JmsAppender"); + return null; + } + //try { + return new JmsAppender(getName(), getFilter(), layout, isIgnoreExceptions(), getPropertyArray(), + actualJmsManager); + /*} catch (final JMSException e) { + // Never happens since the ctor no longer actually throws a JMSException. + throw new IllegalStateException(e); + }*/ + } + + public Builder setDestinationBindingName(final String destinationBindingName) { + this.destinationBindingName = destinationBindingName; + return this; + } + + public Builder setFactoryBindingName(final String factoryBindingName) { + this.factoryBindingName = factoryBindingName; + return this; + } + + public Builder setFactoryName(final String factoryName) { + this.factoryName = factoryName; + return this; + } + + public Builder setImmediateFail(final boolean immediateFail) { + this.immediateFail = immediateFail; + return this; + } + + public Builder setJmsManager(final JmsManager jmsManager) { + this.jmsManager = jmsManager; + return this; + } + + public Builder setPassword(final char[] password) { + this.password = password; + return this; + } + + /** + * @deprecated Use setPassword(char[]) + */ + @Deprecated + public Builder setPassword(final String password) { + this.password = password == null ? null : password.toCharArray(); + return this; + } + + public Builder setProviderUrl(final String providerUrl) { + this.providerUrl = providerUrl; + return this; + } + + public Builder setReconnectIntervalMillis(final long reconnectIntervalMillis) { + this.reconnectIntervalMillis = reconnectIntervalMillis; + return this; + } + + public Builder setSecurityCredentials(final String securityCredentials) { + this.securityCredentials = securityCredentials; + return this; + } + + public Builder setSecurityPrincipalName(final String securityPrincipalName) { + this.securityPrincipalName = securityPrincipalName; + return this; + } + + public Builder setUrlPkgPrefixes(final String urlPkgPrefixes) { + this.urlPkgPrefixes = urlPkgPrefixes; + return this; + } + + /** + * @deprecated Use {@link #setUserName(String)}. + */ + @Deprecated + public Builder setUsername(final String username) { + this.userName = username; + return this; + } + + public Builder setUserName(final String userName) { + this.userName = userName; + return this; + } + + public Builder setAllowedLdapClasses(final String allowedLdapClasses) { + this.allowedLdapClasses = allowedLdapClasses; + return this; + } + + public Builder setAllowedLdapHosts(final String allowedLdapHosts) { + this.allowedLdapHosts = allowedLdapHosts; + return this; + } + + public Builder setAllowedJndiProtocols(final String allowedJndiProtocols) { + this.allowedJndiProtocols = allowedJndiProtocols; + return this; + } + + /** + * Does not include the password. + */ + @Override + public String toString() { + return "Builder [name=" + getName() + ", factoryName=" + factoryName + ", providerUrl=" + providerUrl + + ", urlPkgPrefixes=" + urlPkgPrefixes + ", securityPrincipalName=" + securityPrincipalName + + ", securityCredentials=" + securityCredentials + ", factoryBindingName=" + factoryBindingName + + ", destinationBindingName=" + destinationBindingName + ", username=" + userName + ", layout=" + + getLayout() + ", filter=" + getFilter() + ", ignoreExceptions=" + isIgnoreExceptions() + + ", jmsManager=" + jmsManager + ", allowedLdapClasses=" + allowedLdapClasses + + ", allowedLdapHosts=" + allowedLdapHosts + ", allowedJndiProtocols=" + allowedJndiProtocols + "]"; + } + + } + + @PluginBuilderFactory + public static Builder newBuilder() { + return new Builder(); + } + + private volatile JmsManager manager; + + protected JmsAppender(final String name, final Filter filter, final Layout layout, + final boolean ignoreExceptions, final Property[] properties, final JmsManager manager) /*throws JMSException*/ { + super(name, filter, layout, ignoreExceptions, properties); + this.manager = manager; + } + + @Deprecated + protected JmsAppender(final String name, final Filter filter, final Layout layout, + final boolean ignoreExceptions, final JmsManager manager) /*throws JMSException*/ { + super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY); + this.manager = manager; + } + + @Override + public void append(final LogEvent event) { + this.manager.send(event, toSerializable(event)); + } + + public JmsManager getManager() { + return manager; + } + + @Override + public boolean stop(final long timeout, final TimeUnit timeUnit) { + setStopping(); + boolean stopped = super.stop(timeout, timeUnit, false); + stopped &= this.manager.stop(timeout, timeUnit); + setStopped(); + return stopped; + } + +} diff --git a/proxy/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java b/proxy/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java new file mode 100644 index 000000000..5c4b45067 --- /dev/null +++ b/proxy/src/main/java/org/apache/logging/log4j/core/net/JndiManager.java @@ -0,0 +1,317 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * 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.logging.log4j.core.net; + +import java.net.URI; +import java.net.URISyntaxException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Properties; +import java.util.concurrent.TimeUnit; + +import javax.naming.Context; +import javax.naming.NamingEnumeration; +import javax.naming.NamingException; +import javax.naming.directory.Attribute; +import javax.naming.directory.Attributes; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; + +import org.apache.logging.log4j.core.appender.AbstractManager; +import org.apache.logging.log4j.core.appender.ManagerFactory; +import org.apache.logging.log4j.core.util.JndiCloser; +import org.apache.logging.log4j.core.util.NetUtils; +import org.apache.logging.log4j.util.PropertiesUtil; + +/** + * Manages a JNDI {@link javax.naming.directory.DirContext}. + * + * @since 2.1 + */ +public class JndiManager extends AbstractManager { + + public static final String ALLOWED_HOSTS = "allowedLdapHosts"; + public static final String ALLOWED_CLASSES = "allowedLdapClasses"; + public static final String ALLOWED_PROTOCOLS = "allowedJndiProtocols"; + + private static final JndiManagerFactory FACTORY = new JndiManagerFactory(); + private static final String PREFIX = "log4j2."; + private static final String LDAP = "ldap"; + private static final String LDAPS = "ldaps"; + private static final String JAVA = "java"; + private static final List permanentAllowedHosts = NetUtils.getLocalIps(); + private static final List permanentAllowedClasses = Arrays.asList(Boolean.class.getName(), + Byte.class.getName(), Character.class.getName(), Double.class.getName(), Float.class.getName(), + Integer.class.getName(), Long.class.getName(), Short.class.getName(), String.class.getName()); + private static final List permanentAllowedProtocols = Arrays.asList(JAVA, LDAP, LDAPS); + private static final String SERIALIZED_DATA = "javaSerializedData"; + private static final String CLASS_NAME = "javaClassName"; + private static final String REFERENCE_ADDRESS = "javaReferenceAddress"; + private static final String OBJECT_FACTORY = "javaFactory"; + private final List allowedHosts; + private final List allowedClasses; + private final List allowedProtocols; + + private final DirContext context; + + private JndiManager(final String name, final DirContext context, final List allowedHosts, + final List allowedClasses, final List allowedProtocols) { + super(null, name); + this.context = context; + this.allowedHosts = allowedHosts; + this.allowedClasses = allowedClasses; + this.allowedProtocols = allowedProtocols; + } + + /** + * Gets the default JndiManager using the default {@link javax.naming.InitialContext}. + * + * @return the default JndiManager + */ + public static JndiManager getDefaultManager() { + return getManager(JndiManager.class.getName(), FACTORY, null); + } + + /** + * Gets a named JndiManager using the default {@link javax.naming.InitialContext}. + * + * @param name the name of the JndiManager instance to create or use if available + * @return a default JndiManager + */ + public static JndiManager getDefaultManager(final String name) { + return getManager(name, FACTORY, null); + } + + /** + * Gets a JndiManager with the provided configuration information. + * + * @param initialContextFactoryName Fully qualified class name of an implementation of + * {@link javax.naming.spi.InitialContextFactory}. + * @param providerURL The provider URL to use for the JNDI connection (specific to the above factory). + * @param urlPkgPrefixes A colon-separated list of package prefixes for the class name of the factory + * class that will create a URL context factory + * @param securityPrincipal The name of the identity of the Principal. + * @param securityCredentials The security credentials of the Principal. + * @param additionalProperties Any additional JNDI environment properties to set or {@code null} for none. + * @return the JndiManager for the provided parameters. + */ + public static JndiManager getJndiManager(final String initialContextFactoryName, + final String providerURL, + final String urlPkgPrefixes, + final String securityPrincipal, + final String securityCredentials, + final Properties additionalProperties) { + final Properties properties = createProperties(initialContextFactoryName, providerURL, urlPkgPrefixes, + securityPrincipal, securityCredentials, additionalProperties); + return getManager(createManagerName(), FACTORY, properties); + } + + /** + * Gets a JndiManager with the provided configuration information. + * + * @param properties JNDI properties, usually created by calling {@link #createProperties(String, String, String, String, String, Properties)}. + * @return the JndiManager for the provided parameters. + * @see #createProperties(String, String, String, String, String, Properties) + * @since 2.9 + */ + public static JndiManager getJndiManager(final Properties properties) { + return getManager(createManagerName(), FACTORY, properties); + } + + private static String createManagerName() { + return JndiManager.class.getName() + '@' + JndiManager.class.hashCode(); + } + + /** + * Creates JNDI Properties with the provided configuration information. + * + * @param initialContextFactoryName + * Fully qualified class name of an implementation of {@link javax.naming.spi.InitialContextFactory}. + * @param providerURL + * The provider URL to use for the JNDI connection (specific to the above factory). + * @param urlPkgPrefixes + * A colon-separated list of package prefixes for the class name of the factory class that will create a + * URL context factory + * @param securityPrincipal + * The name of the identity of the Principal. + * @param securityCredentials + * The security credentials of the Principal. + * @param additionalProperties + * Any additional JNDI environment properties to set or {@code null} for none. + * @return the Properties for the provided parameters. + * @since 2.9 + */ + public static Properties createProperties(final String initialContextFactoryName, final String providerURL, + final String urlPkgPrefixes, final String securityPrincipal, final String securityCredentials, + final Properties additionalProperties) { + if (initialContextFactoryName == null) { + return null; + } + final Properties properties = new Properties(); + properties.setProperty(Context.INITIAL_CONTEXT_FACTORY, initialContextFactoryName); + if (providerURL != null) { + properties.setProperty(Context.PROVIDER_URL, providerURL); + } else { + LOGGER.warn("The JNDI InitialContextFactory class name [{}] was provided, but there was no associated " + + "provider URL. This is likely to cause problems.", initialContextFactoryName); + } + if (urlPkgPrefixes != null) { + properties.setProperty(Context.URL_PKG_PREFIXES, urlPkgPrefixes); + } + if (securityPrincipal != null) { + properties.setProperty(Context.SECURITY_PRINCIPAL, securityPrincipal); + if (securityCredentials != null) { + properties.setProperty(Context.SECURITY_CREDENTIALS, securityCredentials); + } else { + LOGGER.warn("A security principal [{}] was provided, but with no corresponding security credentials.", + securityPrincipal); + } + } + if (additionalProperties != null) { + properties.putAll(additionalProperties); + } + return properties; + } + + @Override + protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) { + return JndiCloser.closeSilently(this.context); + } + + /** + * Looks up a named object through this JNDI context. + * + * @param name name of the object to look up. + * @param the type of the object. + * @return the named object if it could be located. + * @throws NamingException if a naming exception is encountered + */ + @SuppressWarnings("unchecked") + public synchronized T lookup(final String name) throws NamingException { + try { + URI uri = new URI(name); + if (uri.getScheme() != null) { + if (!allowedProtocols.contains(uri.getScheme().toLowerCase(Locale.ROOT))) { + LOGGER.warn("Log4j JNDI does not allow protocol {}", uri.getScheme()); + return null; + } + if (LDAP.equalsIgnoreCase(uri.getScheme()) || LDAPS.equalsIgnoreCase(uri.getScheme())) { + if (!allowedHosts.contains(uri.getHost())) { + LOGGER.warn("Attempt to access ldap server not in allowed list"); + return null; + } + Attributes attributes = this.context.getAttributes(name); + if (attributes != null) { + // In testing the "key" for attributes seems to be lowercase while the attribute id is + // camelcase, but that may just be true for the test LDAP used here. This copies the Attributes + // to a Map ignoring the "key" and using the Attribute's id as the key in the Map so it matches + // the Java schema. + Map attributeMap = new HashMap<>(); + NamingEnumeration enumeration = attributes.getAll(); + while (enumeration.hasMore()) { + Attribute attribute = enumeration.next(); + attributeMap.put(attribute.getID(), attribute); + } + Attribute classNameAttr = attributeMap.get(CLASS_NAME); + if (attributeMap.get(SERIALIZED_DATA) != null) { + if (classNameAttr != null) { + String className = classNameAttr.get().toString(); + if (!allowedClasses.contains(className)) { + LOGGER.warn("Deserialization of {} is not allowed", className); + return null; + } + } else { + LOGGER.warn("No class name provided for {}", name); + return null; + } + } else if (attributeMap.get(REFERENCE_ADDRESS) != null + || attributeMap.get(OBJECT_FACTORY) != null) { + LOGGER.warn("Referenceable class is not allowed for {}", name); + return null; + } + } + } + } + } catch (URISyntaxException ex) { + // This is OK. + } + return (T) this.context.lookup(name); + } + + private static class JndiManagerFactory implements ManagerFactory { + + @Override + public JndiManager createManager(final String name, final Properties data) { + String hosts = data != null ? data.getProperty(ALLOWED_HOSTS) : null; + String classes = data != null ? data.getProperty(ALLOWED_CLASSES) : null; + String protocols = data != null ? data.getProperty(ALLOWED_PROTOCOLS) : null; + List allowedHosts = new ArrayList<>(); + List allowedClasses = new ArrayList<>(); + List allowedProtocols = new ArrayList<>(); + addAll(hosts, allowedHosts, permanentAllowedHosts, ALLOWED_HOSTS, data); + addAll(classes, allowedClasses, permanentAllowedClasses, ALLOWED_CLASSES, data); + addAll(protocols, allowedProtocols, permanentAllowedProtocols, ALLOWED_PROTOCOLS, data); + try { + return new JndiManager(name, new InitialDirContext(data), allowedHosts, allowedClasses, + allowedProtocols); + } catch (final NamingException e) { + LOGGER.error("Error creating JNDI InitialContext.", e); + return null; + } + } + + private void addAll(String toSplit, List list, List permanentList, String propertyName, + Properties data) { + if (toSplit != null) { + list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*"))); + data.remove(propertyName); + } + toSplit = PropertiesUtil.getProperties().getStringProperty(PREFIX + propertyName); + if (toSplit != null) { + list.addAll(Arrays.asList(toSplit.split("\\s*,\\s*"))); + } + list.addAll(permanentList); + } + } + + @Override + public String toString() { + return "JndiManager [context=" + context + ", count=" + count + "]"; + } + +} diff --git a/proxy/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java b/proxy/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java new file mode 100644 index 000000000..dd4d1ef33 --- /dev/null +++ b/proxy/src/main/java/org/apache/logging/log4j/core/util/NetUtils.java @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2018 Velocity Contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +/* + * 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.logging.log4j.core.util; + +import java.io.File; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.MalformedURLException; +import java.net.NetworkInterface; +import java.net.SocketException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Enumeration; +import java.util.List; + +import org.apache.logging.log4j.Logger; +import org.apache.logging.log4j.status.StatusLogger; +import org.apache.logging.log4j.util.Strings; + +/** + * Networking-related convenience methods. + */ +public final class NetUtils { + + private static final Logger LOGGER = StatusLogger.getLogger(); + private static final String UNKNOWN_LOCALHOST = "UNKNOWN_LOCALHOST"; + + private NetUtils() { + // empty + } + + /** + * This method gets the network name of the machine we are running on. Returns "UNKNOWN_LOCALHOST" in the unlikely + * case where the host name cannot be found. + * + * @return String the name of the local host + */ + public static String getLocalHostname() { + try { + final InetAddress addr = InetAddress.getLocalHost(); + return addr == null ? UNKNOWN_LOCALHOST : addr.getHostName(); + } catch (final UnknownHostException uhe) { + try { + final Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + final NetworkInterface nic = interfaces.nextElement(); + final Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + final InetAddress address = addresses.nextElement(); + if (!address.isLoopbackAddress()) { + final String hostname = address.getHostName(); + if (hostname != null) { + return hostname; + } + } + } + } + } + } catch (final SocketException se) { + // ignore and log below. + } + LOGGER.error("Could not determine local host name", uhe); + return UNKNOWN_LOCALHOST; + } + } + + /** + * Returns all the local host names and ip addresses. + * @return The local host names and ip addresses. + */ + public static List getLocalIps() { + List localIps = new ArrayList<>(); + localIps.add("localhost"); + localIps.add("127.0.0.1"); + try { + final InetAddress addr = Inet4Address.getLocalHost(); + setHostName(addr, localIps); + } catch (final UnknownHostException ex) { + // Ignore this. + } + try { + final Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); + if (interfaces != null) { + while (interfaces.hasMoreElements()) { + final NetworkInterface nic = interfaces.nextElement(); + final Enumeration addresses = nic.getInetAddresses(); + while (addresses.hasMoreElements()) { + final InetAddress address = addresses.nextElement(); + setHostName(address, localIps); + } + } + } + } catch (final SocketException se) { + // ignore. + } + return localIps; + } + + private static void setHostName(InetAddress address, List localIps) { + String[] parts = address.toString().split("\\s*/\\s*"); + if (parts.length > 0) { + for (String part : parts) { + if (Strings.isNotBlank(part) && !localIps.contains(part)) { + localIps.add(part); + } + } + } + } + + /** + * Returns the local network interface's MAC address if possible. The local network interface is defined here as + * the {@link java.net.NetworkInterface} that is both up and not a loopback interface. + * + * @return the MAC address of the local network interface or {@code null} if no MAC address could be determined. + */ + public static byte[] getMacAddress() { + byte[] mac = null; + try { + final InetAddress localHost = InetAddress.getLocalHost(); + try { + final NetworkInterface localInterface = NetworkInterface.getByInetAddress(localHost); + if (isUpAndNotLoopback(localInterface)) { + mac = localInterface.getHardwareAddress(); + } + if (mac == null) { + final Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + if (networkInterfaces != null) { + while (networkInterfaces.hasMoreElements() && mac == null) { + final NetworkInterface nic = networkInterfaces.nextElement(); + if (isUpAndNotLoopback(nic)) { + mac = nic.getHardwareAddress(); + } + } + } + } + } catch (final SocketException e) { + LOGGER.catching(e); + } + if (ArrayUtils.isEmpty(mac) && localHost != null) { + // Emulate a MAC address with an IP v4 or v6 + final byte[] address = localHost.getAddress(); + // Take only 6 bytes if the address is an IPv6 otherwise will pad with two zero bytes + mac = Arrays.copyOf(address, 6); + } + } catch (final UnknownHostException ignored) { + // ignored + } + return mac; + } + + /** + * Returns the mac address, if it is available, as a string with each byte separated by a ":" character. + * @return the mac address String or null. + */ + public static String getMacAddressString() { + final byte[] macAddr = getMacAddress(); + if (!ArrayUtils.isEmpty(macAddr)) { + StringBuilder sb = new StringBuilder(String.format("%02x", macAddr[0])); + for (int i = 1; i < macAddr.length; ++i) { + sb.append(":").append(String.format("%02x", macAddr[i])); + } + return sb.toString(); + + } + return null; + } + + private static boolean isUpAndNotLoopback(final NetworkInterface ni) throws SocketException { + return ni != null && !ni.isLoopback() && ni.isUp(); + } + + /** + * Converts a URI string or file path to a URI object. + * + * @param path the URI string or path + * @return the URI object + */ + public static URI toURI(final String path) { + try { + // Resolves absolute URI + return new URI(path); + } catch (final URISyntaxException e) { + // A file path or a Apache Commons VFS URL might contain blanks. + // A file path may start with a driver letter + try { + final URL url = new URL(path); + return new URI(url.getProtocol(), url.getHost(), url.getPath(), null); + } catch (MalformedURLException | URISyntaxException nestedEx) { + return new File(path).toURI(); + } + } + } + +}