blob: 662ac106593d0e22e6bccae0098cfe946d412e03 [file] [log] [blame]
/****************************************************************
* Licensed to the Apache Software Foundation (ASF) under one *
* or more contributor license agreements. See the NOTICE file *
* distributed with this work for additional information *
* regarding copyright ownership. The ASF licenses this file *
* to you under the Apache License, Version 2.0 (the *
* "License"); you may not use this file except in compliance *
* with the License. You may obtain a copy of the License at *
* *
* http://www.apache.org/licenses/LICENSE-2.0 *
* *
* Unless required by applicable law or agreed to in writing, *
* software distributed under the License is distributed on an *
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY *
* KIND, either express or implied. See the License for the *
* specific language governing permissions and limitations *
* under the License. *
****************************************************************/
package org.apache.james.imapserver.netty;
import static org.apache.james.jmap.JMAPTestingConstants.LOCALHOST_IP;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import java.io.EOFException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.ConcurrentLinkedDeque;
import org.apache.commons.configuration2.XMLConfiguration;
import org.apache.commons.configuration2.builder.FileBasedConfigurationBuilder;
import org.apache.commons.configuration2.builder.fluent.Parameters;
import org.apache.commons.configuration2.convert.DisabledListDelimiterHandler;
import org.apache.commons.configuration2.io.FileHandler;
import org.apache.commons.net.imap.IMAPReply;
import org.apache.commons.net.imap.IMAPSClient;
import org.apache.james.core.Username;
import org.apache.james.imap.encode.main.DefaultImapEncoderFactory;
import org.apache.james.imap.main.DefaultImapDecoderFactory;
import org.apache.james.imap.processor.base.AbstractChainedProcessor;
import org.apache.james.imap.processor.main.DefaultImapProcessorFactory;
import org.apache.james.mailbox.inmemory.manager.InMemoryIntegrationResources;
import org.apache.james.mailbox.store.FakeAuthenticator;
import org.apache.james.mailbox.store.FakeAuthorizator;
import org.apache.james.mailbox.store.StoreSubscriptionManager;
import org.apache.james.metrics.tests.RecordingMetricFactory;
import org.apache.james.server.core.configuration.Configuration;
import org.apache.james.server.core.filesystem.FileSystemImpl;
import org.apache.james.util.ClassLoaderUtils;
import org.apache.james.utils.TestIMAPClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.RepeatedTest;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;
import org.slf4j.LoggerFactory;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelOption;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import reactor.netty.Connection;
import reactor.netty.tcp.TcpClient;
class IMAPServerTest {
public static ListAppender<ILoggingEvent> getListAppenderForClass(Class clazz) {
Logger logger = (Logger) LoggerFactory.getLogger(clazz);
ListAppender<ILoggingEvent> loggingEventListAppender = new ListAppender<>();
loggingEventListAppender.start();
logger.addAppender(loggingEventListAppender);
return loggingEventListAppender;
}
private static final String _129K_MESSAGE = "header: value\r\n" + "012345678\r\n".repeat(13107);
private static final String _65K_MESSAGE = "header: value\r\n" + "012345678\r\n".repeat(6553);
private static final Username USER = Username.of("user@domain.org");
private static final String USER_PASS = "pass";
public static final String SMALL_MESSAGE = "header: value\r\n\r\nBODY";
private static XMLConfiguration getConfig(InputStream configStream) throws Exception {
FileBasedConfigurationBuilder<XMLConfiguration> builder = new FileBasedConfigurationBuilder<>(XMLConfiguration.class)
.configure(new Parameters()
.xml()
.setListDelimiterHandler(new DisabledListDelimiterHandler()));
XMLConfiguration xmlConfiguration = builder.getConfiguration();
FileHandler fileHandler = new FileHandler(xmlConfiguration);
fileHandler.load(configStream);
configStream.close();
return xmlConfiguration;
}
@RegisterExtension
public TestIMAPClient testIMAPClient = new TestIMAPClient();
private IMAPServer createImapServer(String configurationFile) throws Exception {
FakeAuthenticator authenticator = new FakeAuthenticator();
authenticator.addUser(USER, USER_PASS);
InMemoryIntegrationResources resources = InMemoryIntegrationResources.builder()
.authenticator(authenticator)
.authorizator(FakeAuthorizator.defaultReject())
.inVmEventBus()
.defaultAnnotationLimits()
.defaultMessageParser()
.scanningSearchIndex()
.noPreDeletionHooks()
.storeQuotaManager()
.build();
RecordingMetricFactory metricFactory = new RecordingMetricFactory();
IMAPServer imapServer = new IMAPServer(
DefaultImapDecoderFactory.createDecoder(),
new DefaultImapEncoderFactory().buildImapEncoder(),
DefaultImapProcessorFactory.createXListSupportingProcessor(
resources.getMailboxManager(),
resources.getEventBus(),
new StoreSubscriptionManager(resources.getMailboxManager().getMapperFactory()),
null,
resources.getQuotaManager(),
resources.getQuotaRootResolver(),
metricFactory),
new ImapMetrics(metricFactory));
Configuration configuration = Configuration.builder()
.workingDirectory("../")
.configurationFromClasspath()
.build();
FileSystemImpl fileSystem = new FileSystemImpl(configuration.directories());
imapServer.setFileSystem(fileSystem);
imapServer.configure(getConfig(ClassLoaderUtils.getSystemResourceAsSharedStream(configurationFile)));
imapServer.init();
return imapServer;
}
@Nested
class NoLimit {
IMAPServer imapServer;
private int port;
@BeforeEach
void beforeEach() throws Exception {
imapServer = createImapServer("imapServerNoLimits.xml");
port = imapServer.getListenAddresses().get(0).getPort();
}
@AfterEach
void tearDown() {
imapServer.destroy();
}
@Test
void smallAppendsShouldWork() throws Exception {
assertThatCode(() ->
testIMAPClient.connect("127.0.0.1", port)
.login(USER.asString(), USER_PASS)
.append("INBOX", SMALL_MESSAGE))
.doesNotThrowAnyException();
assertThat(testIMAPClient.select("INBOX")
.readFirstMessage())
.contains("\r\n" + SMALL_MESSAGE + ")\r\n");
}
@Test
void mediumAppendsShouldWork() throws Exception {
assertThatCode(() ->
testIMAPClient.connect("127.0.0.1", port)
.login(USER.asString(), USER_PASS)
.append("INBOX", _65K_MESSAGE))
.doesNotThrowAnyException();
assertThat(testIMAPClient.select("INBOX")
.readFirstMessage())
.contains("\r\n" + _65K_MESSAGE + ")\r\n");
}
@RepeatedTest(200)
void largeAppendsShouldWork() throws Exception {
assertThatCode(() ->
testIMAPClient.connect("127.0.0.1", port)
.login(USER.asString(), USER_PASS)
.append("INBOX", _129K_MESSAGE))
.doesNotThrowAnyException();
assertThat(testIMAPClient.select("INBOX")
.readFirstMessage())
.contains("\r\n" + _129K_MESSAGE + ")\r\n");
}
}
@Nested
class StartTLS {
IMAPServer imapServer;
private int port;
private Connection connection;
private ConcurrentLinkedDeque<String> responses;
@BeforeEach
void beforeEach() throws Exception {
imapServer = createImapServer("imapServerStartTLS.xml");
port = imapServer.getListenAddresses().get(0).getPort();
connection = TcpClient.create()
.noSSL()
.remoteAddress(() -> new InetSocketAddress(LOCALHOST_IP, port))
.option(ChannelOption.TCP_NODELAY, true)
.connectNow();
responses = new ConcurrentLinkedDeque<>();
connection.inbound().receive().asString()
.doOnNext(s -> System.out.println("A: " + s))
.doOnNext(responses::addLast)
.subscribeOn(Schedulers.elastic())
.subscribe();
}
@AfterEach
void tearDown() {
imapServer.destroy();
}
@Test
void extraLinesBatchedWithStartTLSShouldBeSanitized() throws Exception {
IMAPSClient imapClient = new IMAPSClient();
imapClient.connect("127.0.0.1", port);
assertThatThrownBy(() -> imapClient.sendCommand("STARTTLS\r\nA1 NOOP\r\n"))
.isInstanceOf(EOFException.class)
.hasMessage("Connection closed without indication.");
}
@Test
void extraLFLinesBatchedWithStartTLSShouldBeSanitized() throws Exception {
IMAPSClient imapClient = new IMAPSClient();
imapClient.connect("127.0.0.1", port);
assertThatThrownBy(() -> imapClient.sendCommand("STARTTLS\nA1 NOOP\r\n"))
.isInstanceOf(EOFException.class)
.hasMessage("Connection closed without indication.");
}
@Test
void tagsShouldBeWellSanitized() throws Exception {
IMAPSClient imapClient = new IMAPSClient();
imapClient.connect("127.0.0.1", port);
assertThatThrownBy(() -> imapClient.sendCommand("NOOP\r\n A1 STARTTLS\r\nA2 NOOP"))
.isInstanceOf(EOFException.class)
.hasMessage("Connection closed without indication.");
}
@Test
void lineFollowingStartTLSShouldBeSanitized() throws Exception {
IMAPSClient imapClient = new IMAPSClient();
imapClient.connect("127.0.0.1", port);
assertThatThrownBy(() -> imapClient.sendCommand("STARTTLS A1 NOOP\r\n"))
.isInstanceOf(EOFException.class)
.hasMessage("Connection closed without indication.");
}
@Test
void startTLSShouldFailWhenAuthenticated() throws Exception {
// Avoids session fixation attacks as described in https://www.usenix.org/system/files/sec21-poddebniak.pdf
// section 6.2
IMAPSClient imapClient = new IMAPSClient();
imapClient.connect("127.0.0.1", port);
imapClient.login(USER.asString(), USER_PASS);
int imapCode = imapClient.sendCommand("STARTTLS\r\n");
assertThat(imapCode).isEqualTo(IMAPReply.NO);
}
private void send(String format) {
connection.outbound()
.send(Mono.just(Unpooled.wrappedBuffer(format
.getBytes(StandardCharsets.UTF_8))))
.then()
.subscribe();
}
@RepeatedTest(10)
void concurrencyShouldNotLeadToCommandInjection() throws Exception {
ListAppender<ILoggingEvent> listAppender = getListAppenderForClass(AbstractChainedProcessor.class);
send("a0 STARTTLS\r\n");
send("a1 NOOP\r\n");
Thread.sleep(50);
assertThat(listAppender.list)
.filteredOn(event -> event.getFormattedMessage().contains("Processing org.apache.james.imap.message.request.NoopRequest"))
.isEmpty();
}
}
@Nested
class Limit {
IMAPServer imapServer;
private int port;
@BeforeEach
void beforeEach() throws Exception {
imapServer = createImapServer("imapServer.xml");
port = imapServer.getListenAddresses().get(0).getPort();
}
@AfterEach
void tearDown() {
imapServer.destroy();
}
@Test
void smallAppendsShouldWork() throws Exception {
assertThatCode(() ->
testIMAPClient.connect("127.0.0.1", port)
.login(USER.asString(), USER_PASS)
.append("INBOX", SMALL_MESSAGE))
.doesNotThrowAnyException();
assertThat(testIMAPClient.select("INBOX")
.readFirstMessage())
.contains("\r\n" + SMALL_MESSAGE + ")\r\n");
}
@Test
void mediumAppendsShouldWork() throws Exception {
assertThatCode(() ->
testIMAPClient.connect("127.0.0.1", port)
.login(USER.asString(), USER_PASS)
.append("INBOX", _65K_MESSAGE))
.doesNotThrowAnyException();
assertThat(testIMAPClient.select("INBOX")
.readFirstMessage())
.contains("\r\n" + _65K_MESSAGE + ")\r\n");
}
@Test
void largeAppendsShouldBeRejected() {
assertThatThrownBy(() ->
testIMAPClient.connect("127.0.0.1", port)
.login(USER.asString(), USER_PASS)
.append("INBOX", _129K_MESSAGE))
.isInstanceOf(RuntimeException.class)
.hasMessageContaining("BAD APPEND failed.");
}
}
}