View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements. See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache license, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License. You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the license for the specific language governing permissions and
15   * limitations under the license.
16   */
17  package org.apache.logging.log4j.core.net;
18  
19  import java.lang.reflect.InvocationTargetException;
20  import java.lang.reflect.Method;
21  import java.util.HashMap;
22  import java.util.Hashtable;
23  import java.util.Map;
24  
25  import org.apache.logging.log4j.Logger;
26  import org.apache.logging.log4j.core.config.plugins.Plugin;
27  import org.apache.logging.log4j.core.util.Integers;
28  import org.apache.logging.log4j.status.StatusLogger;
29  import org.apache.logging.log4j.util.LoaderUtil;
30  
31  /**
32   * Advertise an entity via ZeroConf/MulticastDNS and the JmDNS library.
33   *
34   * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255 bytes
35   * will be removed prior to advertisement.
36   *
37   */
38  @Plugin(name = "multicastdns", category = "Core", elementType = "advertiser", printObject = false)
39  public class MulticastDnsAdvertiser implements Advertiser {
40      /**
41       * Status logger.
42       */
43      protected static final Logger LOGGER = StatusLogger.getLogger();
44  
45      private static final int MAX_LENGTH = 255;
46      private static final int DEFAULT_PORT = 4555;
47  
48      private static Object jmDNS = initializeJmDns();
49      private static Class<?> jmDNSClass;
50      private static Class<?> serviceInfoClass;
51  
52      public MulticastDnsAdvertiser() {
53          // no arg constructor for reflection
54      }
55  
56      /**
57       * Advertise the provided entity.
58       *
59       * Properties map provided in advertise method must include a "name" entry but may also provide "protocol" (tcp/udp)
60       * as well as a "port" entry
61       *
62       * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255
63       * bytes will be removed prior to advertisement.
64       *
65       * @param properties the properties representing the entity to advertise
66       * @return the object which can be used to unadvertise, or null if advertisement was unsuccessful
67       */
68      @Override
69      public Object advertise(final Map<String, String> properties) {
70          // default to tcp if "protocol" was not set
71          final Map<String, String> truncatedProperties = new HashMap<>();
72          for (final Map.Entry<String, String> entry : properties.entrySet()) {
73              if (entry.getKey().length() <= MAX_LENGTH && entry.getValue().length() <= MAX_LENGTH) {
74                  truncatedProperties.put(entry.getKey(), entry.getValue());
75              }
76          }
77          final String protocol = truncatedProperties.get("protocol");
78          final String zone = "._log4j._" + (protocol != null ? protocol : "tcp") + ".local.";
79          // default to 4555 if "port" was not set
80          final String portString = truncatedProperties.get("port");
81          final int port = Integers.parseInt(portString, DEFAULT_PORT);
82  
83          final String name = truncatedProperties.get("name");
84  
85          // if version 3 is available, use it to construct a serviceInfo instance, otherwise support the version1 API
86          if (jmDNS != null) {
87              boolean isVersion3 = false;
88              try {
89                  // create method is in version 3, not version 1
90                  jmDNSClass.getMethod("create");
91                  isVersion3 = true;
92              } catch (final NoSuchMethodException e) {
93                  // no-op
94              }
95              Object serviceInfo;
96              if (isVersion3) {
97                  serviceInfo = buildServiceInfoVersion3(zone, port, name, truncatedProperties);
98              } else {
99                  serviceInfo = buildServiceInfoVersion1(zone, port, name, truncatedProperties);
100             }
101 
102             try {
103                 final Method method = jmDNSClass.getMethod("registerService", serviceInfoClass);
104                 method.invoke(jmDNS, serviceInfo);
105             } catch (final IllegalAccessException | InvocationTargetException e) {
106                 LOGGER.warn("Unable to invoke registerService method", e);
107             } catch (final NoSuchMethodException e) {
108                 LOGGER.warn("No 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      * 
119      * @param serviceInfo
120      */
121     @Override
122     public void unadvertise(final Object serviceInfo) {
123         if (jmDNS != null) {
124             try {
125                 final Method method = jmDNSClass.getMethod("unregisterService", serviceInfoClass);
126                 method.invoke(jmDNS, serviceInfo);
127             } catch (final IllegalAccessException | InvocationTargetException e) {
128                 LOGGER.warn("Unable to invoke unregisterService method", e);
129             } catch (final NoSuchMethodException e) {
130                 LOGGER.warn("No unregisterService method", e);
131             }
132         }
133     }
134 
135     private static Object createJmDnsVersion1() {
136         try {
137             return jmDNSClass.getConstructor().newInstance();
138         } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
139             LOGGER.warn("Unable to instantiate JMDNS", e);
140         }
141         return null;
142     }
143 
144     private static Object createJmDnsVersion3() {
145         try {
146             final Method jmDNSCreateMethod = jmDNSClass.getMethod("create");
147             return jmDNSCreateMethod.invoke(null, (Object[]) null);
148         } catch (final IllegalAccessException | InvocationTargetException e) {
149             LOGGER.warn("Unable to invoke create method", e);
150         } catch (final NoSuchMethodException e) {
151             LOGGER.warn("Unable to get create method", e);
152         }
153         return null;
154     }
155 
156     private static Object buildServiceInfoVersion1(final String zone, final int port, final String name,
157             final Map<String, String> properties) {
158         // version 1 uses a hashtable
159         @SuppressWarnings("UseOfObsoleteCollectionType")
160         final Hashtable<String, String> hashtableProperties = new Hashtable<>(properties);
161         try {
162             return serviceInfoClass.getConstructor(String.class, String.class, int.class, int.class, int.class,
163                     Hashtable.class).newInstance(zone, name, port, 0, 0, hashtableProperties);
164         } catch (final IllegalAccessException | InstantiationException | InvocationTargetException e) {
165             LOGGER.warn("Unable to construct ServiceInfo instance", e);
166         } catch (final NoSuchMethodException e) {
167             LOGGER.warn("Unable to get ServiceInfo constructor", e);
168         }
169         return null;
170     }
171 
172     private static Object buildServiceInfoVersion3(final String zone, final int port, final String name,
173             final Map<String, String> properties) {
174         try {
175             return serviceInfoClass
176                     // zone/type display name port weight priority properties
177                     .getMethod("create", String.class, String.class, int.class, int.class, int.class, Map.class)
178                     .invoke(null, zone, name, port, 0, 0, properties);
179         } catch (final IllegalAccessException | InvocationTargetException e) {
180             LOGGER.warn("Unable to invoke create method", e);
181         } catch (final NoSuchMethodException e) {
182             LOGGER.warn("Unable to find create method", e);
183         }
184         return null;
185     }
186 
187     private static Object initializeJmDns() {
188         try {
189             jmDNSClass = LoaderUtil.loadClass("javax.jmdns.JmDNS");
190             serviceInfoClass = LoaderUtil.loadClass("javax.jmdns.ServiceInfo");
191             // if version 3 is available, use it to constuct a serviceInfo instance, otherwise support the version1 API
192             boolean isVersion3 = false;
193             try {
194                 // create method is in version 3, not version 1
195                 jmDNSClass.getMethod("create");
196                 isVersion3 = true;
197             } catch (final NoSuchMethodException e) {
198                 // no-op
199             }
200 
201             if (isVersion3) {
202                 return createJmDnsVersion3();
203             }
204             return createJmDnsVersion1();
205         } catch (final ClassNotFoundException | ExceptionInInitializerError e) {
206             LOGGER.warn("JmDNS or serviceInfo class not found", e);
207         }
208         return null;
209     }
210 }