001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements. See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache license, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License. You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the license for the specific language governing permissions and
015 * limitations under the license.
016 */
017package org.apache.logging.log4j.core.net;
018
019import java.lang.reflect.InvocationTargetException;
020import java.lang.reflect.Method;
021import java.util.HashMap;
022import java.util.Hashtable;
023import java.util.Map;
024
025import org.apache.logging.log4j.Logger;
026import org.apache.logging.log4j.core.Core;
027import org.apache.logging.log4j.core.config.plugins.Plugin;
028import org.apache.logging.log4j.core.util.Integers;
029import org.apache.logging.log4j.status.StatusLogger;
030import org.apache.logging.log4j.util.LoaderUtil;
031
032/**
033 * Advertise an entity via ZeroConf/MulticastDNS and the JmDNS library.
034 *
035 * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255 bytes
036 * will be removed prior to advertisement.
037 *
038 */
039@Plugin(name = "multicastdns", category = Core.CATEGORY_NAME, elementType = "advertiser", printObject = false)
040public class MulticastDnsAdvertiser implements Advertiser {
041    /**
042     * Status logger.
043     */
044    protected static final Logger LOGGER = StatusLogger.getLogger();
045
046    private static final int MAX_LENGTH = 255;
047    private static final int DEFAULT_PORT = 4555;
048
049    private static Object jmDNS = initializeJmDns();
050    private static Class<?> jmDNSClass;
051    private static Class<?> serviceInfoClass;
052
053    public MulticastDnsAdvertiser() {
054        // no arg constructor for reflection
055    }
056
057    /**
058     * Advertise the provided entity.
059     *
060     * Properties map provided in advertise method must include a "name" entry but may also provide "protocol" (tcp/udp)
061     * as well as a "port" entry
062     *
063     * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255
064     * bytes will be removed prior to advertisement.
065     *
066     * @param properties the properties representing the entity to advertise
067     * @return the object which can be used to unadvertise, or null if advertisement was unsuccessful
068     */
069    @Override
070    public Object advertise(final Map<String, String> properties) {
071        // default to tcp if "protocol" was not set
072        final Map<String, String> truncatedProperties = new HashMap<>();
073        for (final Map.Entry<String, String> entry : properties.entrySet()) {
074            if (entry.getKey().length() <= MAX_LENGTH && entry.getValue().length() <= MAX_LENGTH) {
075                truncatedProperties.put(entry.getKey(), entry.getValue());
076            }
077        }
078        final String protocol = truncatedProperties.get("protocol");
079        final String zone = "._log4j._" + (protocol != null ? protocol : "tcp") + ".local.";
080        // default to 4555 if "port" was not set
081        final String portString = truncatedProperties.get("port");
082        final int port = Integers.parseInt(portString, DEFAULT_PORT);
083
084        final String name = truncatedProperties.get("name");
085
086        // if version 3 is available, use it to construct a serviceInfo instance, otherwise support the version1 API
087        if (jmDNS != null) {
088            boolean isVersion3 = false;
089            try {
090                // create method is in version 3, not version 1
091                jmDNSClass.getMethod("create");
092                isVersion3 = true;
093            } catch (final NoSuchMethodException e) {
094                // no-op
095            }
096            Object serviceInfo;
097            if (isVersion3) {
098                serviceInfo = buildServiceInfoVersion3(zone, port, name, truncatedProperties);
099            } else {
100                serviceInfo = buildServiceInfoVersion1(zone, port, name, truncatedProperties);
101            }
102
103            try {
104                final Method method = jmDNSClass.getMethod("registerService", serviceInfoClass);
105                method.invoke(jmDNS, serviceInfo);
106            } catch (final IllegalAccessException | InvocationTargetException e) {
107                LOGGER.warn("Unable to invoke registerService method", e);
108            } catch (final NoSuchMethodException e) {
109                LOGGER.warn("No registerService method", e);
110            }
111            return serviceInfo;
112        }
113        LOGGER.warn("JMDNS not available - will not advertise ZeroConf support");
114        return null;
115    }
116
117    /**
118     * Unadvertise the previously advertised entity.
119     *
120     * @param serviceInfo
121     */
122    @Override
123    public void unadvertise(final Object serviceInfo) {
124        if (jmDNS != null) {
125            try {
126                final Method method = jmDNSClass.getMethod("unregisterService", serviceInfoClass);
127                method.invoke(jmDNS, serviceInfo);
128            } catch (final IllegalAccessException | InvocationTargetException e) {
129                LOGGER.warn("Unable to invoke unregisterService method", e);
130            } catch (final NoSuchMethodException e) {
131                LOGGER.warn("No unregisterService method", e);
132            }
133        }
134    }
135
136    private static Object createJmDnsVersion1() {
137        try {
138            return jmDNSClass.getConstructor().newInstance();
139        } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
140            LOGGER.warn("Unable to instantiate JMDNS", e);
141        }
142        return null;
143    }
144
145    private static Object createJmDnsVersion3() {
146        try {
147            final Method jmDNSCreateMethod = jmDNSClass.getMethod("create");
148            return jmDNSCreateMethod.invoke(null, (Object[]) null);
149        } catch (final IllegalAccessException | InvocationTargetException e) {
150            LOGGER.warn("Unable to invoke create method", e);
151        } catch (final NoSuchMethodException e) {
152            LOGGER.warn("Unable to get create method", e);
153        }
154        return null;
155    }
156
157    private static Object buildServiceInfoVersion1(final String zone, final int port, final String name,
158            final Map<String, String> properties) {
159        // version 1 uses a hashtable
160        @SuppressWarnings("UseOfObsoleteCollectionType")
161        final Hashtable<String, String> hashtableProperties = new Hashtable<>(properties);
162        try {
163            return serviceInfoClass.getConstructor(String.class, String.class, int.class, int.class, int.class,
164                    Hashtable.class).newInstance(zone, name, port, 0, 0, hashtableProperties);
165        } catch (final IllegalAccessException | InstantiationException | InvocationTargetException e) {
166            LOGGER.warn("Unable to construct ServiceInfo instance", e);
167        } catch (final NoSuchMethodException e) {
168            LOGGER.warn("Unable to get ServiceInfo constructor", e);
169        }
170        return null;
171    }
172
173    private static Object buildServiceInfoVersion3(final String zone, final int port, final String name,
174            final Map<String, String> properties) {
175        try {
176            return serviceInfoClass
177                    // zone/type display name port weight priority properties
178                    .getMethod("create", String.class, String.class, int.class, int.class, int.class, Map.class)
179                    .invoke(null, zone, name, port, 0, 0, properties);
180        } catch (final IllegalAccessException | InvocationTargetException e) {
181            LOGGER.warn("Unable to invoke create method", e);
182        } catch (final NoSuchMethodException e) {
183            LOGGER.warn("Unable to find create method", e);
184        }
185        return null;
186    }
187
188    private static Object initializeJmDns() {
189        try {
190            jmDNSClass = LoaderUtil.loadClass("javax.jmdns.JmDNS");
191            serviceInfoClass = LoaderUtil.loadClass("javax.jmdns.ServiceInfo");
192            // if version 3 is available, use it to constuct a serviceInfo instance, otherwise support the version1 API
193            boolean isVersion3 = false;
194            try {
195                // create method is in version 3, not version 1
196                jmDNSClass.getMethod("create");
197                isVersion3 = true;
198            } catch (final NoSuchMethodException e) {
199                // no-op
200            }
201
202            if (isVersion3) {
203                return createJmDnsVersion3();
204            }
205            return createJmDnsVersion1();
206        } catch (final ClassNotFoundException | ExceptionInInitializerError e) {
207            LOGGER.warn("JmDNS or serviceInfo class not found", e);
208        }
209        return null;
210    }
211}