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.core.util.Loader;
29  import org.apache.logging.log4j.status.StatusLogger;
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       * 
42       */
43      private static final int MAX_LENGTH = 255;
44      /**
45       * Status logger.
46       */
47      protected static final Logger LOGGER = StatusLogger.getLogger();
48      private static final int DEFAULT_PORT = 4555;
49  
50      private static Object jmDNS = initializeJmDns();
51      private static Class<?> jmDNSClass;
52      private static Class<?> serviceInfoClass;
53  
54      public MulticastDnsAdvertiser() {
55          // no arg constructor for reflection
56      }
57  
58      /**
59       * Advertise the provided entity.
60       *
61       * Properties map provided in advertise method must include a "name" entry but may also provide "protocol" (tcp/udp)
62       * as well as a "port" entry
63       *
64       * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255
65       * bytes will be removed prior to advertisement.
66       *
67       * @param properties the properties representing the entity to advertise
68       * @return the object which can be used to unadvertise, or null if advertisement was unsuccessful
69       */
70      @Override
71      public Object advertise(final Map<String, String> properties) {
72          // default to tcp if "protocol" was not set
73          final Map<String, String> truncatedProperties = new HashMap<>();
74          for (final Map.Entry<String, String> entry : properties.entrySet()) {
75              if (entry.getKey().length() <= MAX_LENGTH && entry.getValue().length() <= MAX_LENGTH) {
76                  truncatedProperties.put(entry.getKey(), entry.getValue());
77              }
78          }
79          final String protocol = truncatedProperties.get("protocol");
80          final String zone = "._log4j._" + (protocol != null ? protocol : "tcp") + ".local.";
81          // default to 4555 if "port" was not set
82          final String portString = truncatedProperties.get("port");
83          final int port = Integers.parseInt(portString, DEFAULT_PORT);
84  
85          final String name = truncatedProperties.get("name");
86  
87          // if version 3 is available, use it to construct a serviceInfo instance, otherwise support the version1 API
88          if (jmDNS != null) {
89              boolean isVersion3 = false;
90              try {
91                  // create method is in version 3, not version 1
92                  jmDNSClass.getMethod("create");
93                  isVersion3 = true;
94              } catch (final NoSuchMethodException e) {
95                  // no-op
96              }
97              Object serviceInfo;
98              if (isVersion3) {
99                  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 }