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.config.plugins.Plugin;
027import org.apache.logging.log4j.core.util.Integers;
028import org.apache.logging.log4j.core.util.Loader;
029import org.apache.logging.log4j.status.StatusLogger;
030
031/**
032 * Advertise an entity via ZeroConf/MulticastDNS and the JmDNS library.
033 *
034 * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255 bytes
035 * will be removed prior to advertisement.
036 *
037 */
038@Plugin(name = "multicastdns", category = "Core", elementType = "advertiser", printObject = false)
039public class MulticastDnsAdvertiser implements Advertiser {
040    /**
041     * 
042     */
043    private static final int MAX_LENGTH = 255;
044    /**
045     * Status logger.
046     */
047    protected static final Logger LOGGER = StatusLogger.getLogger();
048    private static final int DEFAULT_PORT = 4555;
049
050    private static Object jmDNS = initializeJmDns();
051    private static Class<?> jmDNSClass;
052    private static Class<?> serviceInfoClass;
053
054    public MulticastDnsAdvertiser() {
055        // no arg constructor for reflection
056    }
057
058    /**
059     * Advertise the provided entity.
060     *
061     * Properties map provided in advertise method must include a "name" entry but may also provide "protocol" (tcp/udp)
062     * as well as a "port" entry
063     *
064     * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255
065     * bytes will be removed prior to advertisement.
066     *
067     * @param properties the properties representing the entity to advertise
068     * @return the object which can be used to unadvertise, or null if advertisement was unsuccessful
069     */
070    @Override
071    public Object advertise(final Map<String, String> properties) {
072        // default to tcp if "protocol" was not set
073        final Map<String, String> truncatedProperties = new HashMap<>();
074        for (final Map.Entry<String, String> entry : properties.entrySet()) {
075            if (entry.getKey().length() <= MAX_LENGTH && entry.getValue().length() <= MAX_LENGTH) {
076                truncatedProperties.put(entry.getKey(), entry.getValue());
077            }
078        }
079        final String protocol = truncatedProperties.get("protocol");
080        final String zone = "._log4j._" + (protocol != null ? protocol : "tcp") + ".local.";
081        // default to 4555 if "port" was not set
082        final String portString = truncatedProperties.get("port");
083        final int port = Integers.parseInt(portString, DEFAULT_PORT);
084
085        final String name = truncatedProperties.get("name");
086
087        // if version 3 is available, use it to construct a serviceInfo instance, otherwise support the version1 API
088        if (jmDNS != null) {
089            boolean isVersion3 = false;
090            try {
091                // create method is in version 3, not version 1
092                jmDNSClass.getMethod("create");
093                isVersion3 = true;
094            } catch (final NoSuchMethodException e) {
095                // no-op
096            }
097            Object serviceInfo;
098            if (isVersion3) {
099                serviceInfo = buildServiceInfoVersion3(zone, port, name, truncatedProperties);
100            } else {
101                serviceInfo = buildServiceInfoVersion1(zone, port, name, truncatedProperties);
102            }
103
104            try {
105                final Method method = jmDNSClass.getMethod("registerService", serviceInfoClass);
106                method.invoke(jmDNS, serviceInfo);
107            } catch (final IllegalAccessException e) {
108                LOGGER.warn("Unable to invoke registerService method", e);
109            } catch (final NoSuchMethodException e) {
110                LOGGER.warn("No registerService method", e);
111            } catch (final InvocationTargetException e) {
112                LOGGER.warn("Unable to invoke registerService method", e);
113            }
114            return serviceInfo;
115        }
116        LOGGER.warn("JMDNS not available - will not advertise ZeroConf support");
117        return null;
118    }
119
120    /**
121     * Unadvertise the previously advertised entity
122     * 
123     * @param serviceInfo
124     */
125    @Override
126    public void unadvertise(final Object serviceInfo) {
127        if (jmDNS != null) {
128            try {
129                final Method method = jmDNSClass.getMethod("unregisterService", serviceInfoClass);
130                method.invoke(jmDNS, serviceInfo);
131            } catch (final IllegalAccessException e) {
132                LOGGER.warn("Unable to invoke unregisterService method", e);
133            } catch (final NoSuchMethodException e) {
134                LOGGER.warn("No unregisterService method", e);
135            } catch (final InvocationTargetException e) {
136                LOGGER.warn("Unable to invoke unregisterService method", e);
137            }
138        }
139    }
140
141    private static Object createJmDnsVersion1() {
142        try {
143            return jmDNSClass.getConstructor().newInstance();
144        } catch (final InstantiationException e) {
145            LOGGER.warn("Unable to instantiate JMDNS", e);
146        } catch (final IllegalAccessException e) {
147            LOGGER.warn("Unable to instantiate JMDNS", e);
148        } catch (final NoSuchMethodException e) {
149            LOGGER.warn("Unable to instantiate JMDNS", e);
150        } catch (final InvocationTargetException e) {
151            LOGGER.warn("Unable to instantiate JMDNS", e);
152        }
153        return null;
154    }
155
156    private static Object createJmDnsVersion3() {
157        try {
158            final Method jmDNSCreateMethod = jmDNSClass.getMethod("create");
159            return jmDNSCreateMethod.invoke(null, (Object[]) null);
160        } catch (final IllegalAccessException e) {
161            LOGGER.warn("Unable to invoke create method", e);
162        } catch (final NoSuchMethodException e) {
163            LOGGER.warn("Unable to get create method", e);
164        } catch (final InvocationTargetException e) {
165            LOGGER.warn("Unable to invoke create method", e);
166        }
167        return null;
168    }
169
170    private static Object buildServiceInfoVersion1(final String zone, final int port, final String name,
171            final Map<String, String> properties) {
172        // version 1 uses a hashtable
173        @SuppressWarnings("UseOfObsoleteCollectionType")
174        final Hashtable<String, String> hashtableProperties = new Hashtable<>(properties);
175        try {
176            return serviceInfoClass.getConstructor(String.class, String.class, int.class, int.class, int.class,
177                    Hashtable.class).newInstance(zone, name, port, 0, 0, hashtableProperties);
178        } catch (final IllegalAccessException e) {
179            LOGGER.warn("Unable to construct ServiceInfo instance", e);
180        } catch (final NoSuchMethodException e) {
181            LOGGER.warn("Unable to get ServiceInfo constructor", e);
182        } catch (final InstantiationException e) {
183            LOGGER.warn("Unable to construct ServiceInfo instance", e);
184        } catch (final InvocationTargetException e) {
185            LOGGER.warn("Unable to construct ServiceInfo instance", e);
186        }
187        return null;
188    }
189
190    private static Object buildServiceInfoVersion3(final String zone, final int port, final String name,
191            final Map<String, String> properties) {
192        try {
193            return serviceInfoClass
194                    // zone/type display name port weight priority properties
195                    .getMethod("create", String.class, String.class, int.class, int.class, int.class, Map.class)
196                    .invoke(null, zone, name, port, 0, 0, properties);
197        } catch (final IllegalAccessException e) {
198            LOGGER.warn("Unable to invoke create method", e);
199        } catch (final NoSuchMethodException e) {
200            LOGGER.warn("Unable to find create method", e);
201        } catch (final InvocationTargetException e) {
202            LOGGER.warn("Unable to invoke create method", e);
203        }
204        return null;
205    }
206
207    private static Object initializeJmDns() {
208        try {
209            jmDNSClass = Loader.loadClass("javax.jmdns.JmDNS");
210            serviceInfoClass = Loader.loadClass("javax.jmdns.ServiceInfo");
211            // if version 3 is available, use it to constuct a serviceInfo instance, otherwise support the version1 API
212            boolean isVersion3 = false;
213            try {
214                // create method is in version 3, not version 1
215                jmDNSClass.getMethod("create");
216                isVersion3 = true;
217            } catch (final NoSuchMethodException e) {
218                // no-op
219            }
220
221            if (isVersion3) {
222                return createJmDnsVersion3();
223            }
224            return createJmDnsVersion1();
225        } catch (final ClassNotFoundException e) {
226            LOGGER.warn("JmDNS or serviceInfo class not found", e);
227        } catch (final ExceptionInInitializerError e2) {
228            LOGGER.warn("JmDNS or serviceInfo class not found", e2);
229        }
230        return null;
231    }
232}