001/*
002 * Logback: the reliable, generic, fast and flexible logging framework.
003 * Copyright (C) 1999-2026, QOS.ch. All rights reserved.
004 *
005 * This program and the accompanying materials are dual-licensed under
006 * either the terms of the Eclipse Public License v2.0 as published by
007 * the Eclipse Foundation
008 *
009 *   or (per the licensee's choosing)
010 *
011 * under the terms of the GNU Lesser General Public License version 2.1
012 * as published by the Free Software Foundation.
013 */
014package ch.qos.logback.classic.net;
015
016import java.io.IOException;
017import java.lang.reflect.Constructor;
018import java.net.InetAddress;
019import java.net.ServerSocket;
020import java.net.Socket;
021import java.util.ArrayList;
022import java.util.Collection;
023import java.util.List;
024import java.util.concurrent.CountDownLatch;
025
026import javax.net.ServerSocketFactory;
027
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import ch.qos.logback.classic.LoggerContext;
032import ch.qos.logback.classic.joran.JoranConfigurator;
033import ch.qos.logback.core.joran.spi.JoranException;
034import ch.qos.logback.core.util.IpAddressMatcher;
035
036/**
037 * A simple {@link SocketNode} based server.
038 * 
039 * <pre>
040 *      &lt;b&gt;Usage:&lt;/b&gt; java ch.qos.logback.classic.net.SimpleSocketServer port configFile
041 *                     allowedAddress [allowedAddress ...]
042 * </pre>
043 * 
044 * where <em>port</em> is a port number where the server listens,
045 * <em>configFile</em> is an XML configuration file fed to
046 * {@link JoranConfigurator}, and each <em>allowedAddress</em> is a client IP
047 * or CIDR range that is permitted to connect (e.g. {@code 192.168.1.10} or
048 * {@code 192.168.1.0/24}). At least one allowed address must be specified on
049 * the command line.
050 * 
051 * <p>
052 * When embedding the server programmatically, allowed client addresses must be
053 * registered with {@link #addAllowedClientAddress(String)} or
054 * {@link #setAllowedClientAddresses(Collection)} before clients can connect.
055 * Supported forms are single IPs and CIDR network ranges. An empty whitelist
056 * means no clients are allowed.
057 * </p>
058 * 
059 * @author Ceki G&uuml;lc&uuml;
060 * @author S&eacute;bastien Pennec
061 * 
062 * @since 0.8.4
063 */
064public class SimpleSocketServer extends Thread {
065
066    Logger logger = LoggerFactory.getLogger(SimpleSocketServer.class);
067
068    private final int port;
069    private final LoggerContext lc;
070    private boolean closed = false;
071    private ServerSocket serverSocket;
072    private List<SocketNode> socketNodeList = new ArrayList<SocketNode>();
073
074    /**
075     * Only clients whose remote address matches one of these matchers are
076     * accepted. Empty means no clients are allowed.
077     */
078    private final List<IpAddressMatcher> allowedClientAddresses = new ArrayList<IpAddressMatcher>();
079
080    // used for testing purposes
081    private CountDownLatch latch;
082
083    public static void main(String argv[]) throws Exception {
084        doMain(SimpleSocketServer.class, argv);
085    }
086
087    protected static void doMain(Class<? extends SimpleSocketServer> serverClass, String argv[]) throws Exception {
088        if (argv.length < 3) {
089            if (argv.length == 2) {
090                usage("No allowed client IP addresses specified. Please explicitly whitelist client IPs"
091                        + " or CIDR ranges on the command line (e.g. 192.168.1.0/24).", serverClass);
092            } else {
093                usage("Wrong number of arguments.", serverClass);
094            }
095        }
096
097        int port = parsePortNumber(argv[0]);
098        String configFile = argv[1];
099        LoggerContext lc = (LoggerContext) LoggerFactory.getILoggerFactory();
100        configureLC(lc, configFile);
101
102        SimpleSocketServer sss = createServer(serverClass, lc, port);
103        for (int i = 2; i < argv.length; i++) {
104            try {
105                sss.addAllowedClientAddress(argv[i]);
106            } catch (IllegalArgumentException e) {
107                usage("Invalid allowed client address [" + argv[i] + "]: " + e.getMessage(), serverClass);
108            }
109        }
110
111
112
113        // start the server in a separate thread
114        sss.start();
115    }
116
117    private static SimpleSocketServer createServer(Class<? extends SimpleSocketServer> serverClass, LoggerContext lc,
118            int port) throws Exception {
119        Constructor<? extends SimpleSocketServer> constructor = serverClass.getConstructor(LoggerContext.class,
120                int.class);
121        return constructor.newInstance(lc, port);
122    }
123
124    public SimpleSocketServer(LoggerContext lc, int port) {
125        this.lc = lc;
126        this.port = port;
127    }
128
129    /**
130     * Authorize a single client IP address or a CIDR network range.
131     * <p>
132     * Only matching clients are accepted; others are closed immediately after
133     * {@code accept()}. When no allowed addresses are registered, no clients
134     * are accepted.
135     * </p>
136     *
137     * @param addressOrCidr a single IP (e.g. {@code 10.0.0.5}) or CIDR range
138     *                      (e.g. {@code 192.168.1.0/24})
139     * @throws IllegalArgumentException if the specification is invalid
140     * @since 1.6.2
141     */
142    public void addAllowedClientAddress(String addressOrCidr) {
143        allowedClientAddresses.add(new IpAddressMatcher(addressOrCidr));
144    }
145
146    /**
147     * Replace the set of authorized client addresses with the given collection.
148     * Each entry must be a single IP or CIDR range. Passing an empty collection
149     * (or {@code null}) clears the whitelist so that no clients are allowed.
150     *
151     * @param addresses allowed addresses / CIDR ranges, or {@code null}
152     * @throws IllegalArgumentException if any specification is invalid
153     * @since 1.6.2
154     */
155    public void setAllowedClientAddresses(Collection<String> addresses) {
156        allowedClientAddresses.clear();
157        if (addresses == null) {
158            return;
159        }
160        for (String addressOrCidr : addresses) {
161            addAllowedClientAddress(addressOrCidr);
162        }
163    }
164
165    /**
166     * Returns {@code true} if the client is allowed to connect.
167     * <p>
168     * When no allowed addresses are configured, no clients are allowed.
169     * </p>
170     *
171     * @param clientAddress the remote address of the connecting client
172     * @return {@code true} if the connection should be accepted
173     * @since 1.6.2
174     */
175    protected boolean isClientAllowed(InetAddress clientAddress) {
176        if (allowedClientAddresses.isEmpty() || clientAddress == null) {
177            return false;
178        }
179        for (IpAddressMatcher matcher : allowedClientAddresses) {
180            if (matcher.matches(clientAddress)) {
181                return true;
182            }
183        }
184        return false;
185    }
186
187    public void run() {
188
189        final String oldThreadName = Thread.currentThread().getName();
190
191        try {
192
193            final String newThreadName = getServerThreadName();
194            Thread.currentThread().setName(newThreadName);
195
196            logger.info("Listening on port " + port);
197            if (allowedClientAddresses.isEmpty()) {
198                logger.warn("No allowed client addresses configured; all incoming connections will be denied. "
199                        + "Use addAllowedClientAddress() or pass allowed addresses on the command line.");
200            } else {
201                logger.info("Client IP whitelist in effect ({} allowed address pattern(s))",
202                        allowedClientAddresses.size());
203            }
204            serverSocket = getServerSocketFactory().createServerSocket(port);
205            while (!closed) {
206                logger.info("Waiting to accept a new client.");
207                signalAlmostReadiness();
208                Socket socket = serverSocket.accept();
209                InetAddress clientAddress = socket.getInetAddress();
210                logger.info("Connected to client at " + clientAddress);
211                if (!isClientAllowed(clientAddress)) {
212                    logger.warn("Denying connection from unauthorized client " + clientAddress);
213                    closeSocketQuietly(socket);
214                    continue;
215                }
216                logger.info("Starting new socket node.");
217                SocketNode newSocketNode = new SocketNode(this, socket, lc);
218                synchronized (socketNodeList) {
219                    socketNodeList.add(newSocketNode);
220                }
221                final String clientThreadName = getClientThreadName(socket);
222                new Thread(newSocketNode, clientThreadName).start();
223            }
224        } catch (Exception e) {
225            if (closed) {
226                logger.info("Exception in run method for a closed server. This is normal.");
227            } else {
228                logger.error("Unexpected failure in run method", e);
229            }
230        }
231
232        finally {
233            Thread.currentThread().setName(oldThreadName);
234        }
235    }
236
237    private void closeSocketQuietly(Socket socket) {
238        try {
239            socket.close();
240        } catch (IOException e) {
241            logger.debug("Failed to close unauthorized client socket", e);
242        }
243    }
244
245    /**
246     * Returns the name given to the server thread.
247     */
248    protected String getServerThreadName() {
249        return String.format("Logback %s (port %d)", getClass().getSimpleName(), port);
250    }
251
252    /**
253     * Returns a name to identify each client thread.
254     */
255    protected String getClientThreadName(Socket socket) {
256        return String.format("Logback SocketNode (client: %s)", socket.getRemoteSocketAddress());
257    }
258
259    /**
260     * Gets the platform default {@link ServerSocketFactory}.
261     * <p>
262     * Subclasses may override to provide a custom server socket factory.
263     */
264    protected ServerSocketFactory getServerSocketFactory() {
265        return ServerSocketFactory.getDefault();
266    }
267
268    /**
269     * Signal another thread that we have established a connection This is useful
270     * for testing purposes.
271     */
272    void signalAlmostReadiness() {
273        if (latch != null && latch.getCount() != 0) {
274            // System.out.println("signalAlmostReadiness() with latch "+latch);
275            latch.countDown();
276        }
277    }
278
279    /**
280     * Used for testing purposes
281     * 
282     * @param latch
283     */
284    void setLatch(CountDownLatch latch) {
285        this.latch = latch;
286    }
287
288    /**
289     * Used for testing purposes
290     */
291    public CountDownLatch getLatch() {
292        return latch;
293    }
294
295    public boolean isClosed() {
296        return closed;
297    }
298
299    public void close() {
300        closed = true;
301        if (serverSocket != null) {
302            try {
303                serverSocket.close();
304            } catch (IOException e) {
305                logger.error("Failed to close serverSocket", e);
306            } finally {
307                serverSocket = null;
308            }
309        }
310
311        logger.info("closing this server");
312        synchronized (socketNodeList) {
313            for (SocketNode sn : socketNodeList) {
314                sn.close();
315            }
316        }
317        if (socketNodeList.size() != 0) {
318            logger.warn("Was expecting a 0-sized socketNodeList after server shutdown");
319        }
320
321    }
322
323    public void socketNodeClosing(SocketNode sn) {
324        logger.debug("Removing {}", sn);
325
326        // don't allow simultaneous access to the socketNodeList
327        // (e.g. removal whole iterating on the list causes
328        // java.util.ConcurrentModificationException)
329        synchronized (socketNodeList) {
330            socketNodeList.remove(sn);
331        }
332    }
333
334    static void usage(String msg) {
335        usage(msg, SimpleSocketServer.class);
336    }
337
338    static void usage(String msg, Class<? extends SimpleSocketServer> serverClass) {
339        System.err.println(msg);
340        System.err.println("Usage: java " + serverClass.getName()
341                + " port configFile allowedAddress [allowedAddress ...]");
342        System.err.println(
343                "  allowedAddress: a single IP (e.g. 192.168.1.10) or CIDR range (e.g. 192.168.1.0/24)");
344        System.err.println("  At least one allowedAddress must be specified.");
345        System.exit(1);
346    }
347
348    static int parsePortNumber(String portStr) {
349        try {
350            return Integer.parseInt(portStr);
351        } catch (java.lang.NumberFormatException e) {
352            e.printStackTrace();
353            usage("Could not interpret port number [" + portStr + "].");
354            // we won't get here
355            return -1;
356        }
357    }
358
359    static public void configureLC(LoggerContext lc, String configFile) throws JoranException {
360        JoranConfigurator configurator = new JoranConfigurator();
361        lc.reset();
362        configurator.setContext(lc);
363        configurator.doConfigure(configFile);
364    }
365}