Ведение журнала MDC для SSHD-сервера с пользовательской файловой системой для каждой компании - PullRequest
0 голосов
/ 14 сентября 2018

Мы используем Apache-Mina SSHD 1.7 для предоставления SFTP-сервера, который использует пользовательскую реализацию файловой системы, которая создает файловую систему для каждой компании.Таким образом, пользователи одной и той же компании (или, точнее, одного и того же соединителя) получат доступ к одной и той же файловой системе, а пользователи другой компании получат доступ к файловой системе, уникальной для их компании.Кроме того, файловая система представляет собой просто представление базы данных MySQL и после некоторых преобразований записывает загруженные файлы непосредственно в БД и считывает файлы при загрузке из БД.

Настройка сервера выглядит следующим образом:

void init() {
    server = MessageSftpServer.setUpDefaultServer();
    server.setPort(port);
    LOG.debug("Server is configured for port {}", port);

    File pemFile = new File(pemLocation);
    FileKeyPairProvider provider = new FileKeyPairProvider(pemFile.toPath());
    validateKeyPairProvider(provider.loadKeys(), publicKeyList);
    server.setKeyPairProvider(provider);
    server.setCommandFactory(new ScpCommandFactory());
    server.setPasswordAuthenticator(
        (String username, String password, ServerSession session) -> {
            ...
        });

    PropertyResolverUtils.updateProperty(server, ServerAuthenticationManager.MAX_AUTH_REQUESTS, 3);

    SftpSubsystemFactory sftpFactory = new SftpSubsystemFactory.Builder()
        .withShutdownOnExit(false)
        .withUnsupportedAttributePolicy(UnsupportedAttributePolicy.Warn)
        .build();
    server.setSubsystemFactories(Collections.singletonList(sftpFactory));
    // add our custom virtual file system to trick the user into believing she is operating against
    // a true file system instead of just operating against a backing database
    server.setFileSystemFactory(
        new DBFileSystemFactory(connectorService, companyService, mmService, template));
    // filter connection attempts based on remote IPs defined in connectors
    server.addSessionListener(whitelistSessionListener);
}

В фабрике файловой системы мы просто создаем URI для поставщика файловой системы и передаем его соответствующему методу

@Override
public FileSystem createFileSystem(Session session) throws IOException {

    SFTPServerConnectorEntity connector =
    connectorService.getSFTPServerConnectorForUser(session.getUsername());
    if (null == connector) {
        throw new IOException("No SFTP Server connector found for user " + session.getUsername());
    }
    String ip = CommonUtils.getIPforSession(session);
    URI fsUri = URI.create("dbfs://" + session.getUsername() + "@" + ip + "/" + connector.getUuid());

    LOG.debug("Checking whether to create file system for user {} connected via IP {}",
        session.getUsername(), ip);

    Map<String, Object> env = new HashMap<>();
    env.put("UserAgent", session.getClientVersion());

    try {
        return fileSystemProvider.newFileSystem(fsUri, env);
    } catch (FileSystemAlreadyExistsException fsaeEx) {
        LOG.debug("Reusing existing filesystem for connector {}", connector.getUuid());
        return fileSystemProvider.getFileSystem(fsUri);
    }
}

и внутри провайдера мы просто анализируем значения из предоставленного URI и переменных среды, чтобы создать окончательную файловую систему, если в кэше еще не было ни одной доступной

