package dmg.cells.services.login; import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Throwables; import com.google.common.util.concurrent.MoreExecutors; import javatunnel.UserValidatable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.io.IOException; import java.io.InputStream; import java.io.InterruptedIOException; import java.io.OutputStream; import java.io.PrintWriter; import java.io.Serializable; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; import java.nio.channels.ServerSocketChannel; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; import dmg.cells.nucleus.CellAdapter; import dmg.cells.nucleus.CellAddressCore; import dmg.cells.nucleus.CellEvent; import dmg.cells.nucleus.CellEventListener; import dmg.cells.nucleus.CellMessage; import dmg.cells.nucleus.CellMessageAnswerable; import dmg.cells.nucleus.CellNucleus; import dmg.cells.nucleus.CellPath; import dmg.cells.nucleus.CellVersion; import dmg.cells.nucleus.NoRouteToCellException; import dmg.util.KeepAliveListener; import org.dcache.util.Args; import org.dcache.util.NDC; import org.dcache.util.Version; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.collect.Maps.newHashMap; import static com.google.common.net.InetAddresses.toUriString; import static org.dcache.util.ByteUnit.KiB; /** * * * * @author Patrick Fuhrmann * @version 0.1, 15 Feb 1998 */ public class LoginManager extends CellAdapter implements UserValidatable { private static final Object DEAD_CELL = new Object(); private static final Logger LOGGER = LoggerFactory.getLogger(LoginManager.class); private final CellNucleus _nucleus; private final Args _args; private ListenThread _listenThread; private final AtomicInteger _connectionDeniedCounter = new AtomicInteger(); private final AtomicInteger _loginCounter = new AtomicInteger(); private final AtomicInteger _loginFailures = new AtomicInteger(); private CellVersion _version; private ScheduledExecutorService _scheduledExecutor; private ConcurrentMap<String, Object> _children = new ConcurrentHashMap<>(); private CellPath _authenticator; private KeepAliveTask _keepAlive; private LoginBrokerPublisher _loginBrokerPublisher; private LoginCellFactory _loginCellFactory; private volatile boolean _sending = true; private volatile int _maxLogin = -1; /** * Tagging interface that a CellMessage payload implements to indicate * the notification should be forwarded to all children. */ public interface OfInterestToChildren { } /** * <pre> * usage <listenPort> <loginCellFactoryClass> * * all residual arguments and all options are sent to * the <loginCellClass> : * <init>(String name , StreamEngine engine , Args args ) * * and to the Authentication module (class) * * <init>(CellNucleus nucleus , Args args ) * * Both get their own copy. * </pre> */ public LoginManager(String name, String argString) { this(name, "Generic", argString); } public LoginManager(String name, String cellType, String argString) { super(name, cellType, argString); _nucleus = getNucleus(); _args = getArgs(); } @Override protected void starting() throws Exception { if (_args.argc() < 2) { throw new IllegalArgumentException( "USAGE : ... <listenPort> <loginCell>" + " [-maxLogin=<n>|-1]" + " [-keepAlive=<seconds>]" + " [-acceptErrorWait=<msecs>]" + " [args givenToLoginClass]"); } int listenPort = Integer.parseInt(_args.argv(0)); String loginCell = _args.argv(1); Args childArgs = new Args(_args.toString() .replaceFirst("(^|\\s)-consume=\\S*", "") .replaceFirst("(^|\\s)-subscribe=\\S*", "")); childArgs.shift(); childArgs.shift(); // get the authentication _authenticator = new CellPath(_args.getOption("authenticator", "pam")); String maxLogin = _args.getOpt("maxLogin"); if (maxLogin != null) { try { _maxLogin = Integer.parseInt(maxLogin); } catch (NumberFormatException ee) {/* bad values ignored */} } if (_maxLogin < 0) { LOGGER.info("Maximum login feature disabled"); } else { LOGGER.info("Maximum logins set to: {}", _maxLogin); } _scheduledExecutor = Executors.newSingleThreadScheduledExecutor(_nucleus); // keep alive long keepAlive = TimeUnit.SECONDS.toMillis(_args.getLongOption("keepAlive", 0L)); LOGGER.info("Keep alive set to {} ms", keepAlive); _keepAlive = new KeepAliveTask(); _keepAlive.schedule(keepAlive); _loginCellFactory = new LoginCellFactoryBuilder() .setName(loginCell) .setCellEndpoint(this) .setLoginManagerName(getCellName()) .setArgs(childArgs) .build(); _version = new CellVersion(Version.of(_loginCellFactory)); String topic = _args.getOpt("brokerTopic"); if (topic != null) { Splitter byComma = Splitter.on(",").omitEmptyStrings(); Splitter byColon = Splitter.on(":").omitEmptyStrings(); _loginBrokerPublisher = new LoginBrokerPublisher(); _loginBrokerPublisher.setExecutor(_scheduledExecutor); _loginBrokerPublisher.setTopic(topic); _loginBrokerPublisher.setCellEndpoint(this); _loginBrokerPublisher.setCellAddress(_nucleus.getThisAddress()); _loginBrokerPublisher.setTags(byComma.splitToList(_args.getOption("brokerTags"))); _loginBrokerPublisher.setProtocolEngine(_loginCellFactory.getName()); _loginBrokerPublisher.setProtocolFamily(_args.getOption("protocolFamily", "")); _loginBrokerPublisher.setProtocolVersion(_args.getOption("protocolVersion", "1.0")); _loginBrokerPublisher.setUpdateTime(_args.getLongOption("brokerUpdateTime")); _loginBrokerPublisher.setUpdateTimeUnit(TimeUnit.valueOf(_args.getOption("brokerUpdateTimeUnit"))); _loginBrokerPublisher.setUpdateThreshold(_args.getDoubleOption("brokerUpdateOffset")); _loginBrokerPublisher.setRoot(Strings.emptyToNull(_args.getOption("brokerRoot", _args.getOption("root")))); _loginBrokerPublisher.setReadPaths(byColon.splitToList(_args.getOption("brokerReadPaths", "/"))); _loginBrokerPublisher.setWritePaths(byColon.splitToList(_args.getOption("brokerWritePaths", "/"))); _loginBrokerPublisher.setAddress(Strings.emptyToNull(_args.getOption("brokerAddress"))); _loginBrokerPublisher.setPort(_args.getIntOption("brokerPort", 0)); addCommandListener(_loginBrokerPublisher); addCellEventListener(_loginBrokerPublisher); if (_maxLogin < 0) { _maxLogin = 100000; } } else { _loginBrokerPublisher = null; } addCellEventListener(new LoginEventListener()); _listenThread = new ListenThread(listenPort); } @Override protected void started() { _nucleus.newThread(_listenThread, getCellName() + "-listen").start(); if (_loginBrokerPublisher != null) { _loginBrokerPublisher.afterStart(); } } @Override public void messageArrived(CellMessage envelope) { Serializable message = envelope.getMessageObject(); if (_loginBrokerPublisher != null) { if (message instanceof NoRouteToCellException) { _loginBrokerPublisher.messageArrived((NoRouteToCellException) message); } else if (message instanceof LoginBrokerInfoRequest) { _loginBrokerPublisher.messageArrived((LoginBrokerInfoRequest) message); } } // Try delivering message to all children. if (message instanceof OfInterestToChildren) { for (String child : _children.keySet()) { CellMessage msg = envelope.clone(); msg.getDestinationPath().add(child); sendMessage(msg); } } } @Override public CellVersion getCellVersion() { return _version; } public int getListenPort() { return _listenThread.getListenPort(); } public static final String hh_get_children = "[-binary]"; public Object ac_get_children(Args args) { boolean binary = args.hasOption("binary"); if (binary) { /* Important: Do not try to allocate a sized array as _children may be * updated in between creating the array and copying the keys. */ String[] list = _children.keySet().toArray(new String[0]); return new LoginManagerChildrenInfo(getCellName(), getCellDomainName(), list); } else { StringBuilder sb = new StringBuilder(); for (String child : _children.keySet()) { sb.append(child).append('\n'); } return sb.toString(); } } private class LoginEventListener implements CellEventListener { @Override public void cellDied(CellEvent ce) { String removedCell = ce.getSource().toString(); if (!removedCell.startsWith(getCellName())) { return; } /* while in some cases remove may be issued prior cell is inserted into _children * following trick is used: * if there is no mapping for this cell, we create a 'dead' mapping, which will * allow following put to identify it as a 'dead' and remove it. */ Object cell = _children.putIfAbsent(removedCell, DEAD_CELL); if (cell != null) { _children.remove(removedCell, cell); } LOGGER.info("LoginEventListener : removing : {}", removedCell); loadChanged(); } } private class KeepAliveTask implements Runnable { private ScheduledFuture<?> _future; private long _keepAlive; @Override public void run() { try { for (Object o : _children.values()) { if (o instanceof KeepAliveListener) { try { ((KeepAliveListener) o).keepAlive(); } catch (Throwable t) { LOGGER.warn("Problem reported by : {} : {}", o, t); } } } } catch (Throwable t) { LOGGER.warn("runKeepAlive reported : {}", t.toString()); } } public synchronized void schedule(long keepAlive) { _keepAlive = keepAlive; if (_future != null) { _future.cancel(false); } if (_keepAlive > 0) { _future = _scheduledExecutor.scheduleWithFixedDelay(this, _keepAlive, _keepAlive, TimeUnit.MILLISECONDS); } else { _future = null; } LOGGER.info("Keep Alive value changed to {}", _keepAlive); } public synchronized long getKeepAlive() { return _keepAlive; } } public static final String hh_set_keepalive = "<keepAliveValue/seconds>"; public String ac_set_keepalive_$_1(Args args) { long keepAlive = Long.parseLong(args.argv(0)); _keepAlive.schedule(keepAlive * 1000L); return "keepAlive value set to " + keepAlive + " seconds"; } // the cell implementation @Override public String toString() { ListenThread listenThread = _listenThread; LoginCellFactory loginCellFactory = _loginCellFactory; return "p=" + (listenThread == null ? "" : String.valueOf(listenThread.getListenPort())) + ";c=" + (loginCellFactory == null ? "" : loginCellFactory.getName()); } @Override public void getInfo(PrintWriter pw) { pw.println("--- Login Manager ---"); pw.println(" Listen Port : " + _listenThread.getListenPort()); pw.println(" Protocol engine: " + _loginCellFactory.getName()); pw.println(" NioChannel : " + (_listenThread._serverSocket.getChannel() != null)); pw.println(" Logins created : " + _loginCounter); pw.println(" Logins failed : " + _loginFailures); pw.println(" Logins denied : " + _connectionDeniedCounter); pw.println(" KeepAlive : " + (_keepAlive.getKeepAlive() / 1000L)); if (_maxLogin > -1) { pw.println(" Logins/max : " + _children.size() + '/' + _maxLogin); } pw.println(); pw.println("--- Login cell factory ---"); _loginCellFactory.getInfo(pw); if (_loginBrokerPublisher != null) { pw.println(); pw.println("--- Login broker info ---"); _loginBrokerPublisher.getInfo(pw); } } public static final String hh_set_max_logins = "<maxNumberOfLogins>|-1"; public String ac_set_max_logins_$_1(Args args) { int n = Integer.parseInt(args.argv(0)); checkArgument(n == -1 || _maxLogin >= 0, "Can't switch off maxLogin feature"); checkArgument(n >= 0 || _maxLogin == -1, "Can't switch on maxLogin feature"); _maxLogin = n; loadChanged(); return ""; } @Override protected void stopping() { LOGGER.info("cleanUp requested by nucleus, closing listen socket"); if (_loginBrokerPublisher != null) { _loginBrokerPublisher.beforeStop(); } /* Shut down the listen thread in stopping to shut down login cell factories * before cell nucleus cancels pending messages with a timeout. Otherwise * some components used by the factories - such as the pool manager handler * subscriber - enter an eager retry loop. */ if (_listenThread != null) { _listenThread.shutdown(); } } @Override public void stopped() { if (_scheduledExecutor != null) { _scheduledExecutor.shutdown(); } LOGGER.info("Bye Bye"); } private class ListenThread implements Runnable { private static final int SHUTDOWN_TIMEOUT = 60000; private final InetSocketAddress _socketAddress; private final Constructor<?> _ssfConstructor; private final String _factoryArgs; private final long _acceptErrorTimeout; private volatile boolean _shutdown; private ServerSocket _serverSocket; private ListenThread(int listenPort) throws Exception { long timeout; try { timeout = Long.parseLong(_args.getOpt("acceptErrorWait")); } catch (NumberFormatException e) { /* bad values ignored */ timeout = 0; } _acceptErrorTimeout = timeout; String listen = _args.getOpt("listen"); if (Strings.isNullOrEmpty(listen)) { _socketAddress = new InetSocketAddress(listenPort); } else { _socketAddress = new InetSocketAddress(InetAddress.getByName(listen), listenPort); } String ssf = _args.getOpt("socketfactory"); if (ssf != null) { Args args = new Args(ssf); checkArgument(args.argc() >= 1 , "Invalid Arguments for 'socketfactory'"); String tunnelFactoryClass = args.argv(0); /* * the rest is passed to factory constructor */ args.shift(); _factoryArgs = args.toString(); Class<?> ssfClass = Class.forName(tunnelFactoryClass); Constructor<?> constructor; try { constructor = ssfClass.getConstructor(String.class, Map.class); } catch (Exception ee) { constructor = ssfClass.getConstructor(String.class); } _ssfConstructor = constructor; } else { _ssfConstructor = null; _factoryArgs = null; } openPort(); } private void openPort() throws Exception { if (_ssfConstructor == null) { _serverSocket = ServerSocketChannel.open().socket(); } else { Object obj; try { if (_ssfConstructor.getParameterTypes().length == 2) { Map<String, Object> map = newHashMap(getDomainContext()); map.put("UserValidatable", LoginManager.this); obj = _ssfConstructor.newInstance(_factoryArgs, map); } else { obj = _ssfConstructor.newInstance(_factoryArgs); } } catch (InvocationTargetException e) { Throwables.propagateIfPossible(e.getCause(), Exception.class); throw new RuntimeException(e); } Method meth = obj.getClass().getMethod("createServerSocket"); _serverSocket = (ServerSocket) meth.invoke(obj); LOGGER.info("ListenThread : got serverSocket class : {}", _serverSocket.getClass().getName()); } _serverSocket.bind(_socketAddress); if (_loginBrokerPublisher != null) { /* Synchronize to make update atomic. */ synchronized (_loginBrokerPublisher) { _loginBrokerPublisher.setSocketAddress((InetSocketAddress) _serverSocket.getLocalSocketAddress()); String address = Strings.emptyToNull(_args.getOption("brokerAddress")); if (address != null) { _loginBrokerPublisher.setAddress(address); } int port = _args.getIntOption("brokerPort", 0); if (port != 0) { _loginBrokerPublisher.setPort(port); } } } LOGGER.info("Listening on {}", _serverSocket.getLocalSocketAddress()); LOGGER.trace("Nio Socket Channel : {}", (_serverSocket.getChannel() != null)); } public int getListenPort() { return _serverSocket.getLocalPort(); } @Override public void run() { ExecutorService executor = Executors.newCachedThreadPool(_nucleus); try { _loginCellFactory.startAsync().awaitRunning(); while (!_serverSocket.isClosed()) { try { Socket socket = _serverSocket.accept(); socket.setKeepAlive(true); socket.setTcpNoDelay(true); LOGGER.debug("Socket OPEN (ACCEPT) remote = {} local = {}", socket.getRemoteSocketAddress(), socket.getLocalSocketAddress()); LOGGER.info("Nio Channel (accept) : {}", (socket.getChannel() != null)); int currentChildCount = _children.size(); LOGGER.info("New connection : {}", currentChildCount); if ((_maxLogin > -1) && (currentChildCount >= _maxLogin)) { _connectionDeniedCounter.incrementAndGet(); LOGGER.warn("Connection denied: Number of allowed logins exceeded ({} > {}).", currentChildCount, _maxLogin); executor.execute(new ShutdownEngine(socket)); } else { LOGGER.info("Connection request from {}", socket.getInetAddress()); executor.execute(new RunEngineThread(socket)); } } catch (InterruptedIOException ioe) { LOGGER.debug("Listen thread interrupted"); try { _serverSocket.close(); } catch (IOException ignored) { } } catch (IOException ioe) { if (!_serverSocket.isClosed()) { LOGGER.error("I/O error: {}", ioe.toString()); try { _serverSocket.close(); } catch (IOException ignored) { } if (_acceptErrorTimeout > 0L) { synchronized (this) { while (!_shutdown && _serverSocket.isClosed()) { LOGGER.warn("Sleeping {} ms before reopening server socket", _acceptErrorTimeout); wait(_acceptErrorTimeout); if (!_shutdown) { try { openPort(); LOGGER.warn("Resuming operation"); } catch (Exception ee) { LOGGER.warn("Failed to open socket: {}", ee.toString()); } } } } } } } } } catch (InterruptedException ignored) { } finally { // Initiate shutdown of children as early as possible terminateChildren(); shutdownAndAwaitTermination(executor); // At this point we know that no new children will be added terminateChildren(); awaitTerminationOfChildren(); // Now that children should be terminated, we release any shared // resources managed by the factory. _loginCellFactory.stopAsync(); LOGGER.trace("Listen thread finished"); } } private void shutdownAndAwaitTermination(ExecutorService executor) { executor.shutdown(); try { executor.awaitTermination(SHUTDOWN_TIMEOUT, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } } private void awaitTerminationOfChildren() { try { for (Object child : _children.values()) { if (child instanceof CellAdapter) { getNucleus().join(((CellAdapter) child).getCellName()); } } } catch (InterruptedException ignored) { Thread.currentThread().interrupt(); } } private void terminateChildren() { for (Object child : _children.values()) { if (child instanceof CellAdapter) { getNucleus().kill(((CellAdapter) child).getCellName()); } } } public void shutdown() { LOGGER.info("Listen thread shutdown requested"); synchronized (this) { // // it is still hard to stop an Pending I/O call. // if (_shutdown || (_serverSocket == null)) { return; } _shutdown = true; try { LOGGER.debug("Socket SHUTDOWN local = {}", _serverSocket.getLocalSocketAddress()); _serverSocket.close(); } catch (IOException ee) { LOGGER.warn("ServerSocket close: {}", ee.toString()); } notifyAll(); } _loginCellFactory.awaitTerminated(); LOGGER.info("Shutdown sequence done"); } } /** * Class that closes the output half of a TCP socket, drains any pending input and closes the input once drained. * After creation, the {@link #start} method must be called. The activity occurs on a separate thread, allowing * the start method to be non-blocking. */ public static class ShutdownEngine implements Runnable { private final Socket _socket; public ShutdownEngine(Socket socket) { _socket = socket; } @Override public void run() { InputStream inputStream; OutputStream outputStream; try { inputStream = _socket.getInputStream(); outputStream = _socket.getOutputStream(); outputStream.close(); byte[] buffer = new byte[KiB.toBytes(1)]; /* * eat the outstanding date from socket and close it */ while (inputStream.read(buffer, 0, buffer.length) > 0) { } inputStream.close(); } catch (IOException ee) { LOGGER.warn("Shutdown : {}", ee.getMessage()); } finally { try { LOGGER.debug("Socket CLOSE (ACCEPT) remote = {} local = {}", _socket.getRemoteSocketAddress(), _socket.getLocalSocketAddress()); _socket.close(); } catch (IOException e) { // ignore } } LOGGER.info("Shutdown : done"); } } private class RunEngineThread implements Runnable { private Socket _socket; private RunEngineThread(Socket socket) { _socket = socket; } @Override public void run() { Thread t = Thread.currentThread(); InetSocketAddress remoteSocketAddress = (InetSocketAddress) _socket.getRemoteSocketAddress(); NDC.push(toUriString(remoteSocketAddress.getAddress()) + ':' + remoteSocketAddress.getPort()); try { LOGGER.info("acceptThread ({}): creating protocol engine", t); Object cell = _loginCellFactory.newCell(_socket); try { Method m = cell.getClass().getMethod("getCellName"); String cellName = (String) m.invoke(cell); LOGGER.info("Invoked cell name : {}", cellName); if (_children.putIfAbsent(cellName, cell) == DEAD_CELL) { /* while cell may be already gone do following trick: * if put return an old cell, then it's a dead cell and we * have to remove it. Dead cell is inserted by cleanup procedure: * if a remove for non existing cells issued, then cells is dead, and * we put it into _children. */ _children.remove(cellName, DEAD_CELL); } loadChanged(); } catch (IllegalAccessException | IllegalArgumentException | NoSuchMethodException | SecurityException | InvocationTargetException ee) { LOGGER.warn("Can't determine child name", ee); } _loginCounter.incrementAndGet(); } catch (InvocationTargetException e) { Throwable cause = e.getCause(); if (cause instanceof Error) { throw (Error) cause; } if (cause instanceof RuntimeException) { LOGGER.warn("Bug detected in dCache; please report this to <support@dcache.org>", cause); } else { LOGGER.warn("Exception (ITE) in secure protocol: {}", cause.getMessage()); } try { _socket.close(); } catch (IOException ee) {/* dead any way....*/} _loginFailures.incrementAndGet(); } catch (Exception e) { LOGGER.warn("Exception in secure protocol : {}", e.toString()); try { _socket.close(); } catch (IOException ee) {/* dead any way....*/} _loginFailures.incrementAndGet(); } finally { NDC.pop(); } } } private void loadChanged() { int children = _children.size(); LOGGER.info("New child count : {}", children); if (_loginBrokerPublisher != null) { _loginBrokerPublisher.setLoad(children, _maxLogin); } } @Override public boolean validateUser(String userName, String password) { String[] request = { "request", userName, "check-password", userName, password }; try { CellMessage msg = new CellMessage(_authenticator, request); msg = getNucleus().sendAndWait(msg, (long) 10000); if (msg == null) { LOGGER.warn("Pam request timed out {}", Thread.currentThread().getStackTrace()); return false; } Object[] r = (Object[]) msg.getMessageObject(); return (Boolean) r[5]; } catch (NoRouteToCellException e) { LOGGER.warn(e.getMessage()); return false; } catch (InterruptedException e) { Thread.currentThread().interrupt(); return false; } catch (ExecutionException e) { LOGGER.warn(e.getCause().getMessage()); return false; } } }