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.Constructor;
020import java.lang.reflect.InvocationTargetException;
021import java.lang.reflect.Method;
022import java.util.HashMap;
023import java.util.Hashtable;
024import java.util.Map;
025
026import org.apache.logging.log4j.Logger;
027import org.apache.logging.log4j.core.config.plugins.Plugin;
028import org.apache.logging.log4j.core.helpers.Integers;
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", (Class[])null);
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", new Class[]{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        else
113        {
114            LOGGER.warn("JMDNS not available - will not advertise ZeroConf support");
115            return null;
116        }
117    }
118
119    /**
120     * Unadvertise the previously advertised entity
121     * @param serviceInfo
122     */
123    @Override
124    public void unadvertise(final Object serviceInfo) {
125        if (jmDNS != null) {
126            try {
127                final Method method = jmDNSClass.getMethod("unregisterService", new Class[]{serviceInfoClass});
128                method.invoke(jmDNS, serviceInfo);
129            } catch(final IllegalAccessException e) {
130                LOGGER.warn("Unable to invoke unregisterService method", e);
131            } catch(final NoSuchMethodException e) {
132                LOGGER.warn("No unregisterService method", e);
133            } catch(final InvocationTargetException e) {
134                LOGGER.warn("Unable to invoke unregisterService method", e);
135            }
136        }
137    }
138
139    private static Object createJmDNSVersion1()
140    {
141        try {
142            return jmDNSClass.newInstance();
143        } catch (final InstantiationException e) {
144            LOGGER.warn("Unable to instantiate JMDNS", e);
145        } catch (final IllegalAccessException e) {
146            LOGGER.warn("Unable to instantiate JMDNS", e);
147        }
148        return null;
149    }
150
151    private static Object createJmDNSVersion3()
152    {
153        try {
154            final Method jmDNSCreateMethod = jmDNSClass.getMethod("create", (Class[])null);
155            return jmDNSCreateMethod.invoke(null, (Object[])null);
156        } catch (final IllegalAccessException e) {
157            LOGGER.warn("Unable to invoke create method", e);
158        } catch (final NoSuchMethodException e) {
159            LOGGER.warn("Unable to get create method", e);
160        } catch (final InvocationTargetException e) {
161            LOGGER.warn("Unable to invoke create method", e);
162        }
163        return null;
164    }
165
166    private Object buildServiceInfoVersion1(final String zone, final int port, final String name, final Map<String, String> properties) {
167        //version 1 uses a hashtable
168        final Hashtable<String, String> hashtableProperties = new Hashtable<String, String>(properties);
169        try {
170            final Class<?>[] args = new Class<?>[6];
171            args[0] = String.class;
172            args[1] = String.class;
173            args[2] = int.class;
174            args[3] = int.class; //weight (0)
175            args[4] = int.class; //priority (0)
176            args[5] = Hashtable.class;
177            final Constructor<?> constructor  = serviceInfoClass.getConstructor(args);
178            final Object[] values = new Object[6];
179            values[0] = zone;
180            values[1] = name;
181            values[2] = port;
182            values[3] = 0;
183            values[4] = 0;
184            values[5] = hashtableProperties;
185            return constructor.newInstance(values);
186        } catch (final IllegalAccessException e) {
187            LOGGER.warn("Unable to construct ServiceInfo instance", e);
188        } catch (final NoSuchMethodException e) {
189            LOGGER.warn("Unable to get ServiceInfo constructor", e);
190        } catch (final InstantiationException e) {
191            LOGGER.warn("Unable to construct ServiceInfo instance", e);
192        } catch (final InvocationTargetException e) {
193            LOGGER.warn("Unable to construct ServiceInfo instance", e);
194        }
195        return null;
196    }
197
198    private Object buildServiceInfoVersion3(final String zone, final int port, final String name, final Map<String, String> properties) {
199        try {
200            final Class<?>[] args = new Class<?>[6];
201            args[0] = String.class; //zone/type
202            args[1] = String.class; //display name
203            args[2] = int.class; //port
204            args[3] = int.class; //weight (0)
205            args[4] = int.class; //priority (0)
206            args[5] = Map.class;
207            final Method serviceInfoCreateMethod = serviceInfoClass.getMethod("create", args);
208            final Object[] values = new Object[6];
209            values[0] = zone;
210            values[1] = name;
211            values[2] = port;
212            values[3] = 0;
213            values[4] = 0;
214            values[5] = properties;
215            return serviceInfoCreateMethod.invoke(null, values);
216        } catch (final IllegalAccessException e) {
217            LOGGER.warn("Unable to invoke create method", e);
218        } catch (final NoSuchMethodException e) {
219            LOGGER.warn("Unable to find create method", e);
220        } catch (final InvocationTargetException e) {
221            LOGGER.warn("Unable to invoke create method", e);
222        }
223        return null;
224    }
225
226    private static Object initializeJMDNS() {
227        try {
228            jmDNSClass = Class.forName("javax.jmdns.JmDNS");
229            serviceInfoClass = Class.forName("javax.jmdns.ServiceInfo");
230            //if version 3 is available, use it to constuct a serviceInfo instance, otherwise support the version1 API
231            boolean isVersion3 = false;
232            try {
233                //create method is in version 3, not version 1
234                jmDNSClass.getMethod("create", (Class[])null);
235                isVersion3 = true;
236            } catch (final NoSuchMethodException e) {
237                //no-op
238            }
239
240            if (isVersion3) {
241                return createJmDNSVersion3();
242            } else {
243                return createJmDNSVersion1();
244            }
245        } catch (final ClassNotFoundException e) {
246            LOGGER.warn("JmDNS or serviceInfo class not found", e);
247        } catch (final ExceptionInInitializerError e2) {
248            LOGGER.warn("JmDNS or serviceInfo class not found", e2);
249        }
250        return null;
251    }
252}