From 35227e4cbba7c373687ad915672a191b2bcdf8dd Mon Sep 17 00:00:00 2001 From: dboissin Date: Thu, 22 May 2025 11:47:03 +0200 Subject: [PATCH 01/11] feat: support clustered map and bus # Conflicts: # pom.xml # src/main/java/fr/wseduc/webutils/Server.java --- build.gradle | 0 src/main/java/fr/wseduc/bus/BusAddress.java | 2 +- src/main/java/fr/wseduc/sms/Sms.java | 17 ++- src/main/java/fr/wseduc/webutils/Server.java | 63 ++++++----- .../collections/SharedDataHelper.java | 101 ++++++++++++++++++ .../webutils/email/NotificationHelper.java | 28 ++--- .../java/fr/wseduc/webutils/http/Renders.java | 8 +- .../filter/AbstractQueryParamTokenFilter.java | 9 +- .../request/filter/JWTWithBasicFilter.java | 9 +- .../request/filter/UserAuthFilter.java | 28 +++-- 10 files changed, 204 insertions(+), 61 deletions(-) delete mode 100644 build.gradle create mode 100644 src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java diff --git a/build.gradle b/build.gradle deleted file mode 100644 index e69de29b..00000000 diff --git a/src/main/java/fr/wseduc/bus/BusAddress.java b/src/main/java/fr/wseduc/bus/BusAddress.java index c217def2..1c64c642 100644 --- a/src/main/java/fr/wseduc/bus/BusAddress.java +++ b/src/main/java/fr/wseduc/bus/BusAddress.java @@ -26,6 +26,6 @@ String value(); - boolean local() default true; + boolean local() default false; } diff --git a/src/main/java/fr/wseduc/sms/Sms.java b/src/main/java/fr/wseduc/sms/Sms.java index 46138408..d61192e8 100644 --- a/src/main/java/fr/wseduc/sms/Sms.java +++ b/src/main/java/fr/wseduc/sms/Sms.java @@ -21,6 +21,7 @@ import fr.wseduc.webutils.Server; import fr.wseduc.webutils.StringValidation; +import fr.wseduc.webutils.collections.SharedDataHelper; import fr.wseduc.webutils.http.Renders; import io.vertx.core.Future; import io.vertx.core.Promise; @@ -31,12 +32,12 @@ import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; -import io.vertx.core.shareddata.LocalMap; +import io.vertx.core.shareddata.AsyncMap; import org.apache.commons.lang3.StringUtils; import static fr.wseduc.webutils.Utils.handlerToAsyncHandler; +import static fr.wseduc.webutils.Utils.isNotEmpty; -//------------------- public class Sms { private static final Logger log = LoggerFactory.getLogger(Sms.class); @@ -64,14 +65,12 @@ public void init(Vertx vertx, JsonObject config) { this.eb = Server.getEventBus(vertx); this.vertx = vertx; this.config = config; - LocalMap server = vertx.sharedData().getLocalMap("server"); - if(server != null && server.get("smsProvider") != null) { - smsProvider = (String) server.get("smsProvider"); - final String node = (String) server.get("node"); - smsAddress = (node != null ? node : "") + "entcore.sms"; - } else { + SharedDataHelper.getInstance().get("server", "smsProvider").onSuccess(smsProvider -> { + if(isNotEmpty(smsProvider)) { + SmsFactory.this.smsProvider = smsProvider; + } smsAddress = "entcore.sms"; - } + }).onFailure(ex -> log.error("Error getting smsProvider configuration", ex)); } public Sms newInstance( Renders render ) { diff --git a/src/main/java/fr/wseduc/webutils/Server.java b/src/main/java/fr/wseduc/webutils/Server.java index fe3232e1..57f930b3 100644 --- a/src/main/java/fr/wseduc/webutils/Server.java +++ b/src/main/java/fr/wseduc/webutils/Server.java @@ -16,7 +16,10 @@ package fr.wseduc.webutils; +import fr.wseduc.webutils.collections.SharedDataHelper; import fr.wseduc.webutils.data.FileResolver; + +import static fr.wseduc.webutils.Utils.isNotEmpty; import static fr.wseduc.webutils.data.FileResolver.absolutePath; import fr.wseduc.webutils.http.BaseController; import fr.wseduc.webutils.http.Binding; @@ -38,10 +41,12 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.core.shareddata.LocalMap; + import org.vertx.java.core.http.RouteMatcher; import java.io.IOException; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -63,16 +68,26 @@ public abstract class Server extends AbstractVerticle { @Override public void start(Promise startPromise) throws Exception { - super.start(startPromise); - config = config(); - FileResolver.getInstance().setBasePath(config); - rm = new RouteMatcher(); - trace = TracerFactory.getTracer(this.getClass().getSimpleName()); - i18n = I18n.getInstance(); - i18n.init(vertx); - CookieHelper.getInstance().init((String) vertx - .sharedData().getLocalMap("server").get("signKey"), - (String) vertx.sharedData().getLocalMap("server").get("sameSiteValue"), log); + final Promise vertxPromise = Promise.promise(); + super.start(vertxPromise); + vertxPromise.future().onSuccess(x -> { + config = config(); + FileResolver.getInstance().setBasePath(config); + rm = new RouteMatcher(); + trace = TracerFactory.getTracer(this.getClass().getSimpleName()); + i18n = I18n.getInstance(); + i18n.init(vertx); + final SharedDataHelper sharedDataHelper = SharedDataHelper.getInstance(); + sharedDataHelper.init(vertx); + sharedDataHelper.getMulti("server", "signKey", "sameSiteValue", "httpServerOptions") + .onSuccess(serverMap -> init(startPromise, serverMap)) + .onFailure(ex -> log.error("Error loading server map for modue " + this.getClass().getSimpleName(), ex)); + }).onFailure(ex -> log.error("Error on vertx init promise", ex)); + } + + public void init(Promise startPromise, Map serverConfig) { + CookieHelper.getInstance().init( + serverConfig.get("signKey"), serverConfig.get("sameSiteValue"), log); staticRessources = vertx.sharedData().getLocalMap("staticRessources"); dev = "dev".equals(config.getString("mode")); @@ -149,36 +164,36 @@ public void handle(HttpServerRequest event) { JsonArray actions = StartupUtils.loadSecuredActions(vertx); securedActions = StartupUtils.securedActionsToMap(actions); log.info("secureaction loaded : " + actions.encode()); - if (config.getString("integration-mode","BUS").equals("HTTP")) { + if (!Arrays.asList("Starter", "AppRegistry").contains(this.getClass().getSimpleName())) { + if (config.getString("integration-mode","BUS").equals("HTTP")) { StartupUtils.sendStartup(application, StartupUtils.applyOverrideRightForRegistry(actions) , vertx, - config.getInteger("app-registry.port", 8012)); - } else { + config.getInteger("app-registry.port", 8012)); + } else { StartupUtils.sendStartup(application, StartupUtils.applyOverrideRightForRegistry(actions), - Server.getEventBus(vertx), - config.getString("app-registry.address", "wse.app.registry"), vertx); + Server.getEventBus(vertx), + config.getString("app-registry.address", "wse.app.registry"), vertx); + } } } catch (IOException e) { log.error("Error application not registred.", e); } - final HttpServerOptions httpOptions = createHttpServerOptions(); + final HttpServerOptions httpOptions = createHttpServerOptions(serverConfig.get("httpServerOptions")); vertx.createHttpServer(httpOptions) .requestHandler(rm) .listen(config.getInteger("port")) .onSuccess(e -> { - server = e; + Server.this.server = e; startPromise.tryComplete(); }) .onFailure(startPromise::tryFail); } - private HttpServerOptions createHttpServerOptions() { + private HttpServerOptions createHttpServerOptions(String httpServerOptions) { JsonObject rawHttpServerOptions = config().getJsonObject("httpServerOptions"); - if(rawHttpServerOptions == null) { - final LocalMap server = vertx.sharedData().getLocalMap("server"); - rawHttpServerOptions = (JsonObject) server.get("httpServerOptions"); - } - if(rawHttpServerOptions == null) { + if(rawHttpServerOptions == null && isNotEmpty(httpServerOptions)) { + rawHttpServerOptions = new JsonObject(httpServerOptions); + } else { rawHttpServerOptions = new JsonObject(); } return new HttpServerOptions(rawHttpServerOptions); @@ -232,7 +247,7 @@ public static EventBus getEventBus(Vertx vertx) { } protected Server addController(BaseController controller) { - log.info("add controller"); + log.info("add controller : " + controller.getClass().getName()); controller.init(vertx, config, rm, securedActions); securedUriBinding.addAll(controller.securedUriBinding()); mfaProtectedBinding.addAll(controller.getMfaProtectedBindings()); diff --git a/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java b/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java new file mode 100644 index 00000000..fd6cc724 --- /dev/null +++ b/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java @@ -0,0 +1,101 @@ +package fr.wseduc.webutils.collections; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.vertx.core.CompositeFuture; +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; +import io.vertx.core.shareddata.AsyncMap; + +public class SharedDataHelper { + + private static final Logger log = LoggerFactory.getLogger(SharedDataHelper.class); + private Vertx vertx; + + private SharedDataHelper(){} + + private static class SharedDataHolder { + private static final SharedDataHelper instance = new SharedDataHelper(); + } + + public static SharedDataHelper getInstance() { + return SharedDataHolder.instance; + } + + public void init(Vertx vertx) { + this.vertx = vertx; + } + + public Future> getAsyncMap(String mapName) { + final Promise> promise = Promise.promise(); + vertx.sharedData().getAsyncMap(mapName, ar -> { + if (ar.succeeded()) { + promise.complete(ar.result()); + } else { + promise.fail(ar.cause()); + } + }); + return promise.future(); + } + + public Future get(String mapName, K key) { + final Promise promise = Promise.promise(); + vertx.sharedData().getAsyncMap(mapName, ar -> { + if (ar.succeeded()) { + getValue(key, promise, ar.result()); + } else { + promise.fail(ar.cause()); + } + }); + return promise.future(); + } + + private Future getValue(K key, final Promise promise, AsyncMap asyncMap) { + asyncMap.get(key, ar2 -> { + if (ar2.succeeded()) { + try { + promise.complete(ar2.result()); + } catch (ClassCastException e) { + log.error("Cast error on key " + key + " : " + ar2.result(), e); + } + } else { + promise.fail(ar2.cause()); + } + }); + return promise.future(); + } + + public Future getValue(K key, AsyncMap asyncMap) { + return getValue(key, Promise.promise(), asyncMap); + } + + public Future> getMulti(String mapName, K... keys) { + final Promise> promise = Promise.promise(); + vertx.sharedData().getAsyncMap(mapName, ar -> { + if (ar.succeeded()) { + final List futures = new ArrayList<>(); + for (K key: keys) { + futures.add(getValue(key, ar.result())); + } + CompositeFuture.all(futures).onSuccess(cf -> { + final Map res = new HashMap<>(); + int i = 0; + for (K key: keys) { + res.put(key, (V) futures.get(i++).result()); + } + promise.complete(res); + }).onFailure(ex -> promise.fail(ex)); + } else { + promise.fail(ar.cause()); + } + }); + return promise.future(); + } + +} diff --git a/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java b/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java index 1e9325a6..f0feb5b4 100644 --- a/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java +++ b/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java @@ -20,6 +20,7 @@ import java.util.List; import fr.wseduc.webutils.I18n; +import fr.wseduc.webutils.collections.SharedDataHelper; import io.vertx.core.AsyncResult; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -38,24 +39,25 @@ public abstract class NotificationHelper implements SendEmail { protected final Renders render; protected static final Logger log = LoggerFactory.getLogger(NotificationHelper.class); - protected final String senderEmail; - protected final String host; + protected String senderEmail; + protected String host; public NotificationHelper(Vertx vertx, JsonObject config) { this.render = new Renders(vertx, config); - final Object encodedEmailConfig = vertx.sharedData().getLocalMap("server").get("emailConfig"); - - String defaultMail = "noreply@one1d.fr"; - String defaultHost = "http://localhost:8009"; + SharedDataHelper.getInstance().get("server", "emailConfig").onSuccess(encodedEmailConfig -> { + String defaultMail = "noreply@one1d.fr"; + String defaultHost = "http://localhost:8009"; + + if(encodedEmailConfig != null){ + JsonObject emailConfig = new JsonObject(encodedEmailConfig.toString()); + defaultMail = emailConfig.getString("email", defaultMail); + defaultHost = emailConfig.getString("host", defaultHost); + } - if(encodedEmailConfig != null){ - JsonObject emailConfig = new JsonObject(encodedEmailConfig.toString()); - defaultMail = emailConfig.getString("email", defaultMail); - defaultHost = emailConfig.getString("host", defaultHost); - } + senderEmail = config.getString("email", defaultMail); + host = config.getString("host", defaultHost); + }).onFailure(ex -> log.error("Error getting jwt signKey", ex)); - this.senderEmail = config.getString("email", defaultMail); - this.host = config.getString("host", defaultHost); } public void sendEmail(HttpServerRequest request, String to, String cc, String bcc, diff --git a/src/main/java/fr/wseduc/webutils/http/Renders.java b/src/main/java/fr/wseduc/webutils/http/Renders.java index 520f973d..9ec9066f 100644 --- a/src/main/java/fr/wseduc/webutils/http/Renders.java +++ b/src/main/java/fr/wseduc/webutils/http/Renders.java @@ -40,10 +40,11 @@ import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; -import io.vertx.core.shareddata.LocalMap; +import io.vertx.core.shareddata.AsyncMap; import fr.wseduc.webutils.I18n; import fr.wseduc.webutils.Server; +import fr.wseduc.webutils.collections.SharedDataHelper; import static fr.wseduc.webutils.Utils.isNotEmpty; @@ -79,8 +80,9 @@ protected void init(Vertx vertx, JsonObject config) this.pathPrefix = Server.getPathPrefix(config); } - LocalMap server = vertx.sharedData().getLocalMap("server"); - this.staticHost = (String) server.get("static-host"); + SharedDataHelper.getInstance().get("server", "static-host") + .onSuccess(staticHost -> Renders.this.staticHost = staticHost) + .onFailure(ex -> log.error("Error getting static-host conf", ex)); if (templateProcessor == null && vertx != null) { this.templateProcessor = new FileTemplateProcessor(vertx, "view/", false); diff --git a/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java b/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java index 3ddb8a3b..68466641 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java @@ -1,17 +1,24 @@ package fr.wseduc.webutils.request.filter; + +import fr.wseduc.webutils.collections.SharedDataHelper; import fr.wseduc.webutils.security.JWT; import fr.wseduc.webutils.security.SecureHttpServerRequest; import io.vertx.core.Handler; import io.vertx.core.Vertx; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; abstract public class AbstractQueryParamTokenFilter { + private static final Logger log = LoggerFactory.getLogger(AbstractQueryParamTokenFilter.class); public static String QUERYPARAM_TOKEN = "queryparam_token"; private JWT jwt; public AbstractQueryParamTokenFilter init(Vertx vertx) { - this.jwt = new JWT(vertx, (String) vertx.sharedData().getLocalMap("server").get("signKey"), null); + SharedDataHelper.getInstance().get("server", "signKey") + .onSuccess(signKey -> jwt = new JWT(vertx, signKey, null)) + .onFailure(ex -> log.error("Error getting jwt signKey", ex)); return this; } diff --git a/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java b/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java index a5481e37..c879fc0b 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java @@ -1,13 +1,18 @@ package fr.wseduc.webutils.request.filter; +import fr.wseduc.webutils.collections.SharedDataHelper; import fr.wseduc.webutils.security.JWT; import fr.wseduc.webutils.security.SecureHttpServerRequest; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.logging.Logger; +import io.vertx.core.logging.LoggerFactory; public class JWTWithBasicFilter { + private static final Logger log = LoggerFactory.getLogger(JWTWithBasicFilter.class); + private final AbstractBasicFilter basicFilter; private JWT jwt; @@ -16,7 +21,9 @@ public JWTWithBasicFilter(AbstractBasicFilter basicFilter) { } public void init(Vertx vertx) { - this.jwt = new JWT(vertx, (String) vertx.sharedData().getLocalMap("server").get("signKey"), null); + SharedDataHelper.getInstance().get("server", "signKey") + .onSuccess(signKey -> jwt = new JWT(vertx, signKey, null)) + .onFailure(ex -> log.error("Error getting jwt signKey", ex)); } public void validate(final SecureHttpServerRequest request, final Handler handler) { diff --git a/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java b/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java index b284fe00..dac072b0 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java @@ -16,6 +16,7 @@ package fr.wseduc.webutils.request.filter; +import fr.wseduc.webutils.collections.SharedDataHelper; import fr.wseduc.webutils.http.Renders; import fr.wseduc.webutils.request.CookieHelper; import fr.wseduc.webutils.security.SecureHttpServerRequest; @@ -23,6 +24,8 @@ import java.io.UnsupportedEncodingException; import java.net.URLEncoder; +import java.util.HashMap; +import java.util.Map; import io.vertx.core.Handler; import io.vertx.core.Vertx; @@ -30,7 +33,6 @@ import io.vertx.core.json.JsonObject; import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; -import io.vertx.core.shareddata.LocalMap; import static fr.wseduc.webutils.Utils.isNotEmpty; @@ -38,6 +40,8 @@ public class UserAuthFilter implements Filter, WithVertx { private static final Logger log = LoggerFactory.getLogger(UserAuthFilter.class); public static final String SESSION_ID = "oneSessionId"; + private static final long AUTH_REDIRECT_CACHE_DELAY = 60_000L; + private static final Map redirectAuthConfs = new HashMap<>(); private final OAuthResourceProvider oauth; private final AbstractBasicFilter basicFilter; private final JWTWithBasicFilter jwtWithBasicFilter; @@ -141,14 +145,10 @@ public static void redirectLogin(Vertx vertx, HttpServerRequest request) { location = location.split(":")[0] + ":8009"; } callBack = URLEncoder.encode(callBack, "UTF-8"); - LocalMap confServer = null; - if (vertx != null) { - confServer = vertx.sharedData().getLocalMap("server"); - } String loginUri = null; String callbackParam = null; - if (confServer != null) { - final String authLocationsString = (String) confServer.get("authLocations"); + if (!redirectAuthConfs.isEmpty()) { + final String authLocationsString = redirectAuthConfs.get("authLocations"); if (isNotEmpty(authLocationsString)) { final JsonObject authLocations = new JsonObject(authLocationsString); final JsonObject authLocation = authLocations.getJsonObject(host); @@ -157,8 +157,8 @@ public static void redirectLogin(Vertx vertx, HttpServerRequest request) { callbackParam = authLocation.getString("callbackParam"); } } else { - loginUri = (String) confServer.get("loginUri"); - callbackParam = (String) confServer.get("callbackParam"); + loginUri = redirectAuthConfs.get("loginUri"); + callbackParam = redirectAuthConfs.get("callbackParam"); } } if (loginUri != null && !loginUri.trim().isEmpty()) { @@ -195,6 +195,16 @@ public void setVertx(Vertx vertx) { if( queryParamFilter != null ) { queryParamFilter.init(vertx); } + updateCachedAuthRedirectionConfs(); + } + + private void updateCachedAuthRedirectionConfs() { + vertx.setPeriodic(AUTH_REDIRECT_CACHE_DELAY, delay -> { + SharedDataHelper.getInstance() + .getMulti("server", "authLocations", "loginUri", "callbackParam") + .onSuccess(confs -> confs.entrySet().stream().forEach(e -> redirectAuthConfs.put(e.getKey(), e.getValue()))) + .onFailure(ex -> log.error("Error updating authConf redirections", ex)); + }); } } From b1bf090e37778315701f18dac4300dea617018b0 Mon Sep 17 00:00:00 2001 From: Junior BERNARD Date: Wed, 17 Sep 2025 10:27:10 +0200 Subject: [PATCH 02/11] feat: #RBACK-117, zookeeper migration --- src/main/java/fr/wseduc/webutils/Server.java | 12 +++++++----- .../fr/wseduc/webutils/http/BaseController.java | 17 +++++++++++++++++ 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/main/java/fr/wseduc/webutils/Server.java b/src/main/java/fr/wseduc/webutils/Server.java index 57f930b3..e14eb99c 100644 --- a/src/main/java/fr/wseduc/webutils/Server.java +++ b/src/main/java/fr/wseduc/webutils/Server.java @@ -246,12 +246,14 @@ public static EventBus getEventBus(Vertx vertx) { return vertx.eventBus(); } - protected Server addController(BaseController controller) { + protected Future addController(BaseController controller) { log.info("add controller : " + controller.getClass().getName()); - controller.init(vertx, config, rm, securedActions); - securedUriBinding.addAll(controller.securedUriBinding()); - mfaProtectedBinding.addAll(controller.getMfaProtectedBindings()); - return this; + return controller.initAsync(vertx, config, rm, securedActions) + .map(e -> { + securedUriBinding.addAll(controller.securedUriBinding()); + mfaProtectedBinding.addAll(controller.getMfaProtectedBindings()); + return this; + }); } protected Server clearFilters() { diff --git a/src/main/java/fr/wseduc/webutils/http/BaseController.java b/src/main/java/fr/wseduc/webutils/http/BaseController.java index b4d352c2..4118bad5 100644 --- a/src/main/java/fr/wseduc/webutils/http/BaseController.java +++ b/src/main/java/fr/wseduc/webutils/http/BaseController.java @@ -19,6 +19,7 @@ import fr.wseduc.webutils.Controller; import fr.wseduc.webutils.Server; import fr.wseduc.webutils.security.SecuredAction; +import io.vertx.core.Future; import io.vertx.core.Vertx; import io.vertx.core.json.JsonObject; import org.vertx.java.core.http.RouteMatcher; @@ -41,4 +42,20 @@ public void init(Vertx vertx, JsonObject config, RouteMatcher rm, super.init(vertx, config, rm, securedActions); } + /** + * Same as init method but returns a Future in case the init method should + * be asynchronous. + * By default, it just calls the synchronous init method. + * @param vertx The current vertx instance + * @param config Module configuration + * @param rm Route matcher + * @param securedActions Actions + * @return A future that completes when this controller is completely initialized + */ + public Future initAsync(Vertx vertx, JsonObject config, RouteMatcher rm, + Map securedActions) { + init(vertx, config, rm, securedActions); + return Future.succeededFuture(); + } + } From aad297780fb710eb23548a55d9a0193e8e3e45cc Mon Sep 17 00:00:00 2001 From: Junior BERNARD Date: Mon, 22 Sep 2025 12:04:47 +0200 Subject: [PATCH 03/11] feat: #RBACK-131 #RBACK-132 #RBACK-135, health check probes --- src/main/java/fr/wseduc/webutils/Server.java | 119 ++++++++++++++++-- .../webutils/metrics/EventBusProbe.java | 60 +++++++++ .../webutils/metrics/HealthCheckProbe.java | 52 ++++++++ .../metrics/HealthCheckProbeResult.java | 27 ++++ 4 files changed, 246 insertions(+), 12 deletions(-) create mode 100644 src/main/java/fr/wseduc/webutils/metrics/EventBusProbe.java create mode 100644 src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbe.java create mode 100644 src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbeResult.java diff --git a/src/main/java/fr/wseduc/webutils/Server.java b/src/main/java/fr/wseduc/webutils/Server.java index e14eb99c..a27fcf5d 100644 --- a/src/main/java/fr/wseduc/webutils/Server.java +++ b/src/main/java/fr/wseduc/webutils/Server.java @@ -21,12 +21,17 @@ import static fr.wseduc.webutils.Utils.isNotEmpty; import static fr.wseduc.webutils.data.FileResolver.absolutePath; +import static io.vertx.core.Future.succeededFuture; + import fr.wseduc.webutils.http.BaseController; import fr.wseduc.webutils.http.Binding; import fr.wseduc.webutils.http.Renders; import fr.wseduc.webutils.http.StaticResource; import fr.wseduc.webutils.logging.Tracer; import fr.wseduc.webutils.logging.TracerFactory; +import fr.wseduc.webutils.metrics.EventBusProbe; +import fr.wseduc.webutils.metrics.HealthCheckProbe; +import fr.wseduc.webutils.metrics.HealthCheckProbeResult; import fr.wseduc.webutils.request.CookieHelper; import fr.wseduc.webutils.request.filter.Filter; import fr.wseduc.webutils.request.filter.SecurityHandler; @@ -45,12 +50,8 @@ import org.vertx.java.core.http.RouteMatcher; import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; +import java.util.*; +import java.util.stream.Collectors; public abstract class Server extends AbstractVerticle { @@ -65,6 +66,8 @@ public abstract class Server extends AbstractVerticle { private LocalMap staticRessources; private boolean dev; private HttpServer server; + private final List probes = new ArrayList<>(); + private long probeTimeout = 10_000L; @Override public void start(Promise startPromise) throws Exception { @@ -79,16 +82,70 @@ public void start(Promise startPromise) throws Exception { i18n.init(vertx); final SharedDataHelper sharedDataHelper = SharedDataHelper.getInstance(); sharedDataHelper.init(vertx); - sharedDataHelper.getMulti("server", "signKey", "sameSiteValue", "httpServerOptions") - .onSuccess(serverMap -> init(startPromise, serverMap)) - .onFailure(ex -> log.error("Error loading server map for modue " + this.getClass().getSimpleName(), ex)); + initializeProbes() + .onSuccess(e -> { + sharedDataHelper.getMulti("server", "signKey", "sameSiteValue", "httpServerOptions") + .onSuccess(serverMap -> init(startPromise, serverMap)) + .onFailure(ex -> log.error("Error loading server map for modue " + this.getClass().getSimpleName(), ex)); + }).onFailure(th -> startPromise.fail(th)); }).onFailure(ex -> log.error("Error on vertx init promise", ex)); } - public void init(Promise startPromise, Map serverConfig) { + /** + * Read probes from the configuration and initialize them. + * The configuration is read from the "probes" field and is a mixed list of : + * - string, which is the fully qualified name of the probe to instantiate + * - object, with the fields : + * - name, which is the fully qualified name of the probe to instantiate + * - config, which is a JsonObject containing configuration parameters for the probe + * @return A future that completes when the initialization of all probes is done. + */ + private Future initializeProbes() { + this.probeTimeout = config.getLong("probes-timeout", 10_000L); + final List> probes = new ArrayList<>(); + final JsonArray probesConf = config.getJsonArray("probes"); + if(probesConf == null) { + final EventBusProbe probe = new EventBusProbe(); + probes.add(probe.init(vertx, null).map(probe)); + } else { + for (Object o : probesConf) { + final String probeClassName; + final JsonObject conf; + if(o instanceof String) { + probeClassName = (String) o; + conf = new JsonObject(); + } else if(o instanceof JsonObject) { + final JsonObject jo = (JsonObject) o; + probeClassName = jo.getString("name"); + conf = jo.getJsonObject("config"); + } else { + log.error("We expect the probes to be a list of string with the name of the probes or an object"); + continue; + } + try { + final Class probeClass = Class.forName(probeClassName); + if(!HealthCheckProbe.class.isAssignableFrom(probeClass)) { + log.error("Specified class " + probeClassName + " is not a probe class"); + continue; + } + final HealthCheckProbe probe = (HealthCheckProbe) probeClass.newInstance(); + probes.add(probe.init(vertx, conf).map(probe)); + } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) { + log.error("Cannot instantiate probe " + probeClassName, e); + } + } + } + return Future.all(probes) + .map(CompositeFuture::list) + .onSuccess(ps -> this.probes.addAll((List)ps)) + .mapEmpty(); + } + + + public void init(Promise startPromise, Map serverConfig) { CookieHelper.getInstance().init( serverConfig.get("signKey"), serverConfig.get("sameSiteValue"), log); - staticRessources = vertx.sharedData().getLocalMap("staticRessources"); + staticRessources = vertx.sharedData().getLocalMap("staticRessources"); // TODO JBER dev = "dev".equals(config.getString("mode")); log.info("Verticle: " + this.getClass().getSimpleName() + " starts on port: " + config.getInteger("port")); @@ -140,6 +197,28 @@ public void handle(HttpServerRequest request) { } }); + rm.get(prefix + "/health/liveness", event -> Controller.renderJson(event, new JsonObject().put("test", "ok"))); + + rm.get(prefix + "/health/readiness", event -> { + final List> futures = probes.stream() + .map(this::executeProbeWithTimeout) + .collect(Collectors.toList()); + Future.join(futures) + .onSuccess(res -> { + final JsonObject result = mergeProbes(res.list()); + boolean hasKO = res.list().stream().anyMatch(p -> !p.isOk()); + if(hasKO) { + Controller.renderError(event, result); + } else { + Controller.renderJson(event, result); + } + }) + .onFailure(th -> { + log.error("An error occurred while getting readiness probe", th); + Controller.renderError(event); + }); + }); + rm.get(prefix + "/monitoring", new Handler() { @Override public void handle(HttpServerRequest event) { @@ -189,7 +268,23 @@ public void handle(HttpServerRequest event) { .onFailure(startPromise::tryFail); } - private HttpServerOptions createHttpServerOptions(String httpServerOptions) { + private Future executeProbeWithTimeout(HealthCheckProbe healthCheckProbe) { + try { + return healthCheckProbe.probe(probeTimeout); + } catch (RuntimeException e) { + return succeededFuture(new HealthCheckProbeResult(healthCheckProbe.getName(), false, new JsonObject().put("exception", e.getMessage()))); + } + } + + private JsonObject mergeProbes(final List results) { + final JsonObject merged = new JsonObject(); + for (HealthCheckProbeResult result : results) { + merged.put(result.getName(), JsonObject.mapFrom(result)); + } + return merged; + } + + private HttpServerOptions createHttpServerOptions(String httpServerOptions) { JsonObject rawHttpServerOptions = config().getJsonObject("httpServerOptions"); if(rawHttpServerOptions == null && isNotEmpty(httpServerOptions)) { rawHttpServerOptions = new JsonObject(httpServerOptions); diff --git a/src/main/java/fr/wseduc/webutils/metrics/EventBusProbe.java b/src/main/java/fr/wseduc/webutils/metrics/EventBusProbe.java new file mode 100644 index 00000000..4c2b9916 --- /dev/null +++ b/src/main/java/fr/wseduc/webutils/metrics/EventBusProbe.java @@ -0,0 +1,60 @@ +package fr.wseduc.webutils.metrics; + +import io.vertx.core.Future; +import io.vertx.core.Handler; +import io.vertx.core.Vertx; +import io.vertx.core.eventbus.DeliveryOptions; +import io.vertx.core.eventbus.EventBus; +import io.vertx.core.eventbus.Message; +import io.vertx.core.json.JsonObject; + +import static java.lang.System.currentTimeMillis; + +/** + * Checks the round-time trip of a message on the event bus. + */ +public class EventBusProbe implements HealthCheckProbe { + private String probesSubject; + private Vertx vertx; + private EventBus eventBus; + private String probeName; + private boolean local; + private long maxTime; + @Override + public Future init(final Vertx vertx, final JsonObject config) { + final JsonObject configuration = config == null ? new JsonObject() : config; + if(probesSubject != null && !probesSubject.isEmpty()) { + throw new IllegalStateException("probe already initialized"); + } + local = configuration.getBoolean("local", true); + maxTime = configuration.getLong("max-time", 20L); + probeName = "eventbus-" + (local ? "local" : "remote"); + probesSubject = "probes." + currentTimeMillis(); + this.vertx = vertx; + eventBus = vertx.eventBus(); + final Handler> handler = e -> e.reply(new JsonObject().put("status", "ok")); + if(local) { + eventBus.localConsumer(probesSubject).handler(handler); + } else { + eventBus.consumer(probesSubject).handler(handler); + } + return Future.succeededFuture(); + } + + @Override + public String getName() { + return probeName; + } + + @Override + public Vertx getVertx() { + return vertx; + } + + @Override + public Future probe() { + final long start = currentTimeMillis(); + return eventBus.request(probesSubject, new JsonObject().put("payload", "42"), new DeliveryOptions().setLocalOnly(local)) + .map(response -> new HealthCheckProbeResult(getName(), (currentTimeMillis() - start) <= maxTime, null)); + } +} diff --git a/src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbe.java b/src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbe.java new file mode 100644 index 00000000..b22e7a79 --- /dev/null +++ b/src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbe.java @@ -0,0 +1,52 @@ +package fr.wseduc.webutils.metrics; + +import io.vertx.core.Future; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.json.JsonObject; + +import static java.lang.System.currentTimeMillis; + +/** + * A probe to be used to detect that an underlying component is alive and reachable or not. + */ +public interface HealthCheckProbe { + /** + * Initialize the probe. + * @param vertx Vertx instance + * @param config Probe configuration if any (can be {@code null}) + * @return A future that completes when the initialization is done + */ + Future init(final Vertx vertx, final JsonObject config); + + /** + * Perform the actual probe logic (will be cancelled after {@code timeout} milliseconds. + * @param timeout Time after which the probe is cancelled + * @return A future that returns the probe evaluation + */ + default Future probe(final long timeout) { + final Promise promise = Promise.promise(); + final Vertx vertx = getVertx(); + final long start = currentTimeMillis(); + long timer = vertx.setTimer(timeout, e -> promise.tryComplete(new HealthCheckProbeResult( + this.getName(), false, + new JsonObject().put("delay", currentTimeMillis() - start).put("aborted", true)))); + probe().onSuccess(e -> { + vertx.cancelTimer(timer); + final long delay = currentTimeMillis() - start; + JsonObject md = e.getMetadata(); + if(md == null) { + md = new JsonObject(); + } + promise.tryComplete(new HealthCheckProbeResult(e.getName(), e.isOk(), md.put("delay", delay))); + }).onFailure(promise::fail); + return promise.future(); + } + + /** The unique name of this probe. */ + String getName(); + /** The vertx instance attached to this probe.*/ + Vertx getVertx(); + /** The actual probe logic which should be defined by the concrete implementations.*/ + Future probe(); +} diff --git a/src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbeResult.java b/src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbeResult.java new file mode 100644 index 00000000..307e72c9 --- /dev/null +++ b/src/main/java/fr/wseduc/webutils/metrics/HealthCheckProbeResult.java @@ -0,0 +1,27 @@ +package fr.wseduc.webutils.metrics; + +import io.vertx.core.json.JsonObject; + +public class HealthCheckProbeResult { + private final String name; + private final boolean ok; + private final JsonObject metadata; + + public HealthCheckProbeResult(String name, boolean ok, JsonObject metadata) { + this.name = name; + this.ok = ok; + this.metadata = metadata; + } + + public String getName() { + return name; + } + + public boolean isOk() { + return ok; + } + + public JsonObject getMetadata() { + return metadata; + } +} From e6fa559f6e1b7263eef678a09958d7c8437fc113 Mon Sep 17 00:00:00 2001 From: Junior BERNARD Date: Wed, 24 Sep 2025 11:38:38 +0200 Subject: [PATCH 04/11] feat: #RBACK-126, allow JSON logging --- .../wseduc/webutils/request/AccessLogger.java | 69 +++++++++++-------- 1 file changed, 40 insertions(+), 29 deletions(-) diff --git a/src/main/java/fr/wseduc/webutils/request/AccessLogger.java b/src/main/java/fr/wseduc/webutils/request/AccessLogger.java index 30393aea..91363da3 100644 --- a/src/main/java/fr/wseduc/webutils/request/AccessLogger.java +++ b/src/main/java/fr/wseduc/webutils/request/AccessLogger.java @@ -17,19 +17,22 @@ package fr.wseduc.webutils.request; import fr.wseduc.webutils.http.Renders; -import static fr.wseduc.webutils.request.RequestUtils.getTokenHeader; -import static fr.wseduc.webutils.request.RequestUtils.getUserAgent; import fr.wseduc.webutils.security.SecureHttpServerRequest; import io.vertx.core.Handler; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.json.JsonObject; -import io.vertx.core.logging.LoggerFactory; + +import java.time.Instant; + +import static fr.wseduc.webutils.request.RequestUtils.getTokenHeader; +import static fr.wseduc.webutils.request.RequestUtils.getUserAgent; import static org.apache.commons.lang3.StringUtils.isBlank; public class AccessLogger { - protected static final io.vertx.core.logging.Logger log = LoggerFactory.getLogger(AccessLogger.class); - public static final String UNAUTHENTICATED_USER_ID = "unauthenticated"; + protected static final java.util.logging.Logger log = java.util.logging.Logger.getLogger("ACCESS"); + + public static final String UNAUTHENTICATED_USER_ID = "unauthenticated"; public static final String NO_SESSION_COOKIE = "nocookie"; public static final String NO_TOKEN_ID = "notoken"; @@ -44,35 +47,43 @@ public class AccessLogger { * @param handler Downstream process ({@code null} will always be supplied */ public void log(HttpServerRequest request, Handler handler) { - log.trace(formatLog(request)); + log.finest(formatLog(request, null)); handler.handle(null); } - protected String formatLog(final HttpServerRequest request) { - return String.format("\"%s\" \"%s %s%s\" \"%s\"%s", - Renders.getIp(request), request.method(), - request.path(), getQuery(request), - getUserAgent(request), getAuthenticatedUserInfo(request)); + protected String formatLog(final HttpServerRequest request, final String userId) { + JsonObject logEntry = new JsonObject() + .put("timestamp", Instant.now().toString()) + .put("ip", Renders.getIp(request)) + .put("method", request.method().toString()) + .put("path", request.path()) + .put("query", request.query()) + .put("userAgent", getUserAgent(request)); + if(userId != null) { + logEntry.put("userId", userId); + } + + if(request instanceof SecureHttpServerRequest) { + JsonObject auth = getAuthenticatedUserInfo((SecureHttpServerRequest) request); + logEntry.mergeIn(auth); + } + + return logEntry.encode(); } - private String getAuthenticatedUserInfo(final HttpServerRequest request) { - if(request instanceof SecureHttpServerRequest) { - final String userId; - final String tokenId; - final String cookieId; - final JsonObject session = ((SecureHttpServerRequest) request).getSession(); - if(session == null || isBlank(session.getString("externalId"))) { - userId = UNAUTHENTICATED_USER_ID; - } else { - userId = session.getString("externalId"); - } - tokenId = getTokenHeader(request).orElse(NO_TOKEN_ID); - final String sessionId = CookieHelper.getInstance().getSigned("oneSessionId", request); - cookieId = isBlank(sessionId) ? NO_SESSION_COOKIE : sessionId; - return String.format(" - %s %s %s", userId, cookieId, tokenId); - } else { - return ""; - } + private JsonObject getAuthenticatedUserInfo(final SecureHttpServerRequest request) { + final JsonObject session = request.getSession(); + final String userId = (session == null || isBlank(session.getString("externalId"))) + ? UNAUTHENTICATED_USER_ID + : session.getString("externalId"); + final String tokenId = getTokenHeader(request).orElse(NO_TOKEN_ID); + final String sessionId = CookieHelper.getInstance().getSigned("oneSessionId", request); + final String cookieId = isBlank(sessionId) ? NO_SESSION_COOKIE : sessionId; + + return new JsonObject() + .put("userId", userId) + .put("sessionId", cookieId) + .put("tokenId", tokenId); } private String getQuery(HttpServerRequest request) { From a690ef722e3e6d149bbad731cad9383087cf1c40 Mon Sep 17 00:00:00 2001 From: Junior BERNARD Date: Mon, 6 Oct 2025 15:45:52 +0200 Subject: [PATCH 05/11] chore: set zookeeper version --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 825a5b2c..8884638a 100644 --- a/pom.xml +++ b/pom.xml @@ -12,7 +12,7 @@ fr.wseduc web-utils - 3.2-SNAPSHOT + 3.2-zookeeper-SNAPSHOT From 768cc376958b18d67509093f822ae29f3b64adb5 Mon Sep 17 00:00:00 2001 From: Junior BERNARD Date: Fri, 10 Oct 2025 14:28:34 +0200 Subject: [PATCH 06/11] fix: prevent NPE for modules without securedActions --- src/main/java/fr/wseduc/webutils/Controller.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/fr/wseduc/webutils/Controller.java b/src/main/java/fr/wseduc/webutils/Controller.java index 46c9fd57..e514a52a 100644 --- a/src/main/java/fr/wseduc/webutils/Controller.java +++ b/src/main/java/fr/wseduc/webutils/Controller.java @@ -253,7 +253,7 @@ private Handler bindHandler(String method) { if (method == null || method.trim().isEmpty()) { throw new NullPointerException(); } - if (securedActions.containsKey(this.getClass().getName() + "|" + method)) { + if (securedActions!= null && securedActions.containsKey(this.getClass().getName() + "|" + method)) { return executeSecure(method); } return execute(method); @@ -277,7 +277,7 @@ public Map> getUriBinding() { public Map> getSecuredUriBinding() { Map> bindings = new HashMap<>(); for (Entry> e : this.uriBinding.entrySet()) { - if (securedActions.containsKey(e.getKey())) { + if (securedActions!= null && securedActions.containsKey(e.getKey())) { bindings.put(e.getKey(), e.getValue()); } } @@ -287,7 +287,7 @@ public Map> getSecuredUriBinding() { public Set securedUriBinding() { Set bindings = new HashSet<>(); for (Entry> e : this.uriBinding.entrySet()) { - if (securedActions.containsKey(e.getKey())) { + if (securedActions != null && securedActions.containsKey(e.getKey())) { bindings.addAll(e.getValue()); } } @@ -384,7 +384,7 @@ private void addRegEx(String input, HttpMethod httpMethod, String method) { } private String findOverride(String serviceMethod) { - if(!securedActions.containsKey(serviceMethod)) { + if(securedActions == null || !securedActions.containsKey(serviceMethod)) { return null; } return securedActions.get(serviceMethod).getRight(); From 11899196cbdec10f4ccefe100eaedeec20c7b45d Mon Sep 17 00:00:00 2001 From: Junior Bernard Date: Fri, 24 Oct 2025 16:12:13 +0200 Subject: [PATCH 07/11] feat: #RBACK-127, lock imports and exports --- .../webutils/collections/SharedDataHelper.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java b/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java index fd6cc724..6c8ee10b 100644 --- a/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java +++ b/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java @@ -12,6 +12,7 @@ import io.vertx.core.logging.Logger; import io.vertx.core.logging.LoggerFactory; import io.vertx.core.shareddata.AsyncMap; +import io.vertx.core.shareddata.Lock; public class SharedDataHelper { @@ -32,6 +33,19 @@ public void init(Vertx vertx) { this.vertx = vertx; } + public Future getLock(final String lockName, final long timeout) { + return this.vertx.sharedData().getLockWithTimeout(lockName, timeout); + } + + public Future releaseLockAfterDelay(final Lock lock, final long delay) { + return Future.future(p -> { + vertx.setTimer(delay, e -> { + lock.release(); + p.complete(); + }); + }); + } + public Future> getAsyncMap(String mapName) { final Promise> promise = Promise.promise(); vertx.sharedData().getAsyncMap(mapName, ar -> { From 428c512321b429d3fe2c8cce6be97ecf7af661b8 Mon Sep 17 00:00:00 2001 From: Junior Bernard Date: Tue, 28 Oct 2025 15:06:57 +0100 Subject: [PATCH 08/11] feat: #RBACK-164, retry app-registry registration --- .../java/fr/wseduc/webutils/StartupUtils.java | 86 +++++++++++++------ 1 file changed, 62 insertions(+), 24 deletions(-) diff --git a/src/main/java/fr/wseduc/webutils/StartupUtils.java b/src/main/java/fr/wseduc/webutils/StartupUtils.java index 4faefa7e..4abd41f9 100644 --- a/src/main/java/fr/wseduc/webutils/StartupUtils.java +++ b/src/main/java/fr/wseduc/webutils/StartupUtils.java @@ -29,6 +29,8 @@ import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; import io.vertx.core.eventbus.Message; +import io.vertx.core.eventbus.ReplyException; +import io.vertx.core.eventbus.ReplyFailure; import io.vertx.core.file.FileProps; import io.vertx.core.http.*; import io.vertx.core.json.JsonArray; @@ -44,11 +46,20 @@ public class StartupUtils { private static final Logger log = LoggerFactory.getLogger(StartupUtils.class); - public static void sendStartup(final JsonObject app, JsonArray actions, final Vertx vertx, Integer appRegistryPort) throws IOException { + /** Wait 3 seconds between a failed attempt to send a startup signal and the next attempt.*/ + private static final long DELAY_BETWEEN_STARTUP_ATTEMPTS = 3_000L; + + public static void sendStartup(final JsonObject app, JsonArray actions, final Vertx vertx, Integer appRegistryPort) throws IOException { + sendStartup(app, actions, vertx, appRegistryPort, 1); + } + private static void sendStartup(final JsonObject app, JsonArray actions, final Vertx vertx, Integer appRegistryPort, final int attempt) throws IOException { + final JsonArray actionsToSend; if (actions == null || actions.size() == 0) { - actions = loadSecuredActions(vertx); - } - final String s = new JsonObject().put("application", app).put("actions", actions).encode(); + actionsToSend = loadSecuredActions(vertx); + } else { + actionsToSend = actions; + } + final String s = new JsonObject().put("application", app).put("actions", actionsToSend).encode(); final HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions() .setDefaultHost("localhost").setDefaultPort(appRegistryPort).setKeepAlive(false)); httpClient.request(HttpMethod.PUT, "/appregistry/application") @@ -56,9 +67,18 @@ public static void sendStartup(final JsonObject app, JsonArray actions, final Ve .flatMap(req -> req.send(s)) .onSuccess(event -> { if (event.statusCode() != 200) { - log.error("Error recording application : " + s); - httpClient.close(); - } else { + log.error("Error recording application (attempt #" + attempt + "): " + s); + httpClient.close(); + if (event.statusCode() == 404) { + vertx.setTimer(DELAY_BETWEEN_STARTUP_ATTEMPTS, e -> { + try { + sendStartup(app, actionsToSend, vertx, appRegistryPort, attempt + 1); + } catch (IOException ex) { + log.error("Error recording application (attempt #" + attempt + ")", ex); + } + }); + } + } else { final JsonArray widgetsArray = loadWidgets(app.getString("name"), vertx); if(widgetsArray.isEmpty()){ httpClient.close(); @@ -83,29 +103,47 @@ public static void sendStartup(final JsonObject app, JsonArray actions, final Ve .onFailure(except -> log.error("Error sending application to appregistry : " + s, except)); } + public static void sendStartup(final JsonObject app, JsonArray actions, final EventBus eb, final String address, final Vertx vertx, + final Handler>> handler) throws IOException { + sendStartup(app, actions, eb, address, vertx, 1, handler); + } public static void sendStartup(final JsonObject app, JsonArray actions, final EventBus eb, final String address, final Vertx vertx, - final Handler>> handler) throws IOException { - if (actions == null || actions.size() == 0) { - actions = loadSecuredActions(vertx); - } + final int attempt, final Handler>> handler) throws IOException { + final JsonArray actionsToSend; + if (actions == null || actions.size() == 0) { + actionsToSend = loadSecuredActions(vertx); + } else { + actionsToSend = actions; + } JsonObject jo = new JsonObject(); jo.put("application", app) - .put("actions", applyOverrideRightForRegistry(actions)); + .put("actions", applyOverrideRightForRegistry(actionsToSend)); eb.request(address, jo, (AsyncResult> appEvent) -> { - if(appEvent.failed()){ - log.error("Error registering application " + app.getString("name"), appEvent.cause()); - if(handler != null) handler.handle(appEvent); - return; - } + if(appEvent.failed()){ + log.error("Error registering application " + app.getString("name") + " (attempt #" + attempt+ ")", appEvent.cause()); + Throwable cause = appEvent.cause(); + if(cause instanceof ReplyException && ReplyFailure.NO_HANDLERS.equals(((ReplyException)cause).failureType())) { + vertx.setTimer(DELAY_BETWEEN_STARTUP_ATTEMPTS, e -> { + try { + sendStartup(app, actionsToSend, eb, address, vertx, attempt + 1, handler); + } catch (IOException ex) { + log.error("Error recording application (attempt #" + attempt + ")", ex); + } + }); + } else { + if (handler != null) handler.handle(appEvent); + return; + } + } - final JsonArray widgetsArray = loadWidgets(app.getString("name"), vertx); - if(widgetsArray.size() == 0){ - if(handler != null) handler.handle(appEvent); - return; - } + final JsonArray widgetsArray = loadWidgets(app.getString("name"), vertx); + if(widgetsArray.size() == 0){ + if(handler != null) handler.handle(appEvent); + return; + } - final JsonObject widgets = new JsonObject().put("widgets", widgetsArray); - eb.request(address+".widgets", widgets, (Handler>>) event -> { + final JsonObject widgets = new JsonObject().put("widgets", widgetsArray); + eb.request(address+".widgets", widgets, (Handler>>) event -> { if(event.failed()){ log.error("Error registering widgets for application " + app.getString("name"), event.cause()); } else { From 02faf337d445f4488a7efc6291b7da489116c289 Mon Sep 17 00:00:00 2001 From: Junior Bernard Date: Thu, 30 Oct 2025 17:06:22 +0100 Subject: [PATCH 09/11] wip --- pom.xml | 2 +- src/main/java/fr/wseduc/sms/Sms.java | 2 +- src/main/java/fr/wseduc/webutils/Server.java | 2 +- .../collections/SharedDataHelper.java | 47 +++++++++++++++++++ .../webutils/email/NotificationHelper.java | 2 +- .../java/fr/wseduc/webutils/http/Renders.java | 2 +- .../filter/AbstractQueryParamTokenFilter.java | 2 +- .../request/filter/JWTWithBasicFilter.java | 2 +- .../request/filter/UserAuthFilter.java | 2 +- 9 files changed, 55 insertions(+), 8 deletions(-) diff --git a/pom.xml b/pom.xml index 8884638a..0d3f1d7f 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ io.edifice edifice-parent - 1.0.1 + 1.1-SNAPSHOT fr.wseduc diff --git a/src/main/java/fr/wseduc/sms/Sms.java b/src/main/java/fr/wseduc/sms/Sms.java index d61192e8..25951d3d 100644 --- a/src/main/java/fr/wseduc/sms/Sms.java +++ b/src/main/java/fr/wseduc/sms/Sms.java @@ -65,7 +65,7 @@ public void init(Vertx vertx, JsonObject config) { this.eb = Server.getEventBus(vertx); this.vertx = vertx; this.config = config; - SharedDataHelper.getInstance().get("server", "smsProvider").onSuccess(smsProvider -> { + SharedDataHelper.getInstance().getLocal("server", "smsProvider").onSuccess(smsProvider -> { if(isNotEmpty(smsProvider)) { SmsFactory.this.smsProvider = smsProvider; } diff --git a/src/main/java/fr/wseduc/webutils/Server.java b/src/main/java/fr/wseduc/webutils/Server.java index a27fcf5d..80f707c7 100644 --- a/src/main/java/fr/wseduc/webutils/Server.java +++ b/src/main/java/fr/wseduc/webutils/Server.java @@ -84,7 +84,7 @@ public void start(Promise startPromise) throws Exception { sharedDataHelper.init(vertx); initializeProbes() .onSuccess(e -> { - sharedDataHelper.getMulti("server", "signKey", "sameSiteValue", "httpServerOptions") + sharedDataHelper.getLocalMulti("server", "signKey", "sameSiteValue", "httpServerOptions") .onSuccess(serverMap -> init(startPromise, serverMap)) .onFailure(ex -> log.error("Error loading server map for modue " + this.getClass().getSimpleName(), ex)); }).onFailure(th -> startPromise.fail(th)); diff --git a/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java b/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java index 6c8ee10b..6b992815 100644 --- a/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java +++ b/src/main/java/fr/wseduc/webutils/collections/SharedDataHelper.java @@ -46,6 +46,18 @@ public Future releaseLockAfterDelay(final Lock lock, final long delay) { }); } + public Future> getLocalAsyncMap(String mapName) { + final Promise> promise = Promise.promise(); + vertx.sharedData().getLocalAsyncMap(mapName, ar -> { + if (ar.succeeded()) { + promise.complete(ar.result()); + } else { + promise.fail(ar.cause()); + } + }); + return promise.future(); + } + public Future> getAsyncMap(String mapName) { final Promise> promise = Promise.promise(); vertx.sharedData().getAsyncMap(mapName, ar -> { @@ -70,6 +82,18 @@ public Future get(String mapName, K key) { return promise.future(); } + public Future getLocal(String mapName, K key) { + final Promise promise = Promise.promise(); + vertx.sharedData().getLocalAsyncMap(mapName, ar -> { + if (ar.succeeded()) { + getValue(key, promise, ar.result()); + } else { + promise.fail(ar.cause()); + } + }); + return promise.future(); + } + private Future getValue(K key, final Promise promise, AsyncMap asyncMap) { asyncMap.get(key, ar2 -> { if (ar2.succeeded()) { @@ -112,4 +136,27 @@ public Future> getMulti(String mapName, K... keys) { return promise.future(); } + public Future> getLocalMulti(String mapName, K... keys) { + final Promise> promise = Promise.promise(); + vertx.sharedData().getLocalAsyncMap(mapName, ar -> { + if (ar.succeeded()) { + final List futures = new ArrayList<>(); + for (K key: keys) { + futures.add(getValue(key, ar.result())); + } + CompositeFuture.all(futures).onSuccess(cf -> { + final Map res = new HashMap<>(); + int i = 0; + for (K key: keys) { + res.put(key, (V) futures.get(i++).result()); + } + promise.complete(res); + }).onFailure(ex -> promise.fail(ex)); + } else { + promise.fail(ar.cause()); + } + }); + return promise.future(); + } + } diff --git a/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java b/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java index f0feb5b4..5f2f51ea 100644 --- a/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java +++ b/src/main/java/fr/wseduc/webutils/email/NotificationHelper.java @@ -44,7 +44,7 @@ public abstract class NotificationHelper implements SendEmail { public NotificationHelper(Vertx vertx, JsonObject config) { this.render = new Renders(vertx, config); - SharedDataHelper.getInstance().get("server", "emailConfig").onSuccess(encodedEmailConfig -> { + SharedDataHelper.getInstance().getLocal("server", "emailConfig").onSuccess(encodedEmailConfig -> { String defaultMail = "noreply@one1d.fr"; String defaultHost = "http://localhost:8009"; diff --git a/src/main/java/fr/wseduc/webutils/http/Renders.java b/src/main/java/fr/wseduc/webutils/http/Renders.java index 9ec9066f..1adbf91b 100644 --- a/src/main/java/fr/wseduc/webutils/http/Renders.java +++ b/src/main/java/fr/wseduc/webutils/http/Renders.java @@ -80,7 +80,7 @@ protected void init(Vertx vertx, JsonObject config) this.pathPrefix = Server.getPathPrefix(config); } - SharedDataHelper.getInstance().get("server", "static-host") + SharedDataHelper.getInstance().getLocal("server", "static-host") .onSuccess(staticHost -> Renders.this.staticHost = staticHost) .onFailure(ex -> log.error("Error getting static-host conf", ex)); diff --git a/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java b/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java index 68466641..37ca440c 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/AbstractQueryParamTokenFilter.java @@ -16,7 +16,7 @@ abstract public class AbstractQueryParamTokenFilter { private JWT jwt; public AbstractQueryParamTokenFilter init(Vertx vertx) { - SharedDataHelper.getInstance().get("server", "signKey") + SharedDataHelper.getInstance().getLocal("server", "signKey") .onSuccess(signKey -> jwt = new JWT(vertx, signKey, null)) .onFailure(ex -> log.error("Error getting jwt signKey", ex)); return this; diff --git a/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java b/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java index c879fc0b..99cda932 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/JWTWithBasicFilter.java @@ -21,7 +21,7 @@ public JWTWithBasicFilter(AbstractBasicFilter basicFilter) { } public void init(Vertx vertx) { - SharedDataHelper.getInstance().get("server", "signKey") + SharedDataHelper.getInstance().getLocal("server", "signKey") .onSuccess(signKey -> jwt = new JWT(vertx, signKey, null)) .onFailure(ex -> log.error("Error getting jwt signKey", ex)); } diff --git a/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java b/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java index dac072b0..7a7fd966 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/UserAuthFilter.java @@ -201,7 +201,7 @@ public void setVertx(Vertx vertx) { private void updateCachedAuthRedirectionConfs() { vertx.setPeriodic(AUTH_REDIRECT_CACHE_DELAY, delay -> { SharedDataHelper.getInstance() - .getMulti("server", "authLocations", "loginUri", "callbackParam") + .getLocalMulti("server", "authLocations", "loginUri", "callbackParam") .onSuccess(confs -> confs.entrySet().stream().forEach(e -> redirectAuthConfs.put(e.getKey(), e.getValue()))) .onFailure(ex -> log.error("Error updating authConf redirections", ex)); }); From 5e1c9897c585c61416d79efe4248b9161cfd0f05 Mon Sep 17 00:00:00 2001 From: vbillard91 Date: Wed, 17 Dec 2025 09:55:44 +0100 Subject: [PATCH 10/11] feature: #RBACK-138 Add trace id on endpoint handler + add log on enpoints call --- .../java/fr/wseduc/webutils/Controller.java | 112 ++++++++++++------ .../webutils/http/TraceIdContextHandler.java | 56 +++++++++ .../request/filter/SecurityHandler.java | 26 ++-- .../security/WrappedHttpServerRequest.java | 4 +- 4 files changed, 153 insertions(+), 45 deletions(-) create mode 100644 src/main/java/fr/wseduc/webutils/http/TraceIdContextHandler.java diff --git a/src/main/java/fr/wseduc/webutils/Controller.java b/src/main/java/fr/wseduc/webutils/Controller.java index e514a52a..769f43b8 100644 --- a/src/main/java/fr/wseduc/webutils/Controller.java +++ b/src/main/java/fr/wseduc/webutils/Controller.java @@ -16,35 +16,35 @@ package fr.wseduc.webutils; -import java.io.*; -import java.lang.invoke.MethodHandle; -import java.lang.invoke.MethodHandles; -import java.lang.invoke.MethodType; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - +import fr.wseduc.webutils.http.Binding; +import fr.wseduc.webutils.http.HttpMethod; +import fr.wseduc.webutils.http.Renders; +import fr.wseduc.webutils.http.TraceIdContextHandler; import fr.wseduc.webutils.request.AccessLogger; +import fr.wseduc.webutils.request.filter.SecurityHandler; import fr.wseduc.webutils.request.filter.XSSHandler; +import fr.wseduc.webutils.security.ActionType; +import fr.wseduc.webutils.security.SecuredAction; +import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.eventbus.EventBus; import io.vertx.core.eventbus.Message; import io.vertx.core.http.HttpServerRequest; import io.vertx.core.json.JsonObject; - -import fr.wseduc.webutils.http.Binding; -import fr.wseduc.webutils.http.HttpMethod; -import fr.wseduc.webutils.http.Renders; -import fr.wseduc.webutils.request.filter.SecurityHandler; -import fr.wseduc.webutils.security.ActionType; -import fr.wseduc.webutils.security.SecuredAction; +import org.apache.commons.lang3.time.StopWatch; import org.vertx.java.core.http.RouteMatcher; +import java.io.*; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import java.util.*; +import java.util.Map.Entry; +import java.util.concurrent.TimeUnit; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + public abstract class Controller extends Renders { private static final MethodHandles.Lookup lookup = MethodHandles.publicLookup(); @@ -56,6 +56,9 @@ public abstract class Controller extends Renders { protected EventBus eb; protected String busPrefix = ""; private AccessLogger accessLogger; + public static final String TRACE_ID = "X-Cloud-Trace-Context"; + public static final String TRACE_MTTR = "Trace-MTTR"; + private boolean logRestAccess = false; public Controller(Vertx vertx, JsonObject config, RouteMatcher rm, Map securedActions) { @@ -79,6 +82,9 @@ protected void init(Vertx vertx, JsonObject config, RouteMatcher rm, this.rm = rm; this.securedActions = securedActions; this.eb = Server.getEventBus(vertx); + if (config != null) { + logRestAccess = config.getBoolean("log-rest-access", false); + } if (rm != null) { loadRoutes(); } else { @@ -162,13 +168,14 @@ private Handler execute(final String method) { try { final MethodHandle mh = lookup.bind(this, method, MethodType.methodType(void.class, HttpServerRequest.class)); + final String qualifiedName = this.getClass().getName() + "|" + method; return new XSSHandler() { @Override public void filter(final HttpServerRequest request) { accessLogger.log(request, v -> { try { - mh.invokeExact(request); + doExecuteInvoke(mh, request, qualifiedName, false); } catch (Throwable e) { if (!(e instanceof IllegalStateException) || !"Response is closed".equals(e.getMessage())) { @@ -196,12 +203,13 @@ private Handler executeSecure(final String method) { try { final MethodHandle mh = lookup.bind(this, method, MethodType.methodType(void.class, HttpServerRequest.class)); + final String qualifiedName = this.getClass().getName() + "|" + method; return new SecurityHandler() { @Override public void filter(HttpServerRequest request) { try { - mh.invokeExact(request); + doExecuteInvoke(mh, request, qualifiedName, true); } catch (Throwable e) { if (!(e instanceof IllegalStateException) || !"Response is closed".equals(e.getMessage())) { @@ -224,24 +232,58 @@ public void filter(HttpServerRequest request) { } } + private void doExecuteInvoke(MethodHandle mh, HttpServerRequest request, + String qualifiedName, boolean secured) throws Throwable { + final Context ctx = Vertx.currentContext(); + String traceId = TraceIdContextHandler.getTraceId(ctx, request); + //regex that transform full qualified class into short one: o.e.a.controllers.AdminController for ex + // capture all package name except the last one and replace them by the first letter + String shortQualifiedName = qualifiedName.replaceAll("\\B\\w+(\\.[a-z])","$1"); + if(logRestAccess) { + if (secured) { + log.info(String.format("[%s] Begin secured method : %s", traceId, shortQualifiedName)); + } else { + log.debug(String.format("[%s] Begin method : %s", traceId, shortQualifiedName)); + } + } + request.response().putHeader(TRACE_ID, traceId); + + // if call later and the http response implementation doesn't manage multiple end handler it will be + //erased, not a big deal + if(logRestAccess) { + request.response().endHandler(h -> { + StopWatch watch = TraceIdContextHandler.getTraceTime(ctx); + String mttr = ""; + if (watch != null) { + watch.stop(); + mttr = String.valueOf(watch.getTime(TimeUnit.MILLISECONDS)); + Vertx.currentContext().putLocal(TRACE_MTTR, mttr); + } + if (secured) { + log.info(String.format("[%s] End of secured method : %s", traceId, shortQualifiedName)); + } else { + log.debug(String.format("[%s] End of method : %s", traceId, shortQualifiedName)); + } + }); + } + //invoke the target method on the controller + mh.invokeExact(request); + } + public void registerMethod(String address, String method, boolean local) throws NoSuchMethodException, IllegalAccessException { final MethodHandle mh = lookup.bind(this, method, MethodType.methodType(void.class, Message.class)); - Handler> handler = new Handler>() { - - @Override - public void handle(Message message) { - try { - mh.invokeExact(message); - } catch (Throwable e) { - log.error(e.getMessage(), e); - JsonObject json = new JsonObject().put("status", "error") - .put("message", e.getMessage()); - message.reply(json); - } - } - }; + Handler> handler = message -> { + try { + mh.invokeExact(message); + } catch (Throwable e) { + log.error(e.getMessage(), e); + JsonObject json = new JsonObject().put("status", "error") + .put("message", e.getMessage()); + message.reply(json); + } + }; if (local) { Server.getEventBus(vertx).localConsumer(busPrefix + address, handler); } else { diff --git a/src/main/java/fr/wseduc/webutils/http/TraceIdContextHandler.java b/src/main/java/fr/wseduc/webutils/http/TraceIdContextHandler.java new file mode 100644 index 00000000..8b72f378 --- /dev/null +++ b/src/main/java/fr/wseduc/webutils/http/TraceIdContextHandler.java @@ -0,0 +1,56 @@ +package fr.wseduc.webutils.http; + +import io.vertx.core.Context; +import io.vertx.core.Vertx; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.spi.context.storage.ContextLocal; +import org.apache.commons.lang3.time.StopWatch; + +import java.util.UUID; + +import static fr.wseduc.webutils.Controller.TRACE_ID; + +/** + * Handle trace id management + */ +public class TraceIdContextHandler { + + private static final String TRACE_TIME = "X-Cloud-Trace-Context-Time"; + + public static String getTraceId(Context context, HttpServerRequest request) { + try { + if(context.getLocal(TRACE_ID) == null) { + String traceId = request.getHeader(TRACE_ID); + if(traceId == null) { + traceId = UUID.randomUUID().toString(); + } + context.putLocal(TRACE_ID, traceId); + } + return context.getLocal(TRACE_ID); + } catch (IllegalArgumentException e) { + //we are out of the context, can happen with endHandler or worker if we don't join the context + return ""; + } + } + + public static void setTraceTime(Context context) { + try { + if(context.getLocal(TRACE_TIME) == null) { + context.putLocal(TRACE_TIME, StopWatch.createStarted()); + } + } catch (IllegalArgumentException e) { + //we are out of the context, can happen with endHandler or worker if we don't join the context + } + } + + + public static StopWatch getTraceTime(Context context) { + try { + return context.getLocal(TRACE_TIME); + } catch (IllegalArgumentException e) { + //we are out of the context, can happen with endHandler or worker if we don't join the context + return null; + } + } + +} diff --git a/src/main/java/fr/wseduc/webutils/request/filter/SecurityHandler.java b/src/main/java/fr/wseduc/webutils/request/filter/SecurityHandler.java index e9444de0..f9260865 100644 --- a/src/main/java/fr/wseduc/webutils/request/filter/SecurityHandler.java +++ b/src/main/java/fr/wseduc/webutils/request/filter/SecurityHandler.java @@ -16,22 +16,26 @@ package fr.wseduc.webutils.request.filter; -import java.util.ArrayList; -import java.util.List; - +import fr.wseduc.webutils.http.TraceIdContextHandler; import fr.wseduc.webutils.request.AccessLogger; +import fr.wseduc.webutils.security.SecureHttpServerRequest; import fr.wseduc.webutils.security.XssSecuredHttpServerRequest; +import io.vertx.core.Context; import io.vertx.core.Handler; import io.vertx.core.Vertx; import io.vertx.core.http.HttpServerRequest; -import io.vertx.core.logging.Logger; -import io.vertx.core.logging.LoggerFactory; -import fr.wseduc.webutils.security.SecureHttpServerRequest; +import io.vertx.core.impl.logging.Logger; +import io.vertx.core.impl.logging.LoggerFactory; +import org.apache.commons.lang3.time.StopWatch; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.TimeUnit; /* * Implement a Security Handler with a pre-configurate filters chain */ public abstract class SecurityHandler implements Handler { - private static Logger logger = LoggerFactory.getLogger(SecurityHandler.class); + private static final Logger logger = LoggerFactory.getLogger(SecurityHandler.class); static protected List chain = new ArrayList<>(); static { chain.add(new AccessLoggerFilter(new AccessLogger())); @@ -50,6 +54,12 @@ public void handle(Boolean access) { request.resume(); filter(request); } else { + Context ctx = Vertx.currentContext(); + StopWatch watch = TraceIdContextHandler.getTraceTime(ctx); + if(watch != null) { + watch.stop(); + logger.info(" End of secured method REJECTED : " + request.path() + " in [" + watch.getTime(TimeUnit.MILLISECONDS) + " ms]"); + } chain.get(chain.size() - 1).deny(request); } } @@ -75,6 +85,8 @@ public void handle(Boolean access) { @Override public void handle(HttpServerRequest request) { + final Context ctx = Vertx.currentContext(); + TraceIdContextHandler.setTraceTime(ctx); if (chain != null && !chain.isEmpty()) { SecureHttpServerRequest sr = new XssSecuredHttpServerRequest(request); chain.get(0).canAccess(sr, chainToHandler(sr)); diff --git a/src/main/java/fr/wseduc/webutils/security/WrappedHttpServerRequest.java b/src/main/java/fr/wseduc/webutils/security/WrappedHttpServerRequest.java index fdfd3157..f2bcc3d5 100644 --- a/src/main/java/fr/wseduc/webutils/security/WrappedHttpServerRequest.java +++ b/src/main/java/fr/wseduc/webutils/security/WrappedHttpServerRequest.java @@ -16,8 +16,6 @@ package fr.wseduc.webutils.security; -import java.util.Map; - import fr.wseduc.webutils.http.response.BufferHttpResponse; import fr.wseduc.webutils.request.HttpServerRequestWithBuffering; import fr.wseduc.webutils.request.ProxyHttpRequest; @@ -35,6 +33,7 @@ import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSession; import javax.security.cert.X509Certificate; +import java.util.Map; import java.util.Optional; import java.util.Set; @@ -365,5 +364,4 @@ public Cookie getCookie(String str) public @Nullable Cookie getCookie(String name, String domain, String path) { return request.getCookie(name, domain, path); } - } From 67d4368c91cb2ac8ee73e27ba59d2920b622b2d4 Mon Sep 17 00:00:00 2001 From: mariusestaque Date: Fri, 19 Dec 2025 17:43:01 +0100 Subject: [PATCH 11/11] feat: RBACK-197, support TLS config for http servers --- src/main/java/fr/wseduc/webutils/Server.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/fr/wseduc/webutils/Server.java b/src/main/java/fr/wseduc/webutils/Server.java index 80f707c7..daef8d46 100644 --- a/src/main/java/fr/wseduc/webutils/Server.java +++ b/src/main/java/fr/wseduc/webutils/Server.java @@ -288,7 +288,8 @@ private HttpServerOptions createHttpServerOptions(String httpServerOptions) { JsonObject rawHttpServerOptions = config().getJsonObject("httpServerOptions"); if(rawHttpServerOptions == null && isNotEmpty(httpServerOptions)) { rawHttpServerOptions = new JsonObject(httpServerOptions); - } else { + } + if (rawHttpServerOptions == null) { rawHttpServerOptions = new JsonObject(); } return new HttpServerOptions(rawHttpServerOptions);