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