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.Core;
27  import org.apache.logging.log4j.core.config.plugins.Plugin;
28  import org.apache.logging.log4j.core.util.Integers;
29  import org.apache.logging.log4j.status.StatusLogger;
30  import org.apache.logging.log4j.util.LoaderUtil;
31  
32  /**
33   * Advertise an entity via ZeroConf/MulticastDNS and the JmDNS library.
34   *
35   * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255 bytes
36   * will be removed prior to advertisement.
37   *
38   */
39  @Plugin(name = "multicastdns", category = Core.CATEGORY_NAME, elementType = "advertiser", printObject = false)
40  public class MulticastDnsAdvertiser implements Advertiser {
41      /**
42       * Status logger.
43       */
44      protected static final Logger LOGGER = StatusLogger.getLogger();
45  
46      private static final int MAX_LENGTH = 255;
47      private static final int DEFAULT_PORT = 4555;
48  
49      private static Object jmDNS = initializeJmDns();
50      private static Class<?> jmDNSClass;
51      private static Class<?> serviceInfoClass;
52  
53      public MulticastDnsAdvertiser() {
54          // no arg constructor for reflection
55      }
56  
57      /**
58       * Advertise the provided entity.
59       *
60       * Properties map provided in advertise method must include a "name" entry but may also provide "protocol" (tcp/udp)
61       * as well as a "port" entry
62       *
63       * The length of property names and values must be 255 bytes or less. Entries with names or values larger than 255
64       * bytes will be removed prior to advertisement.
65       *
66       * @param properties the properties representing the entity to advertise
67       * @return the object which can be used to unadvertise, or null if advertisement was unsuccessful
68       */
69      @Override
70      public Object advertise(final Map<String, String> properties) {
71          // default to tcp if "protocol" was not set
72          final Map<String, String> truncatedProperties = new HashMap<>();
73          for (final Map.Entry<String, String> entry : properties.entrySet()) {
74              if (entry.getKey().length() <= MAX_LENGTH && entry.getValue().length() <= MAX_LENGTH) {
75                  truncatedProperties.put(entry.getKey(), entry.getValue());
76              }
77          }
78          final String protocol = truncatedProperties.get("protocol");
79          final String zone = "._log4j._" + (protocol != null ? protocol : "tcp") + ".local.";
80          // default to 4555 if "port" was not set
81          final String portString = truncatedProperties.get("port");
82          final int port = Integers.parseInt(portString, DEFAULT_PORT);
83  
84          final String name = truncatedProperties.get("name");
85  
86          // if version 3 is available, use it to construct a serviceInfo instance, otherwise support the version1 API
87          if (jmDNS != null) {
88              boolean isVersion3 = false;
89              try {
90                  // create method is in version 3, not version 1
91                  jmDNSClass.getMethod("create");
92                  isVersion3 = true;
93              } catch (final NoSuchMethodException e) {
94                  // no-op
95              }
96              Object serviceInfo;
97              if (isVersion3) {
98                  serviceInfo = buildServiceInfoVersion3(zone, port, name, truncatedProperties);
99              } else {
100                 serviceInfo = buildServiceInfoVersion1(zone, port, name, truncatedProperties);
101             }
102 
103             try {
104                 final Method method = jmDNSClass.getMethod("registerService", serviceInfoClass);
105                 method.invoke(jmDNS, serviceInfo);
106             } catch (final IllegalAccessException | InvocationTargetException e) {
107                 LOGGER.warn("Unable to invoke registerService method", e);
108             } catch (final NoSuchMethodException e) {
109                 LOGGER.warn("No registerService method", e);
110             }
111             return serviceInfo;
112         }
113         LOGGER.warn("JMDNS not available - will not advertise ZeroConf support");
114         return null;
115     }
116 
117     /**
118      * Unadvertise the previously advertised entity.
119      *
120      * @param serviceInfo
121      */
122     @Override
123     public void unadvertise(final Object serviceInfo) {
124         if (jmDNS != null) {
125             try {
126                 final Method method = jmDNSClass.getMethod("unregisterService", serviceInfoClass);
127                 method.invoke(jmDNS, serviceInfo);
128             } catch (final IllegalAccessException | InvocationTargetException e) {
129                 LOGGER.warn("Unable to invoke unregisterService method", e);
130             } catch (final NoSuchMethodException e) {
131                 LOGGER.warn("No unregisterService method", e);
132             }
133         }
134     }
135 
136     private static Object createJmDnsVersion1() {
137         try {
138             return jmDNSClass.getConstructor().newInstance();
139         } catch (final InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
140             LOGGER.warn("Unable to instantiate JMDNS", e);
141         }
142         return null;
143     }
144 
145     private static Object createJmDnsVersion3() {
146         try {
147             final Method jmDNSCreateMethod = jmDNSClass.getMethod("create");
148             return jmDNSCreateMethod.invoke(null, (Object[]) null);
149         } catch (final IllegalAccessException | InvocationTargetException e) {
150             LOGGER.warn("Unable to invoke create method", e);
151         } catch (final NoSuchMethodException e) {
152             LOGGER.warn("Unable to get create method", e);
153         }
154         return null;
155     }
156 
157     private static Object buildServiceInfoVersion1(final String zone, final int port, final String name,
158             final Map<String, String> properties) {
159         // version 1 uses a hashtable
160         @SuppressWarnings("UseOfObsoleteCollectionType")
161         final Hashtable<String, String> hashtableProperties = new Hashtable<>(properties);
162         try {
163             return serviceInfoClass.getConstructor(String.class, String.class, int.class, int.class, int.class,
164                     Hashtable.class).newInstance(zone, name, port, 0, 0, hashtableProperties);
165         } catch (final IllegalAccessException | InstantiationException | InvocationTargetException e) {
166             LOGGER.warn("Unable to construct ServiceInfo instance", e);
167         } catch (final NoSuchMethodException e) {
168             LOGGER.warn("Unable to get ServiceInfo constructor", e);
169         }
170         return null;
171     }
172 
173     private static Object buildServiceInfoVersion3(final String zone, final int port, final String name,
174             final Map<String, String> properties) {
175         try {
176             return serviceInfoClass
177                     // zone/type display name port weight priority properties
178                     .getMethod("create", String.class, String.class, int.class, int.class, int.class, Map.class)
179                     .invoke(null, zone, name, port, 0, 0, properties);
180         } catch (final IllegalAccessException | InvocationTargetException e) {
181             LOGGER.warn("Unable to invoke create method", e);
182         } catch (final NoSuchMethodException e) {
183             LOGGER.warn("Unable to find create method", e);
184         }
185         return null;
186     }
187 
188     private static Object initializeJmDns() {
189         try {
190             jmDNSClass = LoaderUtil.loadClass("javax.jmdns.JmDNS");
191             serviceInfoClass = LoaderUtil.loadClass("javax.jmdns.ServiceInfo");
192             // if version 3 is available, use it to constuct a serviceInfo instance, otherwise support the version1 API
193             boolean isVersion3 = false;
194             try {
195                 // create method is in version 3, not version 1
196                 jmDNSClass.getMethod("create");
197                 isVersion3 = true;
198             } catch (final NoSuchMethodException e) {
199                 // no-op
200             }
201 
202             if (isVersion3) {
203                 return createJmDnsVersion3();
204             }
205             return createJmDnsVersion1();
206         } catch (final ClassNotFoundException | ExceptionInInitializerError e) {
207             LOGGER.warn("JmDNS or serviceInfo class not found", e);
208         }
209         return null;
210     }
211 }