@Override
public DBFileSystem newFileSystem(URI uri, Map<String, ?> env) throws IOException {
    LOG.trace("newFileSystem({}, {}))", uri, env);
    ConnectionInfo ci = ConnectionInfo.fromSchemeSpecificPart(uri.getSchemeSpecificPart());
    String cacheKey = generateCacheKey(ci);

    synchronized (fileSystems) {
        if (fileSystems.containsKey(cacheKey)) {
            throw new FileSystemAlreadyExistsException(
                "A filesystem for connector " + ci.getConnectorUuid()
                    + " connected from IP " + ci.getIp() + " already exists");
        }
    }

    SFTPServerConnectorEntity connector =
        connectorService.get(SFTPServerConnectorEntity.class, ci.getConnectorUuid());
    List<CompanyEntity> companies = companyService.getCompaniesForConnector(connector);
    if (companies.size() < 1) {
        throw new IOException("No company for connector " + connector.getUuid() + " found");
    }

    DBFileSystem fileSystem = null;
    synchronized (fileSystems) {
        if (!fileSystems.containsKey(cacheKey)) {
            LOG.info("Created new filesystem for connector {} (Remote IP: {}, User: {}, UserAgent: {})",
                ci.getConnectorUuid(), ci.getIp(), ci.getUser(), env.get("UserAgent"));
            fileSystem = new DBFileSystem(this, connector.getUsername(), companies, connector,
                template, ci.getIp(), (String) env.get("UserAgent"));
            Pair<DBFileSystem, AtomicInteger> sessions =  Pair.of(fileSystem, new AtomicInteger(1));
            fileSystems.put(cacheKey, sessions);
        }
    }

    if (null == fileSystem) {
        throw new FileSystemAlreadyExistsException(
            "A filesystem for connector " + ci.getConnectorUuid()
                + " connected from IP " + ci.getIp() + " already exists");
    }

    return fileSystem;
}

@Override
public DBFileSystem getFileSystem(URI uri) {
    LOG.trace("getFileSystem({}))", uri);
    String schemeSpecificPart = uri.getSchemeSpecificPart();

    if (!schemeSpecificPart.startsWith("//")) {
        throw new IllegalArgumentException(
            "Invalid URI provided. URI must have a form of 'dbfs://ip:port/connector-uuid' where "
                + "'ip' is the IP address of the connected user, 'port'  is the remote port of the user and "
                + "'connector-uuid' is a UUID string identifying the connector the filesystem was created for");
    }

    ConnectionInfo ci = ConnectionInfo.fromSchemeSpecificPart(schemeSpecificPart);
    String cacheKey = generateCacheKey(ci);

    if (!fileSystems.containsKey(cacheKey)) {
        throw new FileSystemNotFoundException(
            "No filesystem found for connector " + ci.getConnectorUuid() + " with connection from IP "
                + ci.getIp());
    }
    Pair<DBFileSystem, AtomicInteger> sessions = fileSystems.get(cacheKey);
    if (!sessions.getKey().isOpen()) {
        throw new FileSystemNotFoundException(
            "Filesystem for connector " + ci.getConnectorUuid() + " with connection from IP " + ci
                .getIp() + " was closed already");
    }
    int curSessions = sessions.getValue().incrementAndGet();
    LOG.info("Added further session to filesystem for connector {}. Current connected sessions: {} (Remote IP: {}, User: {})",
        ci.getConnectorUuid(), curSessions, ci.getIp(), ci.getUser());

    return sessions.getKey();
}

private String generateCacheKey(String user, String ip, String connectorUuid) {
    return connectorUuid + "_" + ip + "_" + user;
}

private String generateCacheKey(ConnectionInfo ci) {
    return generateCacheKey(ci.getUser(), ci.getIp(), ci.getConnectorUuid());
}

Это работаеточень хорошо, однако, так как все больше и больше пользователей добавляются к SFTP-серверу, мониторинг выполненных действий несколько страдает из-за отсутствия правильной регистрации в MDC.Простое добавление журналов MDC не работает должным образом, поскольку Mina или SSHD, в частности, делят потоки между подключенными пользователями, что иногда приводит к тому, что контекст MDC иногда печатает неверную информацию, что еще больше затрудняет анализ журнала.В качестве временного решения мы удалили его в настоящее время из проекта.

Мы также попытались настроить Nio2Session (и несколько других классов), чтобы вмешиваться в создание потоков, хотя эти классы, очевидно, не были разработаныдля наследования, которые впоследствии приводят к проблемам в будущем.

Есть ли лучшая стратегия, чтобы включить ведение журнала MDC в нашем конкретном сценарии, где используется не одна файловая система, а файловая система для каждой компании?

...