Веб-почта по HTTPS на том же адресе: https://mail.jambocheck.com (порт 443)
continuous-integration/drone/pr Build is passing
continuous-integration/drone/pr Build is passing
HTTPS_PORT — второй Netty-сервер с тем же HttpHandler и тем же сертификатом, что SMTP/IMAP (Let's Encrypt от acme, перечитывается при продлении). HSTS добавляет Spring Security. docker-compose.public.yml: 443 → 8443. HTTP 8080 остаётся для прокси внутри сети. Тесты: HttpsConfigTest, всего 74; живой запуск: /api/config по HTTPS, HSTS. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
package com.mailserver.config;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression;
|
||||
import org.springframework.context.SmartLifecycle;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.http.server.reactive.ReactorHttpHandlerAdapter;
|
||||
import reactor.netty.DisposableServer;
|
||||
import reactor.netty.http.server.HttpServer;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* The web client and API over HTTPS on the mail host itself (mailserver.https.port, in public compose 8443 →
|
||||
* published 443): https://mail.jambocheck.com opens the web mail, no second name through Jambotron's Caddy.
|
||||
* Same certificate as SMTP/IMAP (Let's Encrypt from the acme service, reloaded when renewed). The plain HTTP port
|
||||
* (8080) stays for a reverse proxy inside the network (staging: domain in Jambotron).
|
||||
*/
|
||||
@Configuration
|
||||
public class HttpsConfig {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnExpression("${mailserver.https.port:0} > 0 and ${mailserver.tls.enabled:true}")
|
||||
public HttpsServer httpsServer(HttpHandler handler, MailServerConfig.SmtpTlsContext tls,
|
||||
@Value("${mailserver.smtp.host:0.0.0.0}") String host,
|
||||
@Value("${mailserver.https.port}") int port) {
|
||||
return new HttpsServer(handler, tls, host, port);
|
||||
}
|
||||
|
||||
public static class HttpsServer implements SmartLifecycle {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HttpsServer.class);
|
||||
|
||||
private final HttpHandler handler;
|
||||
private final MailServerConfig.SmtpTlsContext tls;
|
||||
private final String host;
|
||||
private final int port;
|
||||
private volatile DisposableServer server;
|
||||
|
||||
HttpsServer(HttpHandler handler, MailServerConfig.SmtpTlsContext tls, String host, int port) {
|
||||
this.handler = handler;
|
||||
this.tls = tls;
|
||||
this.host = host;
|
||||
this.port = port;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void start() {
|
||||
server = HttpServer.create()
|
||||
.host(host)
|
||||
.port(port)
|
||||
.secure(spec -> spec.sslContext(tls.context()))
|
||||
// HSTS: Spring Security adds it for HTTPS requests by itself
|
||||
.handle(new ReactorHttpHandlerAdapter(handler))
|
||||
.bindNow(Duration.ofSeconds(30));
|
||||
log.info("HTTPS (web client + API) listening on {}:{}", host, server.port());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void stop() {
|
||||
DisposableServer current = server;
|
||||
if (current != null) {
|
||||
current.disposeNow(Duration.ofSeconds(10));
|
||||
server = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isRunning() {
|
||||
return server != null;
|
||||
}
|
||||
|
||||
public int port() {
|
||||
return server == null ? -1 : server.port();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -49,6 +49,8 @@ mailserver.tls.enabled=${MAIL_TLS:true}
|
||||
mailserver.tls.cert-file=${MAIL_TLS_CERT:}
|
||||
mailserver.tls.key-file=${MAIL_TLS_KEY:}
|
||||
mailserver.tls.dir=${MAIL_TLS_DIR:./tls}
|
||||
# Web client + API over HTTPS with the same certificate (public compose: 8443 → 443); 0 — only plain HTTP (server.port)
|
||||
mailserver.https.port=${HTTPS_PORT:0}
|
||||
mailserver.smtps.port=${SMTPS_PORT:0}
|
||||
|
||||
# Outbound: off | mx (straight to recipients' MX, port 25) | smarthost (via a provider, e.g. Resend)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.mailserver.config;
|
||||
|
||||
import com.mailserver.smtp.SmtpTls;
|
||||
import io.netty.handler.ssl.SslContextBuilder;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.core.io.buffer.DefaultDataBufferFactory;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.netty.http.client.HttpClient;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** https://mail.jambocheck.com: the web client over HTTPS with the mail certificate. */
|
||||
class HttpsConfigTest {
|
||||
|
||||
@Test
|
||||
void servesTheWebAppOverTls(@TempDir Path dir) throws Exception {
|
||||
MailServerConfig.SmtpTlsContext tls = new MailServerConfig.SmtpTlsContext(SmtpTls.serverContext("", "", dir, "localhost"));
|
||||
HttpHandler handler = (request, response) -> {
|
||||
response.setStatusCode(HttpStatus.OK);
|
||||
return response.writeWith(Mono.just(DefaultDataBufferFactory.sharedInstance
|
||||
.wrap(("hello " + request.getURI().getScheme()).getBytes(StandardCharsets.UTF_8))));
|
||||
};
|
||||
io.netty.handler.ssl.SslContext trustAll = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build();
|
||||
HttpsConfig.HttpsServer server = new HttpsConfig.HttpsServer(handler, tls, "127.0.0.1", 0);
|
||||
server.start();
|
||||
try {
|
||||
assertThat(server.isRunning()).isTrue();
|
||||
String body = HttpClient.create()
|
||||
.secure(spec -> spec.sslContext(trustAll))
|
||||
.get().uri("https://127.0.0.1:" + server.port() + "/")
|
||||
.responseContent().aggregate().asString().block();
|
||||
assertThat(body).isEqualTo("hello https");
|
||||
} finally {
|
||||
server.stop();
|
||||
}
|
||||
assertThat(server.isRunning()).isFalse();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